summaryrefslogtreecommitdiff
path: root/IkiWiki/Plugin/po.pm
blob: 62a2aa3db2a36a508cb9429c1e4958183ba419d9 (plain)
  1. #!/usr/bin/perl
  2. # .po as a wiki page type
  3. # inspired by the GPL'd po4a-translate,
  4. # which is Copyright 2002, 2003, 2004 by Martin Quinson (mquinson#debian.org)
  5. package IkiWiki::Plugin::po;
  6. use warnings;
  7. use strict;
  8. use IkiWiki 2.00;
  9. use Encode;
  10. use Locale::Po4a::Chooser;
  11. use File::Basename;
  12. use File::Copy;
  13. use File::Spec;
  14. use File::Temp;
  15. use Memoize;
  16. my %translations;
  17. memoize("istranslatable");
  18. memoize("_istranslation");
  19. memoize("percenttranslated");
  20. sub import {
  21. hook(type => "getsetup", id => "po", call => \&getsetup);
  22. hook(type => "checkconfig", id => "po", call => \&checkconfig);
  23. hook(type => "needsbuild", id => "po", call => \&needsbuild);
  24. hook(type => "targetpage", id => "po", call => \&targetpage);
  25. hook(type => "tweakurlpath", id => "po", call => \&tweakurlpath);
  26. hook(type => "tweakbestlink", id => "po", call => \&tweakbestlink);
  27. hook(type => "filter", id => "po", call => \&filter);
  28. hook(type => "htmlize", id => "po", call => \&htmlize);
  29. hook(type => "pagetemplate", id => "po", call => \&pagetemplate);
  30. }
  31. sub getsetup () { #{{{
  32. return
  33. plugin => {
  34. safe => 0,
  35. rebuild => 1, # format plugin
  36. },
  37. po_master_language => {
  38. type => "string",
  39. example => {
  40. 'code' => 'en',
  41. 'name' => 'English'
  42. },
  43. description => "master language (non-PO files)",
  44. safe => 1,
  45. rebuild => 1,
  46. },
  47. po_slave_languages => {
  48. type => "string",
  49. example => {
  50. 'fr' => 'Français',
  51. 'es' => 'Castellano',
  52. 'de' => 'Deutsch'
  53. },
  54. description => "slave languages (PO files)",
  55. safe => 1,
  56. rebuild => 1,
  57. },
  58. po_translatable_pages => {
  59. type => "pagespec",
  60. example => "!*/Discussion",
  61. description => "PageSpec controlling which pages are translatable",
  62. link => "ikiwiki/PageSpec",
  63. safe => 1,
  64. rebuild => 1,
  65. },
  66. po_link_to => {
  67. type => "string",
  68. example => "current",
  69. description => "internal linking behavior (default/current/negotiated)",
  70. safe => 1,
  71. rebuild => 1,
  72. },
  73. } #}}}
  74. sub checkconfig () { #{{{
  75. foreach my $field (qw{po_master_language po_slave_languages}) {
  76. if (! exists $config{$field} || ! defined $config{$field}) {
  77. error(sprintf(gettext("Must specify %s"), $field));
  78. }
  79. }
  80. if (! exists $config{po_link_to} ||
  81. ! defined $config{po_link_to}) {
  82. $config{po_link_to}="default";
  83. }
  84. if (! exists $config{po_translatable_pages} ||
  85. ! defined $config{po_translatable_pages}) {
  86. $config{po_translatable_pages}="";
  87. }
  88. if ($config{po_link_to} eq "negotiated" && ! $config{usedirs}) {
  89. error(gettext("po_link_to=negotiated requires usedirs to be set"));
  90. }
  91. push @{$config{wiki_file_prune_regexps}}, qr/\.pot$/;
  92. } #}}}
  93. sub potfile ($) { #{{{
  94. my $masterfile=shift;
  95. (my $name, my $dir, my $suffix) = fileparse($masterfile, qr/\.[^.]*/);
  96. return File::Spec->catfile($dir, $name . ".pot");
  97. } #}}}
  98. sub pofile ($$) { #{{{
  99. my $masterfile=shift;
  100. my $lang=shift;
  101. (my $name, my $dir, my $suffix) = fileparse($masterfile, qr/\.[^.]*/);
  102. return File::Spec->catfile($dir, $name . "." . $lang . ".po");
  103. } #}}}
  104. sub refreshpot ($) { #{{{
  105. my $masterfile=shift;
  106. my $potfile=potfile($masterfile);
  107. my %options = ("markdown" => (pagetype($masterfile) eq 'mdwn') ? 1 : 0);
  108. my $doc=Locale::Po4a::Chooser::new('text',%options);
  109. $doc->read($masterfile);
  110. $doc->{TT}{utf_mode} = 1;
  111. $doc->{TT}{file_in_charset} = 'utf-8';
  112. $doc->{TT}{file_out_charset} = 'utf-8';
  113. $doc->parse;
  114. $doc->writepo($potfile);
  115. } #}}}
  116. sub refreshpofiles ($@) { #{{{
  117. my $masterfile=shift;
  118. my @pofiles=@_;
  119. my $potfile=potfile($masterfile);
  120. error("[po/refreshpofiles] POT file ($potfile) does not exist") unless (-e $potfile);
  121. foreach my $pofile (@pofiles) {
  122. if (-e $pofile) {
  123. my $cmd = "msgmerge -U $pofile $potfile";
  124. system ($cmd) == 0
  125. or error("[po/refreshpofiles:$pofile] failed to update");
  126. }
  127. else {
  128. File::Copy::syscopy($potfile,$pofile)
  129. or error("[po/refreshpofiles:$pofile] failed to copy the POT file");
  130. }
  131. }
  132. } #}}}
  133. sub needsbuild () { #{{{
  134. my $needsbuild=shift;
  135. # build %translations, using istranslation's side-effect
  136. foreach my $page (keys %pagesources) {
  137. istranslation($page);
  138. }
  139. # refresh/create POT and PO files as needed
  140. foreach my $page (keys %pagesources) {
  141. my $pageneedsbuild = grep { $_ eq $pagesources{$page} } @$needsbuild;
  142. if (istranslatable($page)) {
  143. my $file=srcfile($pagesources{$page});
  144. if ($pageneedsbuild || ! -e potfile($file)) {
  145. refreshpot($file);
  146. }
  147. my @pofiles;
  148. foreach my $lang (keys %{$config{po_slave_languages}}) {
  149. my $pofile=pofile($file, $lang);
  150. if ($pageneedsbuild || ! -e $pofile) {
  151. push @pofiles, $pofile;
  152. }
  153. }
  154. refreshpofiles($file, @pofiles) if (@pofiles);
  155. }
  156. }
  157. # refresh %translations, using istranslation's side-effect
  158. # (not needed yet, but when newly created POT/PO files are
  159. # added to %pagesources and other data structures, we'll need
  160. # this)
  161. foreach my $page (keys %pagesources) {
  162. istranslation($page);
  163. }
  164. # make existing translations depend on the corresponding master page
  165. foreach my $master (keys %translations) {
  166. foreach my $slave (values %{$translations{$master}}) {
  167. add_depends($slave, $master);
  168. }
  169. }
  170. } #}}}
  171. sub targetpage (@) { #{{{
  172. my %params = @_;
  173. my $page=$params{page};
  174. my $ext=$params{ext};
  175. if (istranslation($page)) {
  176. my ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  177. if (! $config{usedirs} || $page eq 'index') {
  178. return $masterpage . "." . $lang . "." . $ext;
  179. }
  180. else {
  181. return $masterpage . "/index." . $lang . "." . $ext;
  182. }
  183. }
  184. elsif (istranslatable($page)) {
  185. if (! $config{usedirs} || $page eq 'index') {
  186. return $page . "." . $config{po_master_language}{code} . "." . $ext;
  187. }
  188. else {
  189. return $page . "/index." . $config{po_master_language}{code} . "." . $ext;
  190. }
  191. }
  192. return;
  193. } #}}}
  194. sub tweakurlpath ($) { #{{{
  195. my %params = @_;
  196. my $url=$params{url};
  197. if ($config{po_link_to} eq "negotiated") {
  198. $url =~ s!/index.$config{po_master_language}{code}.$config{htmlext}$!/!;
  199. }
  200. return $url;
  201. } #}}}
  202. sub tweakbestlink ($$) { #{{{
  203. my %params = @_;
  204. my $page=$params{page};
  205. my $link=$params{link};
  206. if ($config{po_link_to} eq "current"
  207. && istranslatable($link)
  208. && istranslation($page)) {
  209. my ($masterpage, $curlang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  210. return $link . "." . $curlang;
  211. }
  212. return $link;
  213. } #}}}
  214. our %filtered;
  215. # We use filter to convert PO to the master page's type,
  216. # since other plugins should not work on PO files
  217. sub filter (@) { #{{{
  218. my %params = @_;
  219. my $page = $params{page};
  220. my $destpage = $params{destpage};
  221. my $content = decode_utf8(encode_utf8($params{content}));
  222. # decide if this is a PO file that should be converted into a translated document,
  223. # and perform various sanity checks
  224. if (! istranslation($page) || $filtered{$page}{$destpage}) {
  225. return $content;
  226. }
  227. my ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  228. my $file=srcfile(exists $params{file} ? $params{file} : $IkiWiki::pagesources{$page});
  229. my $masterfile = srcfile($pagesources{$masterpage});
  230. my (@pos,@masters);
  231. push @pos,$file;
  232. push @masters,$masterfile;
  233. my %options = (
  234. "markdown" => (pagetype($masterfile) eq 'mdwn') ? 1 : 0,
  235. );
  236. my $doc=Locale::Po4a::Chooser::new('text',%options);
  237. $doc->process(
  238. 'po_in_name' => \@pos,
  239. 'file_in_name' => \@masters,
  240. 'file_in_charset' => 'utf-8',
  241. 'file_out_charset' => 'utf-8',
  242. ) or error("[po/filter:$file]: failed to translate");
  243. my $tmpfh = File::Temp->new(TEMPLATE => "/tmp/ikiwiki-po-filter-out.XXXXXXXXXX");
  244. my $tmpout = $tmpfh->filename;
  245. $doc->write($tmpout) or error("[po/filter:$file] could not write $tmpout");
  246. $content = readfile($tmpout) or error("[po/filter:$file] could not read $tmpout");
  247. $filtered{$page}{$destpage}=1;
  248. return $content;
  249. } #}}}
  250. sub htmlize (@) { #{{{
  251. my %params=@_;
  252. my $page = $params{page};
  253. my $content = $params{content};
  254. my ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  255. my $masterfile = srcfile($pagesources{$masterpage});
  256. # force content to be htmlize'd as if it was the same type as the master page
  257. return IkiWiki::htmlize($page, $page, pagetype($masterfile), $content);
  258. } #}}}
  259. sub percenttranslated ($) { #{{{
  260. my $page=shift;
  261. return "N/A" unless (istranslation($page));
  262. my ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  263. my $file=srcfile($pagesources{$page});
  264. my $masterfile = srcfile($pagesources{$masterpage});
  265. my (@pos,@masters);
  266. push @pos,$file;
  267. push @masters,$masterfile;
  268. my %options = (
  269. "markdown" => (pagetype($masterfile) eq 'mdwn') ? 1 : 0,
  270. );
  271. my $doc=Locale::Po4a::Chooser::new('text',%options);
  272. $doc->process(
  273. 'po_in_name' => \@pos,
  274. 'file_in_name' => \@masters,
  275. 'file_in_charset' => 'utf-8',
  276. 'file_out_charset' => 'utf-8',
  277. ) or error("[po/percenttranslated:$file]: failed to translate");
  278. my ($percent,$hit,$queries) = $doc->stats();
  279. return $percent;
  280. } #}}}
  281. sub otherlanguages ($) { #{{{
  282. my $page=shift;
  283. my @ret;
  284. if (istranslatable($page)) {
  285. foreach my $lang (sort keys %{$translations{$page}}) {
  286. my $translation = $translations{$page}{$lang};
  287. push @ret, {
  288. url => urlto($translation, $page),
  289. code => $lang,
  290. language => $config{po_slave_languages}{$lang},
  291. percent => percenttranslated($translation),
  292. };
  293. }
  294. }
  295. elsif (istranslation($page)) {
  296. my ($masterpage, $curlang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  297. push @ret, {
  298. url => urlto($masterpage, $page),
  299. code => $config{po_master_language}{code},
  300. language => $config{po_master_language}{name},
  301. master => 1,
  302. };
  303. foreach my $lang (sort keys %{$translations{$masterpage}}) {
  304. push @ret, {
  305. url => urlto($translations{$masterpage}{$lang}, $page),
  306. code => $lang,
  307. language => $config{po_slave_languages}{$lang},
  308. percent => percenttranslated($page),
  309. } unless ($lang eq $curlang);
  310. }
  311. }
  312. return @ret;
  313. } #}}}
  314. sub pagetemplate (@) { #{{{
  315. my %params=@_;
  316. my $page=$params{page};
  317. my $template=$params{template};
  318. if (istranslation($page) && $template->query(name => "percenttranslated")) {
  319. $template->param(percenttranslated => percenttranslated($page));
  320. }
  321. if ($template->query(name => "otherlanguages")) {
  322. $template->param(otherlanguages => [otherlanguages($page)]);
  323. }
  324. } # }}}
  325. sub istranslatable ($) { #{{{
  326. my $page=shift;
  327. my $file=$pagesources{$page};
  328. if (! defined $file
  329. || (defined pagetype($file) && pagetype($file) eq 'po')
  330. || $file =~ /\.pot$/) {
  331. return 0;
  332. }
  333. return pagespec_match($page, $config{po_translatable_pages});
  334. } #}}}
  335. sub _istranslation ($) { #{{{
  336. my $page=shift;
  337. my $file=$pagesources{$page};
  338. if (! defined $file) {
  339. return IkiWiki::FailReason->new("no file specified");
  340. }
  341. if (! defined $file
  342. || ! defined pagetype($file)
  343. || ! pagetype($file) eq 'po'
  344. || $file =~ /\.pot$/) {
  345. return 0;
  346. }
  347. my ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  348. if (! defined $masterpage || ! defined $lang
  349. || ! (length($masterpage) > 0) || ! (length($lang) > 0)
  350. || ! defined $pagesources{$masterpage}
  351. || ! defined $config{po_slave_languages}{$lang}) {
  352. return 0;
  353. }
  354. return istranslatable($masterpage);
  355. } #}}}
  356. sub istranslation ($) { #{{{
  357. my $page=shift;
  358. if (_istranslation($page)) {
  359. my ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  360. $translations{$masterpage}{$lang}=$page unless exists $translations{$masterpage}{$lang};
  361. return 1;
  362. }
  363. return 0;
  364. } #}}}
  365. package IkiWiki::PageSpec;
  366. use warnings;
  367. use strict;
  368. use IkiWiki 2.00;
  369. sub match_istranslation ($;@) { #{{{
  370. my $page=shift;
  371. if (IkiWiki::Plugin::po::istranslation($page)) {
  372. return IkiWiki::SuccessReason->new("is a translation page");
  373. }
  374. else {
  375. return IkiWiki::FailReason->new("is not a translation page");
  376. }
  377. } #}}}
  378. sub match_istranslatable ($;@) { #{{{
  379. my $page=shift;
  380. if (IkiWiki::Plugin::po::istranslatable($page)) {
  381. return IkiWiki::SuccessReason->new("is set as translatable in po_translatable_pages");
  382. }
  383. else {
  384. return IkiWiki::FailReason->new("is not set as translatable in po_translatable_pages");
  385. }
  386. } #}}}
  387. sub match_lang ($$;@) { #{{{
  388. my $page=shift;
  389. my $wanted=shift;
  390. my $regexp=IkiWiki::glob2re($wanted);
  391. my $lang;
  392. my $masterpage;
  393. if (IkiWiki::Plugin::po::istranslation($page)) {
  394. ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  395. }
  396. else {
  397. $lang = $config{po_master_language}{code};
  398. }
  399. if ($lang!~/^$regexp$/i) {
  400. return IkiWiki::FailReason->new("file language is $lang, not $wanted");
  401. }
  402. else {
  403. return IkiWiki::SuccessReason->new("file language is $wanted");
  404. }
  405. } #}}}
  406. sub match_currentlang ($$;@) { #{{{
  407. my $page=shift;
  408. shift;
  409. my %params=@_;
  410. my ($currentmasterpage, $currentlang, $masterpage, $lang);
  411. return IkiWiki::FailReason->new("no location provided") unless exists $params{location};
  412. if (IkiWiki::Plugin::po::istranslation($params{location})) {
  413. ($currentmasterpage, $currentlang) = ($params{location} =~ /(.*)[.]([a-z]{2})$/);
  414. }
  415. else {
  416. $currentlang = $config{po_master_language}{code};
  417. }
  418. if (IkiWiki::Plugin::po::istranslation($page)) {
  419. ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  420. }
  421. else {
  422. $lang = $config{po_master_language}{code};
  423. }
  424. if ($lang eq $currentlang) {
  425. return IkiWiki::SuccessReason->new("file language is the same as current one, i.e. $currentlang");
  426. }
  427. else {
  428. return IkiWiki::FailReason->new("file language is $lang, whereas current language is $currentlang");
  429. }
  430. } #}}}
  431. 1