+ return $master_language_code;
+}
+
+sub htmllangcode ($) {
+ (my $lang = shift) =~ tr/_/-/;
+ return $lang;
+}
+
+sub htmllangdir ($) {
+ my $lang = shift;
+ if ($lang =~ /^(ar|fa|he)/) {
+ return 'rtl';
+ }
+ return 'ltr';
+}
+
+sub islanguagecode ($) {
+ my $code=shift;
+
+ return $code =~ /^$language_code_pattern$/;
+}
+
+sub otherlanguage_page ($$) {
+ my $page=shift;
+ my $code=shift;
+
+ return masterpage($page) if $code eq $master_language_code;
+ return masterpage($page) . '.' . $code;
+}
+
+# Returns the list of other languages codes: the master language comes first,
+# then the codes are ordered the same way as in po_slave_languages, if it is
+# an array, or in the language name lexical order, if it is a hash.
+sub otherlanguages_codes ($) {
+ my $page=shift;
+
+ my @ret;
+ return \@ret unless istranslation($page) || istranslatable($page);
+ my $curlang=lang($page);
+ foreach my $lang
+ ($master_language_code, @slavelanguages) {
+ next if $lang eq $curlang;
+ if ($lang eq $master_language_code ||
+ istranslatedto(masterpage($page), $lang)) {
+ push @ret, $lang;
+ }
+ }
+ return \@ret;
+}
+
+sub otherlanguages_pages ($) {
+ my $page=shift;
+
+ my %ret;
+ map {
+ $ret{$_} = otherlanguage_page($page, $_)
+ } @{otherlanguages_codes($page)};
+
+ return \%ret;
+}
+
+sub potfile ($) {
+ my $masterfile=shift;
+
+ (my $name, my $dir, my $suffix) = fileparse($masterfile, qr/\.[^.]*/);
+ $dir='' if $dir eq './';
+ return File::Spec->catpath('', $dir, $name . ".pot");
+}
+
+sub pofile ($$) {
+ my $masterfile=shift;
+ my $lang=shift;
+
+ (my $name, my $dir, my $suffix) = fileparse($masterfile, qr/\.[^.]*/);
+ $dir='' if $dir eq './';
+ return File::Spec->catpath('', $dir, $name . "." . $lang . ".po");
+}
+
+sub pofiles ($) {
+ my $masterfile=shift;
+
+ return map pofile($masterfile, $_), @slavelanguages;
+}
+
+sub refreshpot ($) {
+ my $masterfile=shift;
+
+ my $potfile=potfile($masterfile);
+ my $doc=Locale::Po4a::Chooser::new(po4a_type($masterfile),
+ po4a_options($masterfile));
+ $doc->{TT}{utf_mode} = 1;
+ $doc->{TT}{file_in_charset} = 'UTF-8';
+ $doc->{TT}{file_out_charset} = 'UTF-8';
+ $doc->read($masterfile);
+ # let's cheat a bit to force porefs option to be passed to
+ # Locale::Po4a::Po; this is undocument use of internal
+ # Locale::Po4a::TransTractor's data, compulsory since this module
+ # prevents us from using the porefs option.
+ $doc->{TT}{po_out}=Locale::Po4a::Po->new({ 'porefs' => 'none' });
+ $doc->{TT}{po_out}->set_charset('UTF-8');
+ # do the actual work
+ $doc->parse;
+ IkiWiki::prep_writefile(basename($potfile),dirname($potfile));
+ $doc->writepo($potfile);
+}
+
+sub refreshpofiles ($@) {
+ my $masterfile=shift;
+ my @pofiles=@_;
+
+ my $potfile=potfile($masterfile);
+ if (! -e $potfile) {
+ error("po(refreshpofiles) ".sprintf(gettext("POT file (%s) does not exist"), $potfile));
+ }
+
+ foreach my $pofile (@pofiles) {
+ IkiWiki::prep_writefile(basename($pofile),dirname($pofile));
+
+ if (! -e $pofile) {
+ # If the po file exists in an underlay, copy it
+ # from there.
+ my ($pobase)=$pofile=~/^\Q$config{srcdir}\E\/?(.*)$/;
+ foreach my $dir (@{$config{underlaydirs}}) {
+ if (-e "$dir/$pobase") {
+ File::Copy::syscopy("$dir/$pobase",$pofile)
+ or error("po(refreshpofiles) ".
+ sprintf(gettext("failed to copy underlay PO file to %s"),
+ $pofile));
+ }
+ }
+ }
+
+ if (-e $pofile) {
+ if (! (system("msgmerge", "--previous", "-q", "-U", "--backup=none", $pofile, $potfile) == 0)) {
+ print STDERR ("po(refreshpofiles) ". sprintf(gettext("failed to update %s"), $pofile));
+ }
+ }
+ else {
+ File::Copy::syscopy($potfile,$pofile)
+ or error("po(refreshpofiles) ".
+ sprintf(gettext("failed to copy the POT file to %s"),
+ $pofile));
+ }
+ }
+}
+
+sub buildtranslationscache() {
+ # use istranslation's side-effect
+ map istranslation($_), (keys %pagesources);
+}
+
+sub resettranslationscache() {
+ undef %translations;
+}
+
+sub flushmemoizecache() {
+ Memoize::flush_cache("istranslatable");
+ Memoize::flush_cache("_istranslation");
+ Memoize::flush_cache("percenttranslated");
+}
+
+sub urlto_with_orig_beautiful_urlpath($$) {
+ my $to=shift;
+ my $from=shift;
+
+ inject(name => "IkiWiki::beautify_urlpath", call => $origsubs{'beautify_urlpath'});
+ my $res=urlto($to, $from);
+ inject(name => "IkiWiki::beautify_urlpath", call => \&mybeautify_urlpath);
+
+ return $res;
+}
+
+sub percenttranslated ($) {
+ my $page=shift;
+
+ $page=~s/^\///;
+ return gettext("N/A") unless istranslation($page);
+ my $file=srcfile($pagesources{$page});
+ my $masterfile = srcfile($pagesources{masterpage($page)});
+ my $doc=Locale::Po4a::Chooser::new(po4a_type($masterfile),
+ po4a_options($masterfile));
+ $doc->process(
+ 'po_in_name' => [ $file ],
+ 'file_in_name' => [ $masterfile ],
+ 'file_in_charset' => 'UTF-8',
+ 'file_out_charset' => 'UTF-8',
+ ) or error("po(percenttranslated) ".
+ sprintf(gettext("failed to translate %s"), $page));
+ my ($percent,$hit,$queries) = $doc->stats();
+ $percent =~ s/\.[0-9]+$//;
+ return $percent;
+}
+
+sub languagename ($) {
+ my $code=shift;
+
+ return $master_language_name
+ if $code eq $master_language_code;
+ return $slavelanguages{$code}
+ if defined $slavelanguages{$code};
+ return;
+}
+
+sub otherlanguagesloop ($) {
+ my $page=shift;
+
+ my @ret;
+ if (istranslation($page)) {
+ push @ret, {
+ url => urlto_with_orig_beautiful_urlpath(masterpage($page), $page),
+ code => $master_language_code,
+ html_code => htmllangcode($master_language_code),
+ html_dir => htmllangdir($master_language_code),
+ language => $master_language_name,
+ master => 1,
+ };
+ }
+ foreach my $lang (@{otherlanguages_codes($page)}) {
+ next if $lang eq $master_language_code;
+ my $otherpage = otherlanguage_page($page, $lang);
+ push @ret, {
+ url => urlto_with_orig_beautiful_urlpath($otherpage, $page),
+ code => $lang,
+ html_code => htmllangcode($lang),
+ html_dir => htmllangdir($lang),
+ language => languagename($lang),
+ percent => percenttranslated($otherpage),
+ }
+ }
+ return @ret;
+}
+
+sub homepageurl (;$) {
+ my $page=shift;
+
+ return urlto('', $page);
+}
+
+sub ishomepage ($) {
+ my $page = shift;
+
+ return 1 if $page eq 'index';
+ map { return 1 if $page eq 'index.'.$_ } @slavelanguages;
+ return undef;
+}
+
+sub deletetranslations ($) {
+ my $deletedmasterfile=shift;
+
+ my $deletedmasterpage=pagename($deletedmasterfile);
+ my @todelete;
+ map {
+ my $file = newpagefile($deletedmasterpage.'.'.$_, 'po');
+ my $absfile = "$config{srcdir}/$file";
+ if (-e $absfile && ! -l $absfile && ! -d $absfile) {
+ push @todelete, $file;
+ }
+ } @slavelanguages;
+
+ map {
+ if ($config{rcs}) {
+ IkiWiki::rcs_remove($_);
+ }
+ else {
+ IkiWiki::prune("$config{srcdir}/$_", $config{srcdir});
+ }
+ } @todelete;
+
+ if (@todelete) {
+ commit_and_refresh(
+ gettext("removed obsolete PO files"));
+ }
+}
+
+sub commit_and_refresh ($) {
+ my $msg = shift;
+
+ if ($config{rcs}) {
+ IkiWiki::disable_commit_hook();
+ IkiWiki::rcs_commit_staged(
+ message => $msg,
+ );
+ IkiWiki::enable_commit_hook();
+ IkiWiki::rcs_update();
+ }
+ # Reinitialize module's private variables.
+ resetalreadyfiltered();
+ resettranslationscache();
+ flushmemoizecache();
+ # Trigger a wiki refresh.
+ require IkiWiki::Render;
+ # without preliminary saveindex/loadindex, refresh()
+ # complains about a lot of uninitialized variables
+ IkiWiki::saveindex();
+ IkiWiki::loadindex();
+ IkiWiki::refresh();
+ IkiWiki::saveindex();
+}
+
+sub po_to_markup ($$) {
+ my ($page, $content) = (shift, shift);
+
+ $content = '' unless defined $content;
+ $content = decode_utf8(encode_utf8($content));
+ # CRLF line terminators make poor Locale::Po4a feel bad
+ $content=~s/\r\n/\n/g;
+
+ # There are incompatibilities between some File::Temp versions
+ # (including 0.18, bundled with Lenny's perl-modules package)
+ # and others (e.g. 0.20, previously present in the archive as
+ # a standalone package): under certain circumstances, some
+ # return a relative filename, whereas others return an absolute one;
+ # we here use this module in a way that is at least compatible
+ # with 0.18 and 0.20. Beware, hit'n'run refactorers!
+ my $infile = new File::Temp(TEMPLATE => "ikiwiki-po-filter-in.XXXXXXXXXX",
+ DIR => File::Spec->tmpdir,
+ UNLINK => 1)->filename;
+ my $outfile = new File::Temp(TEMPLATE => "ikiwiki-po-filter-out.XXXXXXXXXX",
+ DIR => File::Spec->tmpdir,
+ UNLINK => 1)->filename;
+
+ my $fail = sub ($) {
+ my $msg = "po(po_to_markup) - $page : " . shift;
+ error($msg, sub { unlink $infile, $outfile});
+ };
+
+ writefile(basename($infile), File::Spec->tmpdir, $content)
+ or return $fail->(sprintf(gettext("failed to write %s"), $infile));
+
+ my $masterfile = srcfile($pagesources{masterpage($page)});
+ my $doc=Locale::Po4a::Chooser::new(po4a_type($masterfile),
+ po4a_options($masterfile));
+ $doc->process(
+ 'po_in_name' => [ $infile ],
+ 'file_in_name' => [ $masterfile ],
+ 'file_in_charset' => 'UTF-8',
+ 'file_out_charset' => 'UTF-8',
+ ) or return $fail->(gettext("failed to translate"));
+ $doc->write($outfile)
+ or return $fail->(sprintf(gettext("failed to write %s"), $outfile));
+
+ $content = readfile($outfile);
+
+ # Unlinking should happen automatically, thanks to File::Temp,
+ # but it does not work here, probably because of the way writefile()
+ # and Locale::Po4a::write() work.
+ unlink $infile, $outfile;
+
+ return $content;
+}
+
+# returns a SuccessReason or FailReason object
+sub isvalidpo ($) {
+ my $content = shift;
+
+ # NB: we don't use po_to_markup here, since Po4a parser does
+ # not mind invalid PO content
+ $content = '' unless defined $content;
+ $content = decode_utf8(encode_utf8($content));
+
+ # There are incompatibilities between some File::Temp versions
+ # (including 0.18, bundled with Lenny's perl-modules package)
+ # and others (e.g. 0.20, previously present in the archive as
+ # a standalone package): under certain circumstances, some
+ # return a relative filename, whereas others return an absolute one;
+ # we here use this module in a way that is at least compatible
+ # with 0.18 and 0.20. Beware, hit'n'run refactorers!
+ my $infile = new File::Temp(TEMPLATE => "ikiwiki-po-isvalidpo.XXXXXXXXXX",
+ DIR => File::Spec->tmpdir,
+ UNLINK => 1)->filename;
+
+ my $fail = sub ($) {
+ my $msg = '[po/isvalidpo] ' . shift;
+ unlink $infile;
+ return IkiWiki::FailReason->new("$msg");
+ };
+
+ writefile(basename($infile), File::Spec->tmpdir, $content)
+ or return $fail->(sprintf(gettext("failed to write %s"), $infile));
+
+ my $res = (system("msgfmt", "--check", $infile, "-o", "/dev/null") == 0);
+
+ # Unlinking should happen automatically, thanks to File::Temp,
+ # but it does not work here, probably because of the way writefile()
+ # and Locale::Po4a::write() work.
+ unlink $infile;
+
+ if ($res) {
+ return IkiWiki::SuccessReason->new("valid gettext data");
+ }
+ return IkiWiki::FailReason->new(gettext("invalid gettext data, go back ".
+ "to previous page to continue edit"));
+}
+
+sub po4a_type ($) {
+ my $file = shift;
+
+ my $pagetype = pagetype($file);
+ if ($pagetype eq 'html') {
+ return 'xhtml';
+ }
+ return 'text';
+}
+
+sub po4a_options($) {
+ my $file = shift;
+
+ my %options;
+ my $pagetype = pagetype($file);
+
+ if ($pagetype eq 'html') {
+ # how to disable options is not consistent across po4a modules
+ $options{includessi} = '';
+ $options{includeexternal} = 0;
+ $options{ontagerror} = 'warn';
+ }
+ elsif ($pagetype eq 'mdwn') {
+ $options{markdown} = 1;
+ }
+ else {
+ $options{markdown} = 0;
+ }
+
+ return %options;
+}
+
+sub splitlangpair ($) {
+ my $pair=shift;
+
+ my ($code, $name) = ( $pair =~ /^($language_code_pattern)\|(.+)$/ );
+ if (! defined $code || ! defined $name ||
+ ! length $code || ! length $name) {
+ # not a fatal error to avoid breaking if used with web setup
+ warn sprintf(gettext("%s has invalid syntax: must use CODE|NAME"),
+ $pair);
+ }
+
+ return $code, $name;
+}
+
+sub joinlangpair ($$) {
+ my $code=shift;
+ my $name=shift;
+
+ return "$code|$name";
+}
+
+# ,----
+# | PageSpecs
+# `----