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