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