summaryrefslogtreecommitdiff
path: root/IkiWiki/Plugin/po.pm
blob: 5abe9d4195863d0bbd697d2e45370e7643826c62 (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. sub import {
  20. hook(type => "getsetup", id => "po", call => \&getsetup);
  21. hook(type => "checkconfig", id => "po", call => \&checkconfig);
  22. hook(type => "needsbuild", id => "po", call => \&needsbuild);
  23. hook(type => "targetpage", id => "po", call => \&targetpage);
  24. hook(type => "tweakurlpath", id => "po", call => \&tweakurlpath);
  25. hook(type => "tweakbestlink", id => "po", call => \&tweakbestlink);
  26. hook(type => "filter", id => "po", call => \&filter);
  27. hook(type => "htmlize", id => "po", call => \&htmlize);
  28. hook(type => "pagetemplate", id => "po", call => \&pagetemplate);
  29. }
  30. sub getsetup () { #{{{
  31. return
  32. plugin => {
  33. safe => 0,
  34. rebuild => 1, # format plugin
  35. },
  36. po_master_language => {
  37. type => "string",
  38. example => {
  39. 'code' => 'en',
  40. 'name' => 'English'
  41. },
  42. description => "master language (non-PO files)",
  43. safe => 1,
  44. rebuild => 1,
  45. },
  46. po_slave_languages => {
  47. type => "string",
  48. example => {
  49. 'fr' => 'Français',
  50. 'es' => 'Castellano',
  51. 'de' => 'Deutsch'
  52. },
  53. description => "slave languages (PO files)",
  54. safe => 1,
  55. rebuild => 1,
  56. },
  57. po_translatable_pages => {
  58. type => "pagespec",
  59. example => "!*/Discussion",
  60. description => "PageSpec controlling which pages are translatable",
  61. link => "ikiwiki/PageSpec",
  62. safe => 1,
  63. rebuild => 1,
  64. },
  65. po_link_to => {
  66. type => "string",
  67. example => "current",
  68. description => "internal linking behavior (default/current/negotiated)",
  69. safe => 1,
  70. rebuild => 1,
  71. },
  72. } #}}}
  73. sub checkconfig () { #{{{
  74. foreach my $field (qw{po_master_language po_slave_languages}) {
  75. if (! exists $config{$field} || ! defined $config{$field}) {
  76. error(sprintf(gettext("Must specify %s"), $field));
  77. }
  78. }
  79. if (! exists $config{po_link_to} ||
  80. ! defined $config{po_link_to}) {
  81. $config{po_link_to}="default";
  82. }
  83. if (! exists $config{po_translatable_pages} ||
  84. ! defined $config{po_translatable_pages}) {
  85. $config{po_translatable_pages}="";
  86. }
  87. if ($config{po_link_to} eq "negotiated" && ! $config{usedirs}) {
  88. error(gettext("po_link_to=negotiated requires usedirs to be set"));
  89. }
  90. push @{$config{wiki_file_prune_regexps}}, qr/\.pot$/;
  91. } #}}}
  92. sub refreshpot ($) { #{{{
  93. my $masterfile=shift;
  94. (my $name, my $dir, my $suffix) = fileparse($masterfile, qr/\.[^.]*/);
  95. my $potfile=File::Spec->catfile($dir, $name . ".pot");
  96. my %options = ("markdown" => (pagetype($masterfile) eq 'mdwn') ? 1 : 0);
  97. my $doc=Locale::Po4a::Chooser::new('text',%options);
  98. $doc->read($masterfile);
  99. $doc->{TT}{utf_mode} = 1;
  100. $doc->{TT}{file_in_charset} = 'utf-8';
  101. $doc->{TT}{file_out_charset} = 'utf-8';
  102. $doc->parse;
  103. $doc->writepo($potfile);
  104. } #}}}
  105. sub refreshpofiles ($@) { #{{{
  106. my $masterfile=shift;
  107. my @pofiles=@_;
  108. (my $name, my $dir, my $suffix) = fileparse($masterfile, qr/\.[^.]*/);
  109. my $potfile=File::Spec->catfile($dir, $name . ".pot");
  110. error("[po/refreshpofiles] POT file ($potfile) does not exist") unless (-e $potfile);
  111. foreach my $pofile (@pofiles) {
  112. if (-e $pofile) {
  113. my $cmd = "msgmerge -U $pofile $potfile";
  114. system ($cmd) == 0
  115. or error("[po/refreshpofiles:$pofile] failed to update");
  116. }
  117. else {
  118. File::Copy::syscopy($potfile,$pofile)
  119. or error("[po/refreshpofiles:$pofile] failed to copy the POT file");
  120. }
  121. }
  122. } #}}}
  123. sub needsbuild () { #{{{
  124. my $needsbuild=shift;
  125. # build %translations, using istranslation's side-effect
  126. foreach my $page (keys %pagesources) {
  127. istranslation($page);
  128. }
  129. # refresh POT and PO files as needed
  130. foreach my $file (@$needsbuild) {
  131. my $page=pagename($file);
  132. if (istranslatable($page)) {
  133. refreshpot(srcfile($file));
  134. my @pofiles;
  135. foreach my $lang (keys %{$translations{$page}}) {
  136. push @pofiles, $pagesources{$translations{$page}{$lang}};
  137. }
  138. refreshpofiles(srcfile($file), map { srcfile($_) } @pofiles);
  139. }
  140. }
  141. # make existing translations depend on the corresponding master page
  142. foreach my $master (keys %translations) {
  143. foreach my $slave (values %{$translations{$master}}) {
  144. add_depends($slave, $master);
  145. }
  146. }
  147. } #}}}
  148. sub targetpage (@) { #{{{
  149. my %params = @_;
  150. my $page=$params{page};
  151. my $ext=$params{ext};
  152. if (istranslation($page)) {
  153. my ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  154. if (! $config{usedirs} || $page eq 'index') {
  155. return $masterpage . "." . $lang . "." . $ext;
  156. }
  157. else {
  158. return $masterpage . "/index." . $lang . "." . $ext;
  159. }
  160. }
  161. elsif (istranslatable($page)) {
  162. if (! $config{usedirs} || $page eq 'index') {
  163. return $page . "." . $config{po_master_language}{code} . "." . $ext;
  164. }
  165. else {
  166. return $page . "/index." . $config{po_master_language}{code} . "." . $ext;
  167. }
  168. }
  169. return;
  170. } #}}}
  171. sub tweakurlpath ($) { #{{{
  172. my %params = @_;
  173. my $url=$params{url};
  174. if ($config{po_link_to} eq "negotiated") {
  175. $url =~ s!/index.$config{po_master_language}{code}.$config{htmlext}$!/!;
  176. }
  177. return $url;
  178. } #}}}
  179. sub tweakbestlink ($$) { #{{{
  180. my %params = @_;
  181. my $page=$params{page};
  182. my $link=$params{link};
  183. if ($config{po_link_to} eq "current"
  184. && istranslatable($link)
  185. && istranslation($page)) {
  186. my ($masterpage, $curlang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  187. return $link . "." . $curlang;
  188. }
  189. return $link;
  190. } #}}}
  191. our %filtered;
  192. # We use filter to convert PO to the master page's type,
  193. # since other plugins should not work on PO files
  194. sub filter (@) { #{{{
  195. my %params = @_;
  196. my $page = $params{page};
  197. my $destpage = $params{destpage};
  198. my $content = decode_utf8(encode_utf8($params{content}));
  199. # decide if this is a PO file that should be converted into a translated document,
  200. # and perform various sanity checks
  201. if (! istranslation($page) || $filtered{$page}{$destpage}) {
  202. return $content;
  203. }
  204. my ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  205. my $file=srcfile(exists $params{file} ? $params{file} : $IkiWiki::pagesources{$page});
  206. my $masterfile = srcfile($pagesources{$masterpage});
  207. my (@pos,@masters);
  208. push @pos,$file;
  209. push @masters,$masterfile;
  210. my %options = (
  211. "markdown" => (pagetype($masterfile) eq 'mdwn') ? 1 : 0,
  212. );
  213. my $doc=Locale::Po4a::Chooser::new('text',%options);
  214. $doc->process(
  215. 'po_in_name' => \@pos,
  216. 'file_in_name' => \@masters,
  217. 'file_in_charset' => 'utf-8',
  218. 'file_out_charset' => 'utf-8',
  219. ) or error("[po/filter:$file]: failed to translate");
  220. my ($percent,$hit,$queries) = $doc->stats();
  221. my $tmpfh = File::Temp->new(TEMPLATE => "/tmp/ikiwiki-po-filter-out.XXXXXXXXXX");
  222. my $tmpout = $tmpfh->filename;
  223. $doc->write($tmpout) or error("[po/filter:$file] could not write $tmpout");
  224. $content = readfile($tmpout) or error("[po/filter:$file] could not read $tmpout");
  225. $filtered{$page}{$destpage}=1;
  226. return $content;
  227. } #}}}
  228. sub htmlize (@) { #{{{
  229. my %params=@_;
  230. my $page = $params{page};
  231. my $content = $params{content};
  232. my ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  233. my $masterfile = srcfile($pagesources{$masterpage});
  234. # force content to be htmlize'd as if it was the same type as the master page
  235. return IkiWiki::htmlize($page, $page, pagetype($masterfile), $content);
  236. } #}}}
  237. sub otherlanguages ($) { #{{{
  238. my $page=shift;
  239. my @ret;
  240. if (istranslatable($page)) {
  241. foreach my $lang (sort keys %{$translations{$page}}) {
  242. push @ret, {
  243. url => urlto($translations{$page}{$lang}, $page),
  244. code => $lang,
  245. language => $config{po_slave_languages}{$lang},
  246. master => 0,
  247. };
  248. }
  249. }
  250. elsif (istranslation($page)) {
  251. my ($masterpage, $curlang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  252. push @ret, {
  253. url => urlto($masterpage, $page),
  254. code => $config{po_master_language}{code},
  255. language => $config{po_master_language}{name},
  256. master => 1,
  257. };
  258. foreach my $lang (sort keys %{$translations{$masterpage}}) {
  259. push @ret, {
  260. url => urlto($translations{$masterpage}{$lang}, $page),
  261. code => $lang,
  262. language => $config{po_slave_languages}{$lang},
  263. master => 0,
  264. } unless ($lang eq $curlang);
  265. }
  266. }
  267. return @ret;
  268. } #}}}
  269. sub pagetemplate (@) { #{{{
  270. my %params=@_;
  271. my $page=$params{page};
  272. my $template=$params{template};
  273. if ($template->query(name => "otherlanguages")) {
  274. $template->param(otherlanguages => [otherlanguages($page)]);
  275. }
  276. } # }}}
  277. sub istranslatable ($) { #{{{
  278. my $page=shift;
  279. my $file=$pagesources{$page};
  280. if (! defined $file
  281. || (defined pagetype($file) && pagetype($file) eq 'po')
  282. || $file =~ /\.pot$/) {
  283. return 0;
  284. }
  285. return pagespec_match($page, $config{po_translatable_pages});
  286. } #}}}
  287. sub _istranslation ($) { #{{{
  288. my $page=shift;
  289. my $file=$pagesources{$page};
  290. if (! defined $file) {
  291. return IkiWiki::FailReason->new("no file specified");
  292. }
  293. if (! defined $file
  294. || ! defined pagetype($file)
  295. || ! pagetype($file) eq 'po'
  296. || $file =~ /\.pot$/) {
  297. return 0;
  298. }
  299. my ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  300. if (! defined $masterpage || ! defined $lang
  301. || ! (length($masterpage) > 0) || ! (length($lang) > 0)
  302. || ! defined $pagesources{$masterpage}
  303. || ! defined $config{po_slave_languages}{$lang}) {
  304. return 0;
  305. }
  306. return istranslatable($masterpage);
  307. } #}}}
  308. sub istranslation ($) { #{{{
  309. my $page=shift;
  310. if (_istranslation($page)) {
  311. my ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  312. $translations{$masterpage}{$lang}=$page unless exists $translations{$masterpage}{$lang};
  313. return 1;
  314. }
  315. return 0;
  316. } #}}}
  317. package IkiWiki::PageSpec;
  318. use warnings;
  319. use strict;
  320. use IkiWiki 2.00;
  321. sub match_istranslation ($;@) { #{{{
  322. my $page=shift;
  323. if (IkiWiki::Plugin::po::istranslation($page)) {
  324. return IkiWiki::SuccessReason->new("is a translation page");
  325. }
  326. else {
  327. return IkiWiki::FailReason->new("is not a translation page");
  328. }
  329. } #}}}
  330. sub match_istranslatable ($;@) { #{{{
  331. my $page=shift;
  332. if (IkiWiki::Plugin::po::istranslatable($page)) {
  333. return IkiWiki::SuccessReason->new("is set as translatable in po_translatable_pages");
  334. }
  335. else {
  336. return IkiWiki::FailReason->new("is not set as translatable in po_translatable_pages");
  337. }
  338. } #}}}
  339. 1