summaryrefslogtreecommitdiff
path: root/IkiWiki/Plugin/po.pm
blob: 205066341fc2105b436d7439312729c5dc06e680 (plain)
  1. #!/usr/bin/perl
  2. # .po as a wiki page type
  3. # Licensed under GPL v2 or greater
  4. # Copyright (C) 2008 intrigeri <intrigeri@boum.org>
  5. # inspired by the GPL'd po4a-translate,
  6. # which is Copyright 2002, 2003, 2004 by Martin Quinson (mquinson#debian.org)
  7. package IkiWiki::Plugin::po;
  8. use warnings;
  9. use strict;
  10. use IkiWiki 2.00;
  11. use Encode;
  12. use Locale::Po4a::Chooser;
  13. use Locale::Po4a::Po;
  14. use File::Basename;
  15. use File::Copy;
  16. use File::Spec;
  17. use File::Temp;
  18. use Memoize;
  19. use UNIVERSAL;
  20. my %translations;
  21. my @origneedsbuild;
  22. my %origsubs;
  23. memoize("istranslatable");
  24. memoize("_istranslation");
  25. memoize("percenttranslated");
  26. sub import {
  27. hook(type => "getsetup", id => "po", call => \&getsetup);
  28. hook(type => "checkconfig", id => "po", call => \&checkconfig);
  29. hook(type => "needsbuild", id => "po", call => \&needsbuild);
  30. hook(type => "scan", id => "po", call => \&scan, last =>1);
  31. hook(type => "filter", id => "po", call => \&filter);
  32. hook(type => "htmlize", id => "po", call => \&htmlize);
  33. hook(type => "pagetemplate", id => "po", call => \&pagetemplate, last => 1);
  34. hook(type => "postscan", id => "po", call => \&postscan);
  35. hook(type => "rename", id => "po", call => \&renamepages);
  36. hook(type => "delete", id => "po", call => \&mydelete);
  37. hook(type => "change", id => "po", call => \&change);
  38. hook(type => "editcontent", id => "po", call => \&editcontent);
  39. $origsubs{'bestlink'}=\&IkiWiki::bestlink;
  40. inject(name => "IkiWiki::bestlink", call => \&mybestlink);
  41. $origsubs{'beautify_urlpath'}=\&IkiWiki::beautify_urlpath;
  42. inject(name => "IkiWiki::beautify_urlpath", call => \&mybeautify_urlpath);
  43. $origsubs{'targetpage'}=\&IkiWiki::targetpage;
  44. inject(name => "IkiWiki::targetpage", call => \&mytargetpage);
  45. $origsubs{'urlto'}=\&IkiWiki::urlto;
  46. inject(name => "IkiWiki::urlto", call => \&myurlto);
  47. $origsubs{'nicepagetitle'}=\&IkiWiki::nicepagetitle;
  48. inject(name => "IkiWiki::nicepagetitle", call => \&mynicepagetitle);
  49. }
  50. # ,----
  51. # | Table of contents
  52. # `----
  53. # 1. Hooks
  54. # 2. Injected functions
  55. # 3. Blackboxes for private data
  56. # 4. Helper functions
  57. # 5. PageSpec's
  58. # ,----
  59. # | Hooks
  60. # `----
  61. sub getsetup () {
  62. return
  63. plugin => {
  64. safe => 0,
  65. rebuild => 1,
  66. },
  67. po_master_language => {
  68. type => "string",
  69. example => {
  70. 'code' => 'en',
  71. 'name' => 'English'
  72. },
  73. description => "master language (non-PO files)",
  74. safe => 1,
  75. rebuild => 1,
  76. },
  77. po_slave_languages => {
  78. type => "string",
  79. example => {
  80. 'fr' => 'Français',
  81. 'es' => 'Castellano',
  82. 'de' => 'Deutsch'
  83. },
  84. description => "slave languages (PO files)",
  85. safe => 1,
  86. rebuild => 1,
  87. },
  88. po_translatable_pages => {
  89. type => "pagespec",
  90. example => "!*/Discussion",
  91. description => "PageSpec controlling which pages are translatable",
  92. link => "ikiwiki/PageSpec",
  93. safe => 1,
  94. rebuild => 1,
  95. },
  96. po_link_to => {
  97. type => "string",
  98. example => "current",
  99. description => "internal linking behavior (default/current/negotiated)",
  100. safe => 1,
  101. rebuild => 1,
  102. },
  103. po_translation_status_in_links => {
  104. type => "boolean",
  105. example => 1,
  106. description => "display translation status in links to translations",
  107. safe => 1,
  108. rebuild => 1,
  109. },
  110. }
  111. sub checkconfig () {
  112. foreach my $field (qw{po_master_language po_slave_languages}) {
  113. if (! exists $config{$field} || ! defined $config{$field}) {
  114. error(sprintf(gettext("Must specify %s"), $field));
  115. }
  116. }
  117. if (! (keys %{$config{po_slave_languages}})) {
  118. error(gettext("At least one slave language must be defined in po_slave_languages"));
  119. }
  120. map {
  121. islanguagecode($_)
  122. or error(sprintf(gettext("%s is not a valid language code"), $_));
  123. } ($config{po_master_language}{code}, keys %{$config{po_slave_languages}});
  124. if (! exists $config{po_translatable_pages} ||
  125. ! defined $config{po_translatable_pages}) {
  126. $config{po_translatable_pages}="";
  127. }
  128. if (! exists $config{po_link_to} ||
  129. ! defined $config{po_link_to}) {
  130. $config{po_link_to}='default';
  131. }
  132. elsif (! grep {
  133. $config{po_link_to} eq $_
  134. } ('default', 'current', 'negotiated')) {
  135. warn(sprintf(gettext('po_link_to=%s is not a valid setting, falling back to po_link_to=default'),
  136. $config{po_link_to}));
  137. $config{po_link_to}='default';
  138. }
  139. elsif ($config{po_link_to} eq "negotiated" && ! $config{usedirs}) {
  140. warn(gettext('po_link_to=negotiated requires usedirs to be enabled, falling back to po_link_to=default'));
  141. $config{po_link_to}='default';
  142. }
  143. if (! exists $config{po_translation_status_in_links} ||
  144. ! defined $config{po_translation_status_in_links}) {
  145. $config{po_translation_status_in_links}=1;
  146. }
  147. push @{$config{wiki_file_prune_regexps}}, qr/\.pot$/;
  148. }
  149. sub needsbuild () {
  150. my $needsbuild=shift;
  151. # backup @needsbuild content so that change() can know whether
  152. # a given master page was rendered because its source file was changed
  153. @origneedsbuild=(@$needsbuild);
  154. flushmemoizecache();
  155. buildtranslationscache();
  156. # make existing translations depend on the corresponding master page
  157. foreach my $master (keys %translations) {
  158. map add_depends($_, $master), values %{otherlanguages($master)};
  159. }
  160. }
  161. # Massage the recorded state of internal links so that:
  162. # - it matches the actually generated links, rather than the links as written
  163. # in the pages' source
  164. # - backlinks are consistent in all cases
  165. sub scan (@) {
  166. my %params=@_;
  167. my $page=$params{page};
  168. my $content=$params{content};
  169. return unless UNIVERSAL::can("IkiWiki::Plugin::link", "import");
  170. if (istranslation($page)) {
  171. foreach my $destpage (@{$links{$page}}) {
  172. if (istranslatable($destpage)) {
  173. # replace one occurence of $destpage in $links{$page}
  174. # (we only want to replace the one that was added by
  175. # IkiWiki::Plugin::link::scan, other occurences may be
  176. # there for other reasons)
  177. for (my $i=0; $i<@{$links{$page}}; $i++) {
  178. if (@{$links{$page}}[$i] eq $destpage) {
  179. @{$links{$page}}[$i] = $destpage . '.' . lang($page);
  180. last;
  181. }
  182. }
  183. }
  184. }
  185. }
  186. elsif (! istranslatable($page) && ! istranslation($page)) {
  187. foreach my $destpage (@{$links{$page}}) {
  188. if (istranslatable($destpage)) {
  189. # make sure any destpage's translations has
  190. # $page in its backlinks
  191. push @{$links{$page}},
  192. values %{otherlanguages($destpage)};
  193. }
  194. }
  195. }
  196. }
  197. # We use filter to convert PO to the master page's format,
  198. # since the rest of ikiwiki should not work on PO files.
  199. sub filter (@) {
  200. my %params = @_;
  201. my $page = $params{page};
  202. my $destpage = $params{destpage};
  203. my $content = decode_utf8(encode_utf8($params{content}));
  204. return $content if ( ! istranslation($page)
  205. || alreadyfiltered($page, $destpage) );
  206. # CRLF line terminators make poor Locale::Po4a feel bad
  207. $content=~s/\r\n/\n/g;
  208. # There are incompatibilities between some File::Temp versions
  209. # (including 0.18, bundled with Lenny's perl-modules package)
  210. # and others (e.g. 0.20, previously present in the archive as
  211. # a standalone package): under certain circumstances, some
  212. # return a relative filename, whereas others return an absolute one;
  213. # we here use this module in a way that is at least compatible
  214. # with 0.18 and 0.20. Beware, hit'n'run refactorers!
  215. my $infile = new File::Temp(TEMPLATE => "ikiwiki-po-filter-in.XXXXXXXXXX",
  216. DIR => File::Spec->tmpdir,
  217. UNLINK => 1)->filename;
  218. my $outfile = new File::Temp(TEMPLATE => "ikiwiki-po-filter-out.XXXXXXXXXX",
  219. DIR => File::Spec->tmpdir,
  220. UNLINK => 1)->filename;
  221. writefile(basename($infile), File::Spec->tmpdir, $content);
  222. my $masterfile = srcfile($pagesources{masterpage($page)});
  223. my %options = (
  224. "markdown" => (pagetype($masterfile) eq 'mdwn') ? 1 : 0,
  225. );
  226. my $doc=Locale::Po4a::Chooser::new('text',%options);
  227. $doc->process(
  228. 'po_in_name' => [ $infile ],
  229. 'file_in_name' => [ $masterfile ],
  230. 'file_in_charset' => 'utf-8',
  231. 'file_out_charset' => 'utf-8',
  232. ) or error("[po/filter:$page]: failed to translate");
  233. $doc->write($outfile) or error("[po/filter:$page] could not write $outfile");
  234. $content = readfile($outfile) or error("[po/filter:$page] could not read $outfile");
  235. # Unlinking should happen automatically, thanks to File::Temp,
  236. # but it does not work here, probably because of the way writefile()
  237. # and Locale::Po4a::write() work.
  238. unlink $infile, $outfile;
  239. setalreadyfiltered($page, $destpage);
  240. return $content;
  241. }
  242. sub htmlize (@) {
  243. my %params=@_;
  244. my $page = $params{page};
  245. my $content = $params{content};
  246. # ignore PO files this plugin did not create
  247. return $content unless istranslation($page);
  248. # force content to be htmlize'd as if it was the same type as the master page
  249. return IkiWiki::htmlize($page, $page,
  250. pagetype(srcfile($pagesources{masterpage($page)})),
  251. $content);
  252. }
  253. sub pagetemplate (@) {
  254. my %params=@_;
  255. my $page=$params{page};
  256. my $destpage=$params{destpage};
  257. my $template=$params{template};
  258. my ($masterpage, $lang) = istranslation($page);
  259. if (istranslation($page) && $template->query(name => "percenttranslated")) {
  260. $template->param(percenttranslated => percenttranslated($page));
  261. }
  262. if ($template->query(name => "istranslation")) {
  263. $template->param(istranslation => scalar istranslation($page));
  264. }
  265. if ($template->query(name => "istranslatable")) {
  266. $template->param(istranslatable => istranslatable($page));
  267. }
  268. if ($template->query(name => "HOMEPAGEURL")) {
  269. $template->param(homepageurl => homepageurl($page));
  270. }
  271. if ($template->query(name => "otherlanguages")) {
  272. $template->param(otherlanguages => [otherlanguagesloop($page)]);
  273. map add_depends($page, $_), (values %{otherlanguages($page)});
  274. }
  275. # Rely on IkiWiki::Render's genpage() to decide wether
  276. # a discussion link should appear on $page; this is not
  277. # totally accurate, though: some broken links may be generated
  278. # when cgiurl is disabled.
  279. # This compromise avoids some code duplication, and will probably
  280. # prevent future breakage when ikiwiki internals change.
  281. # Known limitations are preferred to future random bugs.
  282. if ($template->param('discussionlink') && istranslation($page)) {
  283. $template->param('discussionlink' => htmllink(
  284. $page,
  285. $destpage,
  286. $masterpage . '/' . gettext("Discussion"),
  287. noimageinline => 1,
  288. forcesubpage => 0,
  289. linktext => gettext("Discussion"),
  290. ));
  291. }
  292. # Remove broken parentlink to ./index.html on home page's translations.
  293. # It works because this hook has the "last" parameter set, to ensure it
  294. # runs after parentlinks' own pagetemplate hook.
  295. if ($template->param('parentlinks')
  296. && istranslation($page)
  297. && $masterpage eq "index") {
  298. $template->param('parentlinks' => []);
  299. }
  300. } # }}}
  301. sub postscan (@) {
  302. my %params = @_;
  303. my $page = $params{page};
  304. # backlinks involve back-dependencies, so that nicepagetitle effects,
  305. # such as translation status displayed in links, are updated
  306. use IkiWiki::Render;
  307. map add_depends($page, $_), keys %{$IkiWiki::backlinks{$page}};
  308. }
  309. # Add the renamed page translations to the list of to-be-renamed pages.
  310. sub renamepages() {
  311. my $torename=shift;
  312. my @torename=@{$torename};
  313. foreach my $rename (@torename) {
  314. next unless istranslatable($rename->{src});
  315. my %otherpages=%{otherlanguages($rename->{src})};
  316. while (my ($lang, $otherpage) = each %otherpages) {
  317. push @{$torename}, {
  318. src => $otherpage,
  319. srcfile => $pagesources{$otherpage},
  320. dest => otherlanguage($rename->{dest}, $lang),
  321. destfile => $rename->{dest}.".".$lang.".po",
  322. required => 0,
  323. };
  324. }
  325. }
  326. }
  327. sub mydelete(@) {
  328. my @deleted=@_;
  329. map { deletetranslations($_) } grep istranslatablefile($_), @deleted;
  330. }
  331. sub change(@) {
  332. my @rendered=@_;
  333. my $updated_po_files=0;
  334. # Refresh/create POT and PO files as needed.
  335. foreach my $file (grep {istranslatablefile($_)} @rendered) {
  336. my $page=pagename($file);
  337. my $masterfile=srcfile($file);
  338. my $updated_pot_file=0;
  339. # Only refresh Pot file if it does not exist, or if
  340. # $pagesources{$page} was changed: don't if only the HTML was
  341. # refreshed, e.g. because of a dependency.
  342. if ((grep { $_ eq $pagesources{$page} } @origneedsbuild)
  343. || ! -e potfile($masterfile)) {
  344. refreshpot($masterfile);
  345. $updated_pot_file=1;
  346. }
  347. my @pofiles;
  348. map {
  349. push @pofiles, $_ if ($updated_pot_file || ! -e $_);
  350. } (pofiles($masterfile));
  351. if (@pofiles) {
  352. refreshpofiles($masterfile, @pofiles);
  353. map { IkiWiki::rcs_add($_) } @pofiles if $config{rcs};
  354. $updated_po_files=1;
  355. }
  356. }
  357. if ($updated_po_files) {
  358. commit_and_refresh(
  359. gettext("updated PO files"),
  360. "IkiWiki::Plugin::po::change");
  361. }
  362. }
  363. # As we're previewing or saving a page, the content may have
  364. # changed, so tell the next filter() invocation it must not be lazy.
  365. sub editcontent () {
  366. my %params=@_;
  367. unsetalreadyfiltered($params{page}, $params{page});
  368. return $params{content};
  369. }
  370. # ,----
  371. # | Injected functions
  372. # `----
  373. # Implement po_link_to 'current' and 'negotiated' settings.
  374. sub mybestlink ($$) {
  375. my $page=shift;
  376. my $link=shift;
  377. my $res=$origsubs{'bestlink'}->(masterpage($page), $link);
  378. if (length $res
  379. && ($config{po_link_to} eq "current" || $config{po_link_to} eq "negotiated")
  380. && istranslatable($res)
  381. && istranslation($page)) {
  382. return $res . "." . lang($page);
  383. }
  384. return $res;
  385. }
  386. sub mybeautify_urlpath ($) {
  387. my $url=shift;
  388. my $res=$origsubs{'beautify_urlpath'}->($url);
  389. if ($config{po_link_to} eq "negotiated") {
  390. $res =~ s!/\Qindex.$config{po_master_language}{code}.$config{htmlext}\E$!/!;
  391. $res =~ s!/\Qindex.$config{htmlext}\E$!/!;
  392. map {
  393. $res =~ s!/\Qindex.$_.$config{htmlext}\E$!/!;
  394. } (keys %{$config{po_slave_languages}});
  395. }
  396. return $res;
  397. }
  398. sub mytargetpage ($$) {
  399. my $page=shift;
  400. my $ext=shift;
  401. if (istranslation($page) || istranslatable($page)) {
  402. my ($masterpage, $lang) = (masterpage($page), lang($page));
  403. if (! $config{usedirs} || $masterpage eq 'index') {
  404. return $masterpage . "." . $lang . "." . $ext;
  405. }
  406. else {
  407. return $masterpage . "/index." . $lang . "." . $ext;
  408. }
  409. }
  410. return $origsubs{'targetpage'}->($page, $ext);
  411. }
  412. sub myurlto ($$;$) {
  413. my $to=shift;
  414. my $from=shift;
  415. my $absolute=shift;
  416. # workaround hard-coded /index.$config{htmlext} in IkiWiki::urlto()
  417. if (! length $to
  418. && $config{po_link_to} eq "current"
  419. && istranslatable('index')) {
  420. return IkiWiki::beautify_urlpath(IkiWiki::baseurl($from) . "index." . lang($from) . ".$config{htmlext}");
  421. }
  422. # avoid using our injected beautify_urlpath if run by cgi_editpage,
  423. # so that one is redirected to the just-edited page rather than to the
  424. # negociated translation; to prevent unnecessary fiddling with caller/inject,
  425. # we only do so when our beautify_urlpath would actually do what we want to
  426. # avoid, i.e. when po_link_to = negotiated
  427. if ($config{po_link_to} eq "negotiated") {
  428. my @caller = caller(1);
  429. my $run_by_editpage = 0;
  430. $run_by_editpage = 1 if (exists $caller[3] && defined $caller[3]
  431. && $caller[3] eq "IkiWiki::cgi_editpage");
  432. inject(name => "IkiWiki::beautify_urlpath", call => $origsubs{'beautify_urlpath'})
  433. if $run_by_editpage;
  434. my $res = $origsubs{'urlto'}->($to,$from,$absolute);
  435. inject(name => "IkiWiki::beautify_urlpath", call => \&mybeautify_urlpath)
  436. if $run_by_editpage;
  437. return $res;
  438. }
  439. else {
  440. return $origsubs{'urlto'}->($to,$from,$absolute)
  441. }
  442. }
  443. sub mynicepagetitle ($;$) {
  444. my ($page, $unescaped) = (shift, shift);
  445. my $res = $origsubs{'nicepagetitle'}->($page, $unescaped);
  446. return $res unless istranslation($page);
  447. return $res unless $config{po_translation_status_in_links};
  448. return $res.' ('.percenttranslated($page).' %)';
  449. }
  450. # ,----
  451. # | Blackboxes for private data
  452. # `----
  453. {
  454. my %filtered;
  455. sub alreadyfiltered($$) {
  456. my $page=shift;
  457. my $destpage=shift;
  458. return ( exists $filtered{$page}{$destpage}
  459. && $filtered{$page}{$destpage} eq 1 );
  460. }
  461. sub setalreadyfiltered($$) {
  462. my $page=shift;
  463. my $destpage=shift;
  464. $filtered{$page}{$destpage}=1;
  465. }
  466. sub unsetalreadyfiltered($$) {
  467. my $page=shift;
  468. my $destpage=shift;
  469. if (exists $filtered{$page}{$destpage}) {
  470. delete $filtered{$page}{$destpage};
  471. }
  472. }
  473. sub resetalreadyfiltered() {
  474. undef %filtered;
  475. }
  476. }
  477. # ,----
  478. # | Helper functions
  479. # `----
  480. sub maybe_add_leading_slash ($;$) {
  481. my $str=shift;
  482. my $add=shift;
  483. $add=1 unless defined $add;
  484. return '/' . $str if $add;
  485. return $str;
  486. }
  487. sub istranslatablefile ($) {
  488. my $file=shift;
  489. return 0 unless defined $file;
  490. return 0 if (defined pagetype($file) && pagetype($file) eq 'po');
  491. return 0 if $file =~ /\.pot$/;
  492. return 1 if pagespec_match(pagename($file), $config{po_translatable_pages});
  493. return;
  494. }
  495. sub istranslatable ($) {
  496. my $page=shift;
  497. $page=~s#^/##;
  498. return 1 if istranslatablefile($pagesources{$page});
  499. return;
  500. }
  501. sub _istranslation ($) {
  502. my $page=shift;
  503. my $hasleadingslash = ($page=~s#^/##);
  504. my $file=$pagesources{$page};
  505. return 0 unless (defined $file
  506. && defined pagetype($file)
  507. && pagetype($file) eq 'po');
  508. return 0 if $file =~ /\.pot$/;
  509. my ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
  510. return 0 unless (defined $masterpage && defined $lang
  511. && length $masterpage && length $lang
  512. && defined $pagesources{$masterpage}
  513. && defined $config{po_slave_languages}{$lang});
  514. return (maybe_add_leading_slash($masterpage, $hasleadingslash), $lang)
  515. if istranslatable($masterpage);
  516. }
  517. sub istranslation ($) {
  518. my $page=shift;
  519. if (1 < (my ($masterpage, $lang) = _istranslation($page))) {
  520. my $hasleadingslash = ($masterpage=~s#^/##);
  521. $translations{$masterpage}{$lang}=$page unless exists $translations{$masterpage}{$lang};
  522. return (maybe_add_leading_slash($masterpage, $hasleadingslash), $lang);
  523. }
  524. return;
  525. }
  526. sub masterpage ($) {
  527. my $page=shift;
  528. if ( 1 < (my ($masterpage, $lang) = _istranslation($page))) {
  529. return $masterpage;
  530. }
  531. return $page;
  532. }
  533. sub lang ($) {
  534. my $page=shift;
  535. if (1 < (my ($masterpage, $lang) = _istranslation($page))) {
  536. return $lang;
  537. }
  538. return $config{po_master_language}{code};
  539. }
  540. sub islanguagecode ($) {
  541. my $code=shift;
  542. return ($code =~ /^[a-z]{2}$/);
  543. }
  544. sub otherlanguage ($$) {
  545. my $page=shift;
  546. my $code=shift;
  547. return masterpage($page) if $code eq $config{po_master_language}{code};
  548. return masterpage($page) . '.' . $code;
  549. }
  550. sub otherlanguages ($) {
  551. my $page=shift;
  552. my %ret;
  553. return \%ret unless (istranslation($page) || istranslatable($page));
  554. my $curlang=lang($page);
  555. foreach my $lang
  556. ($config{po_master_language}{code}, keys %{$config{po_slave_languages}}) {
  557. next if $lang eq $curlang;
  558. $ret{$lang}=otherlanguage($page, $lang);
  559. }
  560. return \%ret;
  561. }
  562. sub potfile ($) {
  563. my $masterfile=shift;
  564. (my $name, my $dir, my $suffix) = fileparse($masterfile, qr/\.[^.]*/);
  565. $dir='' if $dir eq './';
  566. return File::Spec->catpath('', $dir, $name . ".pot");
  567. }
  568. sub pofile ($$) {
  569. my $masterfile=shift;
  570. my $lang=shift;
  571. (my $name, my $dir, my $suffix) = fileparse($masterfile, qr/\.[^.]*/);
  572. $dir='' if $dir eq './';
  573. return File::Spec->catpath('', $dir, $name . "." . $lang . ".po");
  574. }
  575. sub pofiles ($) {
  576. my $masterfile=shift;
  577. return map pofile($masterfile, $_), (keys %{$config{po_slave_languages}});
  578. }
  579. sub refreshpot ($) {
  580. my $masterfile=shift;
  581. my $potfile=potfile($masterfile);
  582. my %options = ("markdown" => (pagetype($masterfile) eq 'mdwn') ? 1 : 0);
  583. my $doc=Locale::Po4a::Chooser::new('text',%options);
  584. $doc->{TT}{utf_mode} = 1;
  585. $doc->{TT}{file_in_charset} = 'utf-8';
  586. $doc->{TT}{file_out_charset} = 'utf-8';
  587. $doc->read($masterfile);
  588. # let's cheat a bit to force porefs option to be passed to Locale::Po4a::Po;
  589. # this is undocument use of internal Locale::Po4a::TransTractor's data,
  590. # compulsory since this module prevents us from using the porefs option.
  591. $doc->{TT}{po_out}=Locale::Po4a::Po->new({ 'porefs' => 'none' });
  592. $doc->{TT}{po_out}->set_charset('utf-8');
  593. # do the actual work
  594. $doc->parse;
  595. IkiWiki::prep_writefile(basename($potfile),dirname($potfile));
  596. $doc->writepo($potfile);
  597. }
  598. sub refreshpofiles ($@) {
  599. my $masterfile=shift;
  600. my @pofiles=@_;
  601. my $potfile=potfile($masterfile);
  602. error("[po/refreshpofiles] POT file ($potfile) does not exist") unless (-e $potfile);
  603. foreach my $pofile (@pofiles) {
  604. IkiWiki::prep_writefile(basename($pofile),dirname($pofile));
  605. if (-e $pofile) {
  606. system("msgmerge", "-U", "--backup=none", $pofile, $potfile) == 0
  607. or error("[po/refreshpofiles:$pofile] failed to update");
  608. }
  609. else {
  610. File::Copy::syscopy($potfile,$pofile)
  611. or error("[po/refreshpofiles:$pofile] failed to copy the POT file");
  612. }
  613. }
  614. }
  615. sub buildtranslationscache() {
  616. # use istranslation's side-effect
  617. map istranslation($_), (keys %pagesources);
  618. }
  619. sub resettranslationscache() {
  620. undef %translations;
  621. }
  622. sub flushmemoizecache() {
  623. Memoize::flush_cache("istranslatable");
  624. Memoize::flush_cache("_istranslation");
  625. Memoize::flush_cache("percenttranslated");
  626. }
  627. sub urlto_with_orig_beautiful_urlpath($$) {
  628. my $to=shift;
  629. my $from=shift;
  630. inject(name => "IkiWiki::beautify_urlpath", call => $origsubs{'beautify_urlpath'});
  631. my $res=urlto($to, $from);
  632. inject(name => "IkiWiki::beautify_urlpath", call => \&mybeautify_urlpath);
  633. return $res;
  634. }
  635. sub percenttranslated ($) {
  636. my $page=shift;
  637. $page=~s/^\///;
  638. return gettext("N/A") unless istranslation($page);
  639. my $file=srcfile($pagesources{$page});
  640. my $masterfile = srcfile($pagesources{masterpage($page)});
  641. my %options = (
  642. "markdown" => (pagetype($masterfile) eq 'mdwn') ? 1 : 0,
  643. );
  644. my $doc=Locale::Po4a::Chooser::new('text',%options);
  645. $doc->process(
  646. 'po_in_name' => [ $file ],
  647. 'file_in_name' => [ $masterfile ],
  648. 'file_in_charset' => 'utf-8',
  649. 'file_out_charset' => 'utf-8',
  650. ) or error("[po/percenttranslated:$page]: failed to translate");
  651. my ($percent,$hit,$queries) = $doc->stats();
  652. return $percent;
  653. }
  654. sub languagename ($) {
  655. my $code=shift;
  656. return $config{po_master_language}{name}
  657. if $code eq $config{po_master_language}{code};
  658. return $config{po_slave_languages}{$code}
  659. if defined $config{po_slave_languages}{$code};
  660. return;
  661. }
  662. sub otherlanguagesloop ($) {
  663. my $page=shift;
  664. my @ret;
  665. my %otherpages=%{otherlanguages($page)};
  666. while (my ($lang, $otherpage) = each %otherpages) {
  667. if (istranslation($page) && masterpage($page) eq $otherpage) {
  668. push @ret, {
  669. url => urlto_with_orig_beautiful_urlpath($otherpage, $page),
  670. code => $lang,
  671. language => languagename($lang),
  672. master => 1,
  673. };
  674. }
  675. else {
  676. push @ret, {
  677. url => urlto_with_orig_beautiful_urlpath($otherpage, $page),
  678. code => $lang,
  679. language => languagename($lang),
  680. percent => percenttranslated($otherpage),
  681. }
  682. }
  683. }
  684. return sort {
  685. return -1 if $a->{code} eq $config{po_master_language}{code};
  686. return 1 if $b->{code} eq $config{po_master_language}{code};
  687. return $a->{language} cmp $b->{language};
  688. } @ret;
  689. }
  690. sub homepageurl (;$) {
  691. my $page=shift;
  692. return urlto('', $page);
  693. }
  694. sub deletetranslations ($) {
  695. my $deletedmasterfile=shift;
  696. my $deletedmasterpage=pagename($deletedmasterfile);
  697. my @todelete;
  698. map {
  699. my $file = newpagefile($deletedmasterpage.'.'.$_, 'po');
  700. my $absfile = "$config{srcdir}/$file";
  701. if (-e $absfile && ! -l $absfile && ! -d $absfile) {
  702. push @todelete, $file;
  703. }
  704. } keys %{$config{po_slave_languages}};
  705. map {
  706. if ($config{rcs}) {
  707. IkiWiki::rcs_remove($_);
  708. }
  709. else {
  710. IkiWiki::prune("$config{srcdir}/$_");
  711. }
  712. } @todelete;
  713. if (scalar @todelete) {
  714. commit_and_refresh(
  715. gettext("removed obsolete PO files"),
  716. "IkiWiki::Plugin::po::deletetranslations");
  717. }
  718. }
  719. sub commit_and_refresh ($$) {
  720. my ($msg, $author) = (shift, shift);
  721. if ($config{rcs}) {
  722. IkiWiki::disable_commit_hook();
  723. IkiWiki::rcs_commit_staged($msg, $author, "127.0.0.1");
  724. IkiWiki::enable_commit_hook();
  725. IkiWiki::rcs_update();
  726. }
  727. # Reinitialize module's private variables.
  728. resetalreadyfiltered();
  729. resettranslationscache();
  730. flushmemoizecache();
  731. # Trigger a wiki refresh.
  732. require IkiWiki::Render;
  733. # without preliminary saveindex/loadindex, refresh()
  734. # complains about a lot of uninitialized variables
  735. IkiWiki::saveindex();
  736. IkiWiki::loadindex();
  737. IkiWiki::refresh();
  738. IkiWiki::saveindex();
  739. }
  740. # ,----
  741. # | PageSpec's
  742. # `----
  743. package IkiWiki::PageSpec;
  744. use warnings;
  745. use strict;
  746. use IkiWiki 2.00;
  747. sub match_istranslation ($;@) {
  748. my $page=shift;
  749. if (IkiWiki::Plugin::po::istranslation($page)) {
  750. return IkiWiki::SuccessReason->new("is a translation page");
  751. }
  752. else {
  753. return IkiWiki::FailReason->new("is not a translation page");
  754. }
  755. }
  756. sub match_istranslatable ($;@) {
  757. my $page=shift;
  758. if (IkiWiki::Plugin::po::istranslatable($page)) {
  759. return IkiWiki::SuccessReason->new("is set as translatable in po_translatable_pages");
  760. }
  761. else {
  762. return IkiWiki::FailReason->new("is not set as translatable in po_translatable_pages");
  763. }
  764. }
  765. sub match_lang ($$;@) {
  766. my $page=shift;
  767. my $wanted=shift;
  768. my $regexp=IkiWiki::glob2re($wanted);
  769. my $lang=IkiWiki::Plugin::po::lang($page);
  770. if ($lang!~/^$regexp$/i) {
  771. return IkiWiki::FailReason->new("file language is $lang, not $wanted");
  772. }
  773. else {
  774. return IkiWiki::SuccessReason->new("file language is $wanted");
  775. }
  776. }
  777. sub match_currentlang ($$;@) {
  778. my $page=shift;
  779. shift;
  780. my %params=@_;
  781. return IkiWiki::FailReason->new("no location provided") unless exists $params{location};
  782. my $currentlang=IkiWiki::Plugin::po::lang($params{location});
  783. my $lang=IkiWiki::Plugin::po::lang($page);
  784. if ($lang eq $currentlang) {
  785. return IkiWiki::SuccessReason->new("file language is the same as current one, i.e. $currentlang");
  786. }
  787. else {
  788. return IkiWiki::FailReason->new("file language is $lang, whereas current language is $currentlang");
  789. }
  790. }
  791. 1