summaryrefslogtreecommitdiff
path: root/IkiWiki/Plugin/po.pm
blob: 98c070481978a560d328ecc107dac4396d3a341e (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 refreshpot ($) { #{{{
  94. my $masterfile=shift;
  95. (my $name, my $dir, my $suffix) = fileparse($masterfile, qr/\.[^.]*/);
  96. my $potfile=File::Spec->catfile($dir, $name . ".pot");
  97. my %options = ("markdown" => (pagetype($masterfile) eq 'mdwn') ? 1 : 0);
  98. my $doc=Locale::Po4a::Chooser::new('text',%options);
  99. $doc->read($masterfile);
  100. $doc->{TT}{utf_mode} = 1;
  101. $doc->{TT}{file_in_charset} = 'utf-8';
  102. $doc->{TT}{file_out_charset} = 'utf-8';
  103. $doc->parse;
  104. $doc->writepo($potfile);
  105. } #}}}
  106. sub refreshpofiles ($@) { #{{{
  107. my $masterfile=shift;
  108. my @pofiles=@_;
  109. (my $name, my $dir, my $suffix) = fileparse($masterfile, qr/\.[^.]*/);
  110. my $potfile=File::Spec->catfile($dir, $name . ".pot");
  111. error("[po/refreshpofiles] POT file ($potfile) does not exist") unless (-e $potfile);
  112. foreach my $pofile (@pofiles) {
  113. if (-e $pofile) {
  114. my $cmd = "msgmerge -U $pofile $potfile";
  115. system ($cmd) == 0
  116. or error("[po/refreshpofiles:$pofile] failed to update");
  117. }
  118. else {
  119. File::Copy::syscopy($potfile,$pofile)
  120. or error("[po/refreshpofiles:$pofile] failed to copy the POT file");
  121. }
  122. }
  123. } #}}}
  124. sub needsbuild () { #{{{
  125. my $needsbuild=shift;
  126. # build %translations, using istranslation's side-effect
  127. foreach my $page (keys %pagesources) {
  128. istranslation($page);
  129. }
  130. # refresh POT and PO files as needed
  131. foreach my $file (@$needsbuild) {
  132. my $page=pagename($file);
  133. if (istranslatable($page)) {
  134. refreshpot(srcfile($file));
  135. my @pofiles;
  136. foreach my $lang (keys %{$translations{$page}}) {
  137. push @pofiles, $pagesources{$translations{$page}{$lang}};
  138. }
  139. refreshpofiles(srcfile($file), map { srcfile($_) } @pofiles);
  140. }
  141. }
  142. # make existing translations depend on the corresponding master page
  143. foreach my $master (keys %translations) {
  144. foreach my $slave (values %{$translations{$master}}) {
  145. add_depends($slave, $master);
  146. }
  147. }
  148. } #}}}
  149. sub targetpage (@) { #{{{
  150. my %params = @_;
  151. my $page=$params{page};
  152. my $ext=$params{ext};
  153. if (istranslation($page)) {
  154. my ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  155. if (! $config{usedirs} || $page eq 'index') {
  156. return $masterpage . "." . $lang . "." . $ext;
  157. }
  158. else {
  159. return $masterpage . "/index." . $lang . "." . $ext;
  160. }
  161. }
  162. elsif (istranslatable($page)) {
  163. if (! $config{usedirs} || $page eq 'index') {
  164. return $page . "." . $config{po_master_language}{code} . "." . $ext;
  165. }
  166. else {
  167. return $page . "/index." . $config{po_master_language}{code} . "." . $ext;
  168. }
  169. }
  170. return;
  171. } #}}}
  172. sub tweakurlpath ($) { #{{{
  173. my %params = @_;
  174. my $url=$params{url};
  175. if ($config{po_link_to} eq "negotiated") {
  176. $url =~ s!/index.$config{po_master_language}{code}.$config{htmlext}$!/!;
  177. }
  178. return $url;
  179. } #}}}
  180. sub tweakbestlink ($$) { #{{{
  181. my %params = @_;
  182. my $page=$params{page};
  183. my $link=$params{link};
  184. if ($config{po_link_to} eq "current"
  185. && istranslatable($link)
  186. && istranslation($page)) {
  187. my ($masterpage, $curlang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  188. return $link . "." . $curlang;
  189. }
  190. return $link;
  191. } #}}}
  192. our %filtered;
  193. # We use filter to convert PO to the master page's type,
  194. # since other plugins should not work on PO files
  195. sub filter (@) { #{{{
  196. my %params = @_;
  197. my $page = $params{page};
  198. my $destpage = $params{destpage};
  199. my $content = decode_utf8(encode_utf8($params{content}));
  200. # decide if this is a PO file that should be converted into a translated document,
  201. # and perform various sanity checks
  202. if (! istranslation($page) || $filtered{$page}{$destpage}) {
  203. return $content;
  204. }
  205. my ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  206. my $file=srcfile(exists $params{file} ? $params{file} : $IkiWiki::pagesources{$page});
  207. my $masterfile = srcfile($pagesources{$masterpage});
  208. my (@pos,@masters);
  209. push @pos,$file;
  210. push @masters,$masterfile;
  211. my %options = (
  212. "markdown" => (pagetype($masterfile) eq 'mdwn') ? 1 : 0,
  213. );
  214. my $doc=Locale::Po4a::Chooser::new('text',%options);
  215. $doc->process(
  216. 'po_in_name' => \@pos,
  217. 'file_in_name' => \@masters,
  218. 'file_in_charset' => 'utf-8',
  219. 'file_out_charset' => 'utf-8',
  220. ) or error("[po/filter:$file]: failed to translate");
  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 percenttranslated ($) { #{{{
  238. my $page=shift;
  239. return "N/A" unless (istranslation($page));
  240. my ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  241. my $file=srcfile($pagesources{$page});
  242. my $masterfile = srcfile($pagesources{$masterpage});
  243. my (@pos,@masters);
  244. push @pos,$file;
  245. push @masters,$masterfile;
  246. my %options = (
  247. "markdown" => (pagetype($masterfile) eq 'mdwn') ? 1 : 0,
  248. );
  249. my $doc=Locale::Po4a::Chooser::new('text',%options);
  250. $doc->process(
  251. 'po_in_name' => \@pos,
  252. 'file_in_name' => \@masters,
  253. 'file_in_charset' => 'utf-8',
  254. 'file_out_charset' => 'utf-8',
  255. ) or error("[po/percenttranslated:$file]: failed to translate");
  256. my ($percent,$hit,$queries) = $doc->stats();
  257. return $percent;
  258. } #}}}
  259. sub otherlanguages ($) { #{{{
  260. my $page=shift;
  261. my @ret;
  262. if (istranslatable($page)) {
  263. foreach my $lang (sort keys %{$translations{$page}}) {
  264. my $translation = $translations{$page}{$lang};
  265. push @ret, {
  266. url => urlto($translation, $page),
  267. code => $lang,
  268. language => $config{po_slave_languages}{$lang},
  269. percent => percenttranslated($translation),
  270. };
  271. }
  272. }
  273. elsif (istranslation($page)) {
  274. my ($masterpage, $curlang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  275. push @ret, {
  276. url => urlto($masterpage, $page),
  277. code => $config{po_master_language}{code},
  278. language => $config{po_master_language}{name},
  279. master => 1,
  280. };
  281. foreach my $lang (sort keys %{$translations{$masterpage}}) {
  282. push @ret, {
  283. url => urlto($translations{$masterpage}{$lang}, $page),
  284. code => $lang,
  285. language => $config{po_slave_languages}{$lang},
  286. percent => percenttranslated($page),
  287. } unless ($lang eq $curlang);
  288. }
  289. }
  290. return @ret;
  291. } #}}}
  292. sub pagetemplate (@) { #{{{
  293. my %params=@_;
  294. my $page=$params{page};
  295. my $template=$params{template};
  296. if (istranslation($page) && $template->query(name => "percenttranslated")) {
  297. $template->param(percenttranslated => percenttranslated($page));
  298. }
  299. if ($template->query(name => "otherlanguages")) {
  300. $template->param(otherlanguages => [otherlanguages($page)]);
  301. }
  302. } # }}}
  303. sub istranslatable ($) { #{{{
  304. my $page=shift;
  305. my $file=$pagesources{$page};
  306. if (! defined $file
  307. || (defined pagetype($file) && pagetype($file) eq 'po')
  308. || $file =~ /\.pot$/) {
  309. return 0;
  310. }
  311. return pagespec_match($page, $config{po_translatable_pages});
  312. } #}}}
  313. sub _istranslation ($) { #{{{
  314. my $page=shift;
  315. my $file=$pagesources{$page};
  316. if (! defined $file) {
  317. return IkiWiki::FailReason->new("no file specified");
  318. }
  319. if (! defined $file
  320. || ! defined pagetype($file)
  321. || ! pagetype($file) eq 'po'
  322. || $file =~ /\.pot$/) {
  323. return 0;
  324. }
  325. my ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  326. if (! defined $masterpage || ! defined $lang
  327. || ! (length($masterpage) > 0) || ! (length($lang) > 0)
  328. || ! defined $pagesources{$masterpage}
  329. || ! defined $config{po_slave_languages}{$lang}) {
  330. return 0;
  331. }
  332. return istranslatable($masterpage);
  333. } #}}}
  334. sub istranslation ($) { #{{{
  335. my $page=shift;
  336. if (_istranslation($page)) {
  337. my ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  338. $translations{$masterpage}{$lang}=$page unless exists $translations{$masterpage}{$lang};
  339. return 1;
  340. }
  341. return 0;
  342. } #}}}
  343. package IkiWiki::PageSpec;
  344. use warnings;
  345. use strict;
  346. use IkiWiki 2.00;
  347. sub match_istranslation ($;@) { #{{{
  348. my $page=shift;
  349. if (IkiWiki::Plugin::po::istranslation($page)) {
  350. return IkiWiki::SuccessReason->new("is a translation page");
  351. }
  352. else {
  353. return IkiWiki::FailReason->new("is not a translation page");
  354. }
  355. } #}}}
  356. sub match_istranslatable ($;@) { #{{{
  357. my $page=shift;
  358. if (IkiWiki::Plugin::po::istranslatable($page)) {
  359. return IkiWiki::SuccessReason->new("is set as translatable in po_translatable_pages");
  360. }
  361. else {
  362. return IkiWiki::FailReason->new("is not set as translatable in po_translatable_pages");
  363. }
  364. } #}}}
  365. sub match_lang ($$;@) { #{{{
  366. my $page=shift;
  367. my $wanted=shift;
  368. my $regexp=IkiWiki::glob2re($wanted);
  369. my $lang;
  370. my $masterpage;
  371. if (IkiWiki::Plugin::po::istranslation($page)) {
  372. ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  373. }
  374. else {
  375. $lang = $config{po_master_language}{code};
  376. }
  377. if ($lang!~/^$regexp$/i) {
  378. return IkiWiki::FailReason->new("file language is $lang, not $wanted");
  379. }
  380. else {
  381. return IkiWiki::SuccessReason->new("file language is $wanted");
  382. }
  383. } #}}}
  384. sub match_currentlang ($$;@) { #{{{
  385. my $page=shift;
  386. shift;
  387. my %params=@_;
  388. my ($currentmasterpage, $currentlang, $masterpage, $lang);
  389. return IkiWiki::FailReason->new("no location provided") unless exists $params{location};
  390. if (IkiWiki::Plugin::po::istranslation($params{location})) {
  391. ($currentmasterpage, $currentlang) = ($params{location} =~ /(.*)[.]([a-z]{2})$/);
  392. }
  393. else {
  394. $currentlang = $config{po_master_language}{code};
  395. }
  396. if (IkiWiki::Plugin::po::istranslation($page)) {
  397. ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  398. }
  399. else {
  400. $lang = $config{po_master_language}{code};
  401. }
  402. if ($lang eq $currentlang) {
  403. return IkiWiki::SuccessReason->new("file language is the same as current one, i.e. $currentlang");
  404. }
  405. else {
  406. return IkiWiki::FailReason->new("file language is $lang, whereas current language is $currentlang");
  407. }
  408. } #}}}
  409. 1