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