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