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