summaryrefslogtreecommitdiff
path: root/IkiWiki/Plugin/po.pm
blob: 43b36430f7267cd3abb5d808185971c376be285a (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. # make existing translations depend on the corresponding master page
  158. foreach my $master (keys %translations) {
  159. foreach my $slave (values %{$translations{$master}}) {
  160. add_depends($slave, $master);
  161. }
  162. }
  163. } #}}}
  164. sub targetpage (@) { #{{{
  165. my %params = @_;
  166. my $page=$params{page};
  167. my $ext=$params{ext};
  168. if (istranslation($page)) {
  169. my ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  170. if (! $config{usedirs} || $page eq 'index') {
  171. return $masterpage . "." . $lang . "." . $ext;
  172. }
  173. else {
  174. return $masterpage . "/index." . $lang . "." . $ext;
  175. }
  176. }
  177. elsif (istranslatable($page)) {
  178. if (! $config{usedirs} || $page eq 'index') {
  179. return $page . "." . $config{po_master_language}{code} . "." . $ext;
  180. }
  181. else {
  182. return $page . "/index." . $config{po_master_language}{code} . "." . $ext;
  183. }
  184. }
  185. return;
  186. } #}}}
  187. sub tweakurlpath ($) { #{{{
  188. my %params = @_;
  189. my $url=$params{url};
  190. if ($config{po_link_to} eq "negotiated") {
  191. $url =~ s!/index.$config{po_master_language}{code}.$config{htmlext}$!/!;
  192. }
  193. return $url;
  194. } #}}}
  195. sub tweakbestlink ($$) { #{{{
  196. my %params = @_;
  197. my $page=$params{page};
  198. my $link=$params{link};
  199. if ($config{po_link_to} eq "current"
  200. && istranslatable($link)
  201. && istranslation($page)) {
  202. my ($masterpage, $curlang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  203. return $link . "." . $curlang;
  204. }
  205. return $link;
  206. } #}}}
  207. our %filtered;
  208. # We use filter to convert PO to the master page's type,
  209. # since other plugins should not work on PO files
  210. sub filter (@) { #{{{
  211. my %params = @_;
  212. my $page = $params{page};
  213. my $destpage = $params{destpage};
  214. my $content = decode_utf8(encode_utf8($params{content}));
  215. # decide if this is a PO file that should be converted into a translated document,
  216. # and perform various sanity checks
  217. if (! istranslation($page) || $filtered{$page}{$destpage}) {
  218. return $content;
  219. }
  220. my ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  221. my $file=srcfile(exists $params{file} ? $params{file} : $IkiWiki::pagesources{$page});
  222. my $masterfile = srcfile($pagesources{$masterpage});
  223. my (@pos,@masters);
  224. push @pos,$file;
  225. push @masters,$masterfile;
  226. my %options = (
  227. "markdown" => (pagetype($masterfile) eq 'mdwn') ? 1 : 0,
  228. );
  229. my $doc=Locale::Po4a::Chooser::new('text',%options);
  230. $doc->process(
  231. 'po_in_name' => \@pos,
  232. 'file_in_name' => \@masters,
  233. 'file_in_charset' => 'utf-8',
  234. 'file_out_charset' => 'utf-8',
  235. ) or error("[po/filter:$file]: failed to translate");
  236. my $tmpfh = File::Temp->new(TEMPLATE => "/tmp/ikiwiki-po-filter-out.XXXXXXXXXX");
  237. my $tmpout = $tmpfh->filename;
  238. $doc->write($tmpout) or error("[po/filter:$file] could not write $tmpout");
  239. $content = readfile($tmpout) or error("[po/filter:$file] could not read $tmpout");
  240. $filtered{$page}{$destpage}=1;
  241. return $content;
  242. } #}}}
  243. sub htmlize (@) { #{{{
  244. my %params=@_;
  245. my $page = $params{page};
  246. my $content = $params{content};
  247. my ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  248. my $masterfile = srcfile($pagesources{$masterpage});
  249. # force content to be htmlize'd as if it was the same type as the master page
  250. return IkiWiki::htmlize($page, $page, pagetype($masterfile), $content);
  251. } #}}}
  252. sub percenttranslated ($) { #{{{
  253. my $page=shift;
  254. return "N/A" unless (istranslation($page));
  255. my ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  256. my $file=srcfile($pagesources{$page});
  257. my $masterfile = srcfile($pagesources{$masterpage});
  258. my (@pos,@masters);
  259. push @pos,$file;
  260. push @masters,$masterfile;
  261. my %options = (
  262. "markdown" => (pagetype($masterfile) eq 'mdwn') ? 1 : 0,
  263. );
  264. my $doc=Locale::Po4a::Chooser::new('text',%options);
  265. $doc->process(
  266. 'po_in_name' => \@pos,
  267. 'file_in_name' => \@masters,
  268. 'file_in_charset' => 'utf-8',
  269. 'file_out_charset' => 'utf-8',
  270. ) or error("[po/percenttranslated:$file]: failed to translate");
  271. my ($percent,$hit,$queries) = $doc->stats();
  272. return $percent;
  273. } #}}}
  274. sub otherlanguages ($) { #{{{
  275. my $page=shift;
  276. my @ret;
  277. if (istranslatable($page)) {
  278. foreach my $lang (sort keys %{$translations{$page}}) {
  279. my $translation = $translations{$page}{$lang};
  280. push @ret, {
  281. url => urlto($translation, $page),
  282. code => $lang,
  283. language => $config{po_slave_languages}{$lang},
  284. percent => percenttranslated($translation),
  285. };
  286. }
  287. }
  288. elsif (istranslation($page)) {
  289. my ($masterpage, $curlang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  290. push @ret, {
  291. url => urlto($masterpage, $page),
  292. code => $config{po_master_language}{code},
  293. language => $config{po_master_language}{name},
  294. master => 1,
  295. };
  296. foreach my $lang (sort keys %{$translations{$masterpage}}) {
  297. push @ret, {
  298. url => urlto($translations{$masterpage}{$lang}, $page),
  299. code => $lang,
  300. language => $config{po_slave_languages}{$lang},
  301. percent => percenttranslated($page),
  302. } unless ($lang eq $curlang);
  303. }
  304. }
  305. return @ret;
  306. } #}}}
  307. sub pagetemplate (@) { #{{{
  308. my %params=@_;
  309. my $page=$params{page};
  310. my $template=$params{template};
  311. if (istranslation($page) && $template->query(name => "percenttranslated")) {
  312. $template->param(percenttranslated => percenttranslated($page));
  313. }
  314. if ($template->query(name => "otherlanguages")) {
  315. $template->param(otherlanguages => [otherlanguages($page)]);
  316. }
  317. } # }}}
  318. sub istranslatable ($) { #{{{
  319. my $page=shift;
  320. my $file=$pagesources{$page};
  321. if (! defined $file
  322. || (defined pagetype($file) && pagetype($file) eq 'po')
  323. || $file =~ /\.pot$/) {
  324. return 0;
  325. }
  326. return pagespec_match($page, $config{po_translatable_pages});
  327. } #}}}
  328. sub _istranslation ($) { #{{{
  329. my $page=shift;
  330. my $file=$pagesources{$page};
  331. if (! defined $file) {
  332. return IkiWiki::FailReason->new("no file specified");
  333. }
  334. if (! defined $file
  335. || ! defined pagetype($file)
  336. || ! pagetype($file) eq 'po'
  337. || $file =~ /\.pot$/) {
  338. return 0;
  339. }
  340. my ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  341. if (! defined $masterpage || ! defined $lang
  342. || ! (length($masterpage) > 0) || ! (length($lang) > 0)
  343. || ! defined $pagesources{$masterpage}
  344. || ! defined $config{po_slave_languages}{$lang}) {
  345. return 0;
  346. }
  347. return istranslatable($masterpage);
  348. } #}}}
  349. sub istranslation ($) { #{{{
  350. my $page=shift;
  351. if (_istranslation($page)) {
  352. my ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  353. $translations{$masterpage}{$lang}=$page unless exists $translations{$masterpage}{$lang};
  354. return 1;
  355. }
  356. return 0;
  357. } #}}}
  358. package IkiWiki::PageSpec;
  359. use warnings;
  360. use strict;
  361. use IkiWiki 2.00;
  362. sub match_istranslation ($;@) { #{{{
  363. my $page=shift;
  364. if (IkiWiki::Plugin::po::istranslation($page)) {
  365. return IkiWiki::SuccessReason->new("is a translation page");
  366. }
  367. else {
  368. return IkiWiki::FailReason->new("is not a translation page");
  369. }
  370. } #}}}
  371. sub match_istranslatable ($;@) { #{{{
  372. my $page=shift;
  373. if (IkiWiki::Plugin::po::istranslatable($page)) {
  374. return IkiWiki::SuccessReason->new("is set as translatable in po_translatable_pages");
  375. }
  376. else {
  377. return IkiWiki::FailReason->new("is not set as translatable in po_translatable_pages");
  378. }
  379. } #}}}
  380. sub match_lang ($$;@) { #{{{
  381. my $page=shift;
  382. my $wanted=shift;
  383. my $regexp=IkiWiki::glob2re($wanted);
  384. my $lang;
  385. my $masterpage;
  386. if (IkiWiki::Plugin::po::istranslation($page)) {
  387. ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  388. }
  389. else {
  390. $lang = $config{po_master_language}{code};
  391. }
  392. if ($lang!~/^$regexp$/i) {
  393. return IkiWiki::FailReason->new("file language is $lang, not $wanted");
  394. }
  395. else {
  396. return IkiWiki::SuccessReason->new("file language is $wanted");
  397. }
  398. } #}}}
  399. sub match_currentlang ($$;@) { #{{{
  400. my $page=shift;
  401. shift;
  402. my %params=@_;
  403. my ($currentmasterpage, $currentlang, $masterpage, $lang);
  404. return IkiWiki::FailReason->new("no location provided") unless exists $params{location};
  405. if (IkiWiki::Plugin::po::istranslation($params{location})) {
  406. ($currentmasterpage, $currentlang) = ($params{location} =~ /(.*)[.]([a-z]{2})$/);
  407. }
  408. else {
  409. $currentlang = $config{po_master_language}{code};
  410. }
  411. if (IkiWiki::Plugin::po::istranslation($page)) {
  412. ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  413. }
  414. else {
  415. $lang = $config{po_master_language}{code};
  416. }
  417. if ($lang eq $currentlang) {
  418. return IkiWiki::SuccessReason->new("file language is the same as current one, i.e. $currentlang");
  419. }
  420. else {
  421. return IkiWiki::FailReason->new("file language is $lang, whereas current language is $currentlang");
  422. }
  423. } #}}}
  424. 1