]> git.vanrenterghem.biz Git - git.ikiwiki.info.git/blobdiff - IkiWiki/Plugin/po.pm
Add modified opengraph plugin.
[git.ikiwiki.info.git] / IkiWiki / Plugin / po.pm
index 759e812e61bb3218be256504585ee61e122dd4ee..13b98b739bcd3ea76a6c059f95601bdadc6c1aa6 100644 (file)
@@ -23,11 +23,14 @@ use File::Copy;
 use File::Spec;
 use File::Temp;
 use Memoize;
-use UNIVERSAL;
 
+my ($master_language_code, $master_language_name);
 my %translations;
 my @origneedsbuild;
 my %origsubs;
+my @slavelanguages; # language codes ordered as in config po_slave_languages
+my %slavelanguages; # language code to name lookup
+my $language_code_pattern = '[a-zA-Z]+(?:_[a-zA-Z]+)?';
 
 memoize("istranslatable");
 memoize("_istranslation");
@@ -35,7 +38,8 @@ memoize("percenttranslated");
 
 sub import {
        hook(type => "getsetup", id => "po", call => \&getsetup);
-       hook(type => "checkconfig", id => "po", call => \&checkconfig);
+       hook(type => "checkconfig", id => "po", call => \&checkconfig,
+               last => 1);
        hook(type => "needsbuild", id => "po", call => \&needsbuild);
        hook(type => "scan", id => "po", call => \&scan, last => 1);
        hook(type => "filter", id => "po", call => \&filter);
@@ -43,26 +47,32 @@ sub import {
        hook(type => "pagetemplate", id => "po", call => \&pagetemplate, last => 1);
        hook(type => "rename", id => "po", call => \&renamepages, first => 1);
        hook(type => "delete", id => "po", call => \&mydelete);
-       hook(type => "change", id => "po", call => \&change);
+       hook(type => "rendered", id => "po", call => \&rendered);
        hook(type => "checkcontent", id => "po", call => \&checkcontent);
        hook(type => "canremove", id => "po", call => \&canremove);
        hook(type => "canrename", id => "po", call => \&canrename);
-       hook(type => "editcontent", id => "po", call => \&editcontent);
        hook(type => "formbuilder_setup", id => "po", call => \&formbuilder_setup, last => 1);
        hook(type => "formbuilder", id => "po", call => \&formbuilder);
 
-       $origsubs{'bestlink'}=\&IkiWiki::bestlink;
-       inject(name => "IkiWiki::bestlink", call => \&mybestlink);
-       $origsubs{'beautify_urlpath'}=\&IkiWiki::beautify_urlpath;
-       inject(name => "IkiWiki::beautify_urlpath", call => \&mybeautify_urlpath);
-       $origsubs{'targetpage'}=\&IkiWiki::targetpage;
-       inject(name => "IkiWiki::targetpage", call => \&mytargetpage);
-       $origsubs{'urlto'}=\&IkiWiki::urlto;
-       inject(name => "IkiWiki::urlto", call => \&myurlto);
-       $origsubs{'cgiurl'}=\&IkiWiki::cgiurl;
-       inject(name => "IkiWiki::cgiurl", call => \&mycgiurl);
-       $origsubs{'rootpage'}=\&IkiWiki::rootpage;
-       inject(name => "IkiWiki::rootpage", call => \&myrootpage);
+       if (! %origsubs) {
+               $origsubs{'bestlink'}=\&IkiWiki::bestlink;
+               inject(name => "IkiWiki::bestlink", call => \&mybestlink);
+               $origsubs{'beautify_urlpath'}=\&IkiWiki::beautify_urlpath;
+               inject(name => "IkiWiki::beautify_urlpath", call => \&mybeautify_urlpath);
+               $origsubs{'targetpage'}=\&IkiWiki::targetpage;
+               inject(name => "IkiWiki::targetpage", call => \&mytargetpage);
+               $origsubs{'urlto'}=\&IkiWiki::urlto;
+               inject(name => "IkiWiki::urlto", call => \&myurlto);
+               $origsubs{'cgiurl'}=\&IkiWiki::cgiurl;
+               inject(name => "IkiWiki::cgiurl", call => \&mycgiurl);
+               if (IkiWiki->can('rootpage')) {
+                       $origsubs{'rootpage'}=\&IkiWiki::rootpage;
+                       inject(name => "IkiWiki::rootpage", call => \&myrootpage)
+                               if defined $origsubs{'rootpage'};
+               }
+               $origsubs{'isselflink'}=\&IkiWiki::isselflink;
+               inject(name => "IkiWiki::isselflink", call => \&myisselflink);
+       }
 }
 
 
@@ -84,28 +94,25 @@ sub import {
 sub getsetup () {
        return
                plugin => {
-                       safe => 0,
+                       safe => 1,
                        rebuild => 1, # format plugin
                        section => "format",
                },
                po_master_language => {
                        type => "string",
-                       example => {
-                               'code' => 'en',
-                               'name' => 'English'
-                       },
+                       example => "en|English",
                        description => "master language (non-PO files)",
                        safe => 1,
                        rebuild => 1,
                },
                po_slave_languages => {
                        type => "string",
-                       example => {
-                               'fr' => 'Français',
-                               'es' => 'Español',
-                               'de' => 'Deutsch'
-                       },
-                       description => "slave languages (PO files)",
+                       example => [
+                               'fr|Français',
+                               'es|Español',
+                               'de|Deutsch'
+                       ],
+                       description => "slave languages (translated via PO files) format: ll|Langname",
                        safe => 1,
                        rebuild => 1,
                },
@@ -127,18 +134,49 @@ sub getsetup () {
 }
 
 sub checkconfig () {
-       foreach my $field (qw{po_master_language}) {
-               if (! exists $config{$field} || ! defined $config{$field}) {
-                       error(sprintf(gettext("Must specify %s when using the %s plugin"),
-                                     $field, 'po'));
+       if (exists $config{po_master_language}) {
+               if (! ref $config{po_master_language}) {
+                       ($master_language_code, $master_language_name)=
+                               splitlangpair($config{po_master_language});
+               }
+               else {
+                       $master_language_code=$config{po_master_language}{code};
+                       $master_language_name=$config{po_master_language}{name};
+                       $config{po_master_language}=joinlangpair($master_language_code, $master_language_name);
+               }
+       }
+       if (! defined $master_language_code) {
+               $master_language_code='en';
+       }
+       if (! defined $master_language_name) {
+               $master_language_name='English';
+       }
+
+       if (ref $config{po_slave_languages} eq 'ARRAY') {
+               foreach my $pair (@{$config{po_slave_languages}}) {
+                       my ($code, $name)=splitlangpair($pair);
+                       if (defined $code && ! exists $slavelanguages{$code}) {
+                               push @slavelanguages, $code;
+                               $slavelanguages{$code} = $name;
+                       }
                }
        }
-       delete $config{po_slave_languages}{$config{po_master_language}{code}};;
+       elsif (ref $config{po_slave_languages} eq 'HASH') {
+               %slavelanguages=%{$config{po_slave_languages}};
+               @slavelanguages = sort {
+                       $config{po_slave_languages}->{$a} cmp $config{po_slave_languages}->{$b};
+               } keys %slavelanguages;
+               $config{po_slave_languages}=[
+                       map { joinlangpair($_, $slavelanguages{$_}) } @slavelanguages
+               ]
+       }
+
+       delete $slavelanguages{$master_language_code};
 
        map {
                islanguagecode($_)
                        or error(sprintf(gettext("%s is not a valid language code"), $_));
-       } ($config{po_master_language}{code}, keys %{$config{po_slave_languages}});
+       } ($master_language_code, @slavelanguages);
 
        if (! exists $config{po_translatable_pages} ||
            ! defined $config{po_translatable_pages}) {
@@ -167,16 +205,16 @@ sub checkconfig () {
                next if $underlay=~/^locale\//;
 
                # Underlays containing the po files for slave languages.
-               foreach my $ll (keys %{$config{po_slave_languages}}) {
+               foreach my $ll (@slavelanguages) {
                        add_underlay("po/$ll/$underlay")
                                if -d "$config{underlaydirbase}/po/$ll/$underlay";
                }
        
-               if ($config{po_master_language}{code} ne 'en') {
+               if ($master_language_code ne 'en') {
                        # Add underlay containing translated source files
                        # for the master language.
-                       add_underlay("locale/$config{po_master_language}{code}/$underlay")
-                               if -d "$config{underlaydirbase}/locale/$config{po_master_language}{code}/$underlay";
+                       add_underlay("locale/$master_language_code/$underlay")
+                               if -d "$config{underlaydirbase}/locale/$master_language_code/$underlay";
                }
        }
 }
@@ -193,42 +231,67 @@ sub needsbuild () {
 
        # make existing translations depend on the corresponding master page
        foreach my $master (keys %translations) {
-               map add_depends($_, $master), values %{otherlanguages($master)};
+               map add_depends($_, $master), values %{otherlanguages_pages($master)};
        }
+
+       return $needsbuild;
 }
 
-# Massage the recorded state of internal links so that:
-# - it matches the actually generated links, rather than the links as written
-#   in the pages' source
-# - backlinks are consistent in all cases
 sub scan (@) {
        my %params=@_;
        my $page=$params{page};
        my $content=$params{content};
-
-       if (istranslation($page)) {
-               foreach my $destpage (@{$links{$page}}) {
-                       if (istranslatable($destpage)) {
-                               # replace the occurence of $destpage in $links{$page}
-                               for (my $i=0; $i<@{$links{$page}}; $i++) {
-                                       if (@{$links{$page}}[$i] eq $destpage) {
-                                               @{$links{$page}}[$i] = $destpage . '.' . lang($page);
-                                               last;
-                                       }
-                               }
+       my $run_by_po=$params{run_by_po};
+
+       # Massage the recorded state of internal links so that:
+       # - it matches the actually generated links, rather than the links as
+       #   written in the pages' source
+       # - backlinks are consistent in all cases
+
+       # A second scan pass is made over translation pages, so as an
+       # optimization, we only do so on the second pass in this case,
+       # i.e. when this hook is called by itself.
+       if ($run_by_po && istranslation($page)) {
+               # replace the occurence of $destpage in $links{$page}
+               my @orig_links = @{$links{$page}};
+               $links{$page} = [];
+               foreach my $destpage (@orig_links) {
+                       if (istranslatedto($destpage, lang($page))) {
+                               add_link($page, $destpage . '.' . lang($page));
+                       }
+                       else {
+                               add_link($page, $destpage);
                        }
                }
        }
-       elsif (! istranslatable($page) && ! istranslation($page)) {
+       # No second scan pass is done for a non-translation page, so
+       # links massaging must happen on first pass in this case.
+       elsif (! $run_by_po && ! istranslatable($page) && ! istranslation($page)) {
                foreach my $destpage (@{$links{$page}}) {
                        if (istranslatable($destpage)) {
                                # make sure any destpage's translations has
                                # $page in its backlinks
-                               push @{$links{$page}},
-                                       values %{otherlanguages($destpage)};
+                               foreach my $link (values %{otherlanguages_pages($destpage)}) {
+                                       add_link($page, $link);
+                               }
                        }
                }
        }
+
+       # Re-run the preprocess hooks in scan mode, then the scan hooks,
+       # over the po-to-markup converted content
+       return if $run_by_po; # avoid looping endlessly
+       return unless istranslation($page);
+       $content = po_to_markup($page, $content);
+       require IkiWiki;
+       IkiWiki::preprocess($page, $page, $content, 1);
+       IkiWiki::run_hooks(scan => sub {
+               shift->(
+                       page => $page,
+                       content => $content,
+                       run_by_po => 1,
+               );
+       });
 }
 
 # We use filter to convert PO to the master page's format,
@@ -239,9 +302,8 @@ sub filter (@) {
        my $page = $params{page};
        my $destpage = $params{destpage};
        my $content = $params{content};
-       if (istranslation($page) && ! alreadyfiltered($page, $destpage)) {
+       if (istranslation($page)) {
                $content = po_to_markup($page, $content);
-               setalreadyfiltered($page, $destpage);
        }
        return $content;
 }
@@ -278,12 +340,25 @@ sub pagetemplate (@) {
        if ($template->query(name => "istranslatable")) {
                $template->param(istranslatable => istranslatable($page));
        }
+       my $lang_code = istranslation($page) ? lang($page) : $master_language_code;
+       if ($template->query(name => "lang_code")) {
+               $template->param(lang_code => $lang_code);
+       }
+       if ($template->query(name => "html_lang_code")) {
+               $template->param(html_lang_code => htmllangcode($lang_code));
+       }
+       if ($template->query(name => "html_lang_dir")) {
+               $template->param(html_lang_dir => htmllangdir($lang_code));
+       }
+       if ($template->query(name => "lang_name")) {
+               $template->param(lang_name => languagename($lang_code));
+       }
        if ($template->query(name => "HOMEPAGEURL")) {
                $template->param(homepageurl => homepageurl($page));
        }
        if ($template->query(name => "otherlanguages")) {
                $template->param(otherlanguages => [otherlanguagesloop($page)]);
-               map add_depends($page, $_), (values %{otherlanguages($page)});
+               map add_depends($page, $_), (values %{otherlanguages_pages($page)});
        }
        if ($config{discussion} && istranslation($page)) {
                if ($page !~ /.*\/\Q$config{discussionpage}\E$/i &&
@@ -307,7 +382,8 @@ sub pagetemplate (@) {
            && $masterpage eq "index") {
                $template->param('parentlinks' => []);
        }
-       if (ishomepage($page) && $template->query(name => "title")) {
+       if (ishomepage($page) && $template->query(name => "title")
+           && !$template->param("title_overridden")) {
                $template->param(title => $config{wikiname});
        }
 }
@@ -336,12 +412,12 @@ sub renamepages (@) {
        return () unless istranslatable($torename{src});
 
        my @ret;
-       my %otherpages=%{otherlanguages($torename{src})};
+       my %otherpages=%{otherlanguages_pages($torename{src})};
        while (my ($lang, $otherpage) = each %otherpages) {
                push @ret, {
                        src => $otherpage,
                        srcfile => $pagesources{$otherpage},
-                       dest => otherlanguage($torename{dest}, $lang),
+                       dest => otherlanguage_page($torename{dest}, $lang),
                        destfile => $torename{dest}.".".$lang.".po",
                        required => 0,
                };
@@ -355,44 +431,9 @@ sub mydelete (@) {
        map { deletetranslations($_) } grep istranslatablefile($_), @deleted;
 }
 
-sub change (@) {
+sub rendered (@) {
        my @rendered=@_;
 
-       # All meta titles are first extracted at scan time, i.e. before we turn
-       # PO files back into translated markdown; escaping of double-quotes in
-       # PO files breaks the meta plugin's parsing enough to save ugly titles
-       # to %pagestate at this time.
-       #
-       # Then, at render time, every page passes in turn through the Great
-       # Rendering Chain (filter->preprocess->linkify->htmlize), and the meta
-       # plugin's preprocess hook is this time in a position to correctly
-       # extract the titles from slave pages.
-       #
-       # This is, unfortunately, too late: if the page A, linking to the page
-       # B, is rendered before B, it will display the wrongly-extracted meta
-       # title as the link text to B.
-       #
-       # On the one hand, such a corner case only happens on rebuild: on
-       # refresh, every rendered page is fixed to contain correct meta titles.
-       # On the other hand, it can take some time to get every page fixed.
-       # We therefore re-render every rendered page after a rebuild to fix them
-       # at once. As this more or less doubles the time needed to rebuild the
-       # wiki, we do so only when really needed.
-
-       if (@rendered
-           && exists $config{rebuild} && defined $config{rebuild} && $config{rebuild}
-           && UNIVERSAL::can("IkiWiki::Plugin::meta", "getsetup")
-           && exists $config{meta_overrides_page_title}
-           && defined $config{meta_overrides_page_title}
-           && $config{meta_overrides_page_title}) {
-               debug(sprintf(gettext("rebuilding all pages to fix meta titles")));
-               resetalreadyfiltered();
-               require IkiWiki::Render;
-               foreach my $file (@rendered) {
-                       IkiWiki::render($file, sprintf(gettext("building %s"), $file));
-               }
-       }
-
        my $updated_po_files=0;
 
        # Refresh/create POT and PO files as needed.
@@ -427,8 +468,7 @@ sub change (@) {
 
        if ($updated_po_files) {
                commit_and_refresh(
-                       gettext("updated PO files"),
-                       "IkiWiki::Plugin::po::change");
+                       gettext("updated PO files"));
        }
 }
 
@@ -478,15 +518,6 @@ sub canrename (@) {
        return undef;
 }
 
-# As we're previewing or saving a page, the content may have
-# changed, so tell the next filter() invocation it must not be lazy.
-sub editcontent () {
-       my %params=@_;
-
-       unsetalreadyfiltered($params{page}, $params{page});
-       return $params{content};
-}
-
 sub formbuilder_setup (@) {
        my %params=@_;
        my $form=$params{form};
@@ -497,7 +528,7 @@ sub formbuilder_setup (@) {
        if ($form->field("do") eq "create") {
                # Warn the user: new pages must be written in master language.
                my $template=template("pocreatepage.tmpl");
-               $template->param(LANG => $config{po_master_language}{name});
+               $template->param(LANG => $master_language_name);
                $form->tmpl_param(message => $template->output);
        }
        elsif ($form->field("do") eq "edit") {
@@ -506,7 +537,7 @@ sub formbuilder_setup (@) {
                # their buttons, which is why this hook must be run last.
                # The canrename/canremove hooks already ensure this is forbidden
                # at the backend level, so this is only UI sugar.
-               if (istranslation($form->field("page"))) {
+               if (istranslation(scalar $form->field("page"))) {
                        map {
                                for (my $i = 0; $i < @{$params{buttons}}; $i++) {
                                        if (@{$params{buttons}}[$i] eq $_) {
@@ -533,7 +564,7 @@ sub formbuilder (@) {
        # This cannot be done in the formbuilder_setup hook as the list of types is
        # computed later.
        if ($form->field("do") eq "create") {
-               foreach my $field ($form->field) {
+               foreach my $field ($form->field) {
                        next unless "$field" eq "type";
                        next unless $field->type eq 'select';
                        my $orig_value = $field->value;
@@ -567,12 +598,12 @@ sub mybestlink ($$) {
        my $link=shift;
 
        return $origsubs{'bestlink'}->($page, $link)
-               if $config{po_link_to} eq "default";
+               if defined $config{po_link_to} && $config{po_link_to} eq "default";
 
        my $res=$origsubs{'bestlink'}->(masterpage($page), $link);
        my @caller = caller(1);
        if (length $res
-           && istranslatable($res)
+           && istranslatedto($res, lang($page))
            && istranslation($page)
            &&  !(exists $caller[3] && defined $caller[3]
                  && ($caller[3] eq "IkiWiki::PageSpec::match_link"))) {
@@ -585,33 +616,37 @@ sub mybeautify_urlpath ($) {
        my $url=shift;
 
        my $res=$origsubs{'beautify_urlpath'}->($url);
-       if ($config{po_link_to} eq "negotiated") {
-               $res =~ s!/\Qindex.$config{po_master_language}{code}.$config{htmlext}\E$!/!;
+       if (defined $config{po_link_to} && $config{po_link_to} eq "negotiated") {
+               $res =~ s!/\Qindex.$master_language_code.$config{htmlext}\E$!/!;
                $res =~ s!/\Qindex.$config{htmlext}\E$!/!;
                map {
                        $res =~ s!/\Qindex.$_.$config{htmlext}\E$!/!;
-               } (keys %{$config{po_slave_languages}});
+               } @slavelanguages;
        }
        return $res;
 }
 
-sub mytargetpage ($$) {
+sub mytargetpage ($$;$) {
        my $page=shift;
        my $ext=shift;
+       my $filename=shift;
 
        if (istranslation($page) || istranslatable($page)) {
                my ($masterpage, $lang) = (masterpage($page), lang($page));
-               if (! $config{usedirs} || $masterpage eq 'index') {
+               if (defined $filename) {
+                       return $masterpage . "/" . $filename . "." . $lang . "." . $ext;
+               }
+               elsif (! $config{usedirs} || $masterpage eq 'index') {
                        return $masterpage . "." . $lang . "." . $ext;
                }
                else {
                        return $masterpage . "/index." . $lang . "." . $ext;
                }
        }
-       return $origsubs{'targetpage'}->($page, $ext);
+       return $origsubs{'targetpage'}->($page, $ext, $filename);
 }
 
-sub myurlto ($$;$) {
+sub myurlto ($;$$) {
        my $to=shift;
        my $from=shift;
        my $absolute=shift;
@@ -620,7 +655,12 @@ sub myurlto ($$;$) {
        if (! length $to
            && $config{po_link_to} eq "current"
            && istranslatable('index')) {
-               return IkiWiki::beautify_urlpath(IkiWiki::baseurl($from) . "index." . lang($from) . ".$config{htmlext}");
+               if (defined $from) {
+                       return IkiWiki::beautify_urlpath(IkiWiki::baseurl($from) . "index." . lang($from) . ".$config{htmlext}");
+               }
+               else {
+                       return $origsubs{'urlto'}->($to,$from,$absolute);
+               }
        }
        # avoid using our injected beautify_urlpath if run by cgi_editpage,
        # so that one is redirected to the just-edited page rather than to the
@@ -674,40 +714,15 @@ sub myrootpage (@) {
        return $rootpage;
 }
 
-# ,----
-# | Blackboxes for private data
-# `----
-
-{
-       my %filtered;
-
-       sub alreadyfiltered($$) {
-               my $page=shift;
-               my $destpage=shift;
-
-               return exists $filtered{$page}{$destpage}
-                        && $filtered{$page}{$destpage} eq 1;
-       }
-
-       sub setalreadyfiltered($$) {
-               my $page=shift;
-               my $destpage=shift;
-
-               $filtered{$page}{$destpage}=1;
-       }
-
-       sub unsetalreadyfiltered($$) {
-               my $page=shift;
-               my $destpage=shift;
-
-               if (exists $filtered{$page}{$destpage}) {
-                       delete $filtered{$page}{$destpage};
-               }
-       }
+sub myisselflink ($$) {
+       my $page=shift;
+       my $link=shift;
 
-       sub resetalreadyfiltered() {
-               undef %filtered;
+       return 1 if $origsubs{'isselflink'}->($page, $link);
+       if (istranslation($page)) {
+               return $origsubs{'isselflink'}->(masterpage($page), $link);
        }
+       return;
 }
 
 # ,----
@@ -729,6 +744,7 @@ sub istranslatablefile ($) {
        my $type=pagetype($file);
        return 0 if ! defined $type || $type eq 'po';
        return 0 if $file =~ /\.pot$/;
+       return 0 if ! defined $config{po_translatable_pages};
        return 1 if pagespec_match(pagename($file), $config{po_translatable_pages});
        return;
 }
@@ -741,6 +757,15 @@ sub istranslatable ($) {
        return;
 }
 
+sub istranslatedto ($$) {
+       my $page=shift;
+       my $destlang = shift;
+
+       $page=~s#^/##;
+       return 0 unless istranslatable($page);
+       exists $pagesources{otherlanguage_page($page, $destlang)};
+}
+
 sub _istranslation ($) {
        my $page=shift;
 
@@ -752,11 +777,11 @@ sub _istranslation ($) {
                         && pagetype($file) eq 'po';
        return 0 if $file =~ /\.pot$/;
 
-       my ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/);
+       my ($masterpage, $lang) = ($page =~ /(.*)[.]($language_code_pattern)$/);
        return 0 unless defined $masterpage && defined $lang
                         && length $masterpage && length $lang
                         && defined $pagesources{$masterpage}
-                        && defined $config{po_slave_languages}{$lang};
+                        && defined $slavelanguages{$lang};
 
        return (maybe_add_leading_slash($masterpage, $hasleadingslash), $lang)
                if istranslatable($masterpage);
@@ -788,34 +813,64 @@ sub lang ($) {
        if (1 < (my ($masterpage, $lang) = _istranslation($page))) {
                return $lang;
        }
-       return $config{po_master_language}{code};
+       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 =~ /^[a-z]{2}$/;
+       return $code =~ /^$language_code_pattern$/;
 }
 
-sub otherlanguage ($$) {
+sub otherlanguage_page ($$) {
        my $page=shift;
        my $code=shift;
 
-       return masterpage($page) if $code eq $config{po_master_language}{code};
+       return masterpage($page) if $code eq $master_language_code;
        return masterpage($page) . '.' . $code;
 }
 
-sub otherlanguages ($) {
+# 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 @ret;
+       return \@ret unless istranslation($page) || istranslatable($page);
        my $curlang=lang($page);
        foreach my $lang
-               ($config{po_master_language}{code}, keys %{$config{po_slave_languages}}) {
+               ($master_language_code, @slavelanguages) {
                next if $lang eq $curlang;
-               $ret{$lang}=otherlanguage($page, $lang);
+               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;
 }
 
@@ -839,25 +894,25 @@ sub pofile ($$) {
 sub pofiles ($) {
        my $masterfile=shift;
 
-       return map pofile($masterfile, $_), (keys %{$config{po_slave_languages}});
+       return map pofile($masterfile, $_), @slavelanguages;
 }
 
 sub refreshpot ($) {
        my $masterfile=shift;
 
        my $potfile=potfile($masterfile);
-       my %options = ("markdown" => (pagetype($masterfile) eq 'mdwn') ? 1 : 0);
-       my $doc=Locale::Po4a::Chooser::new('text',%options);
+       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->{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');
+       $doc->{TT}{po_out}->set_charset('UTF-8');
        # do the actual work
        $doc->parse;
        IkiWiki::prep_writefile(basename($potfile),dirname($potfile));
@@ -891,10 +946,9 @@ sub refreshpofiles ($@) {
                }
 
                if (-e $pofile) {
-                       system("msgmerge", "--previous", "-q", "-U", "--backup=none", $pofile, $potfile) == 0
-                               or error("po(refreshpofiles) ".
-                                        sprintf(gettext("failed to update %s"),
-                                                $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)
@@ -938,15 +992,13 @@ sub percenttranslated ($) {
        return gettext("N/A") unless istranslation($page);
        my $file=srcfile($pagesources{$page});
        my $masterfile = srcfile($pagesources{masterpage($page)});
-       my %options = (
-               "markdown" => (pagetype($masterfile) eq 'mdwn') ? 1 : 0,
-       );
-       my $doc=Locale::Po4a::Chooser::new('text',%options);
+       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',
+               '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();
@@ -957,10 +1009,10 @@ sub percenttranslated ($) {
 sub languagename ($) {
        my $code=shift;
 
-       return $config{po_master_language}{name}
-               if $code eq $config{po_master_language}{code};
-       return $config{po_slave_languages}{$code}
-               if defined $config{po_slave_languages}{$code};
+       return $master_language_name
+               if $code eq $master_language_code;
+       return $slavelanguages{$code}
+               if defined $slavelanguages{$code};
        return;
 }
 
@@ -968,30 +1020,29 @@ sub otherlanguagesloop ($) {
        my $page=shift;
 
        my @ret;
-       my %otherpages=%{otherlanguages($page)};
-       while (my ($lang, $otherpage) = each %otherpages) {
-               if (istranslation($page) && masterpage($page) eq $otherpage) {
-                       push @ret, {
-                               url => urlto_with_orig_beautiful_urlpath($otherpage, $page),
-                               code => $lang,
-                               language => languagename($lang),
-                               master => 1,
-                       };
-               }
-               elsif (istranslation($otherpage)) {
-                       push @ret, {
-                               url => urlto_with_orig_beautiful_urlpath($otherpage, $page),
-                               code => $lang,
-                               language => languagename($lang),
-                               percent => percenttranslated($otherpage),
-                       }
+       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 sort {
-               return -1 if $a->{code} eq $config{po_master_language}{code};
-               return 1 if $b->{code} eq $config{po_master_language}{code};
-               return $a->{language} cmp $b->{language};
-       } @ret;
+       return @ret;
 }
 
 sub homepageurl (;$) {
@@ -1004,7 +1055,7 @@ sub ishomepage ($) {
        my $page = shift;
 
        return 1 if $page eq 'index';
-       map { return 1 if $page eq 'index.'.$_ } keys %{$config{po_slave_languages}};
+       map { return 1 if $page eq 'index.'.$_ } @slavelanguages;
        return undef;
 }
 
@@ -1019,35 +1070,35 @@ sub deletetranslations ($) {
                if (-e $absfile && ! -l $absfile && ! -d $absfile) {
                        push @todelete, $file;
                }
-       } keys %{$config{po_slave_languages}};
+       } @slavelanguages;
 
        map {
                if ($config{rcs}) {
                        IkiWiki::rcs_remove($_);
                }
                else {
-                       IkiWiki::prune("$config{srcdir}/$_");
+                       IkiWiki::prune("$config{srcdir}/$_", $config{srcdir});
                }
        } @todelete;
 
        if (@todelete) {
                commit_and_refresh(
-                       gettext("removed obsolete PO files"),
-                       "IkiWiki::Plugin::po::deletetranslations");
+                       gettext("removed obsolete PO files"));
        }
 }
 
-sub commit_and_refresh ($$) {
-       my ($msg, $author) = (shift, shift);
+sub commit_and_refresh ($) {
+       my $msg = shift;
 
        if ($config{rcs}) {
                IkiWiki::disable_commit_hook();
-               IkiWiki::rcs_commit_staged($msg, $author, "127.0.0.1");
+               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.
@@ -1091,15 +1142,13 @@ sub po_to_markup ($$) {
                or return $fail->(sprintf(gettext("failed to write %s"), $infile));
 
        my $masterfile = srcfile($pagesources{masterpage($page)});
-       my %options = (
-               "markdown" => (pagetype($masterfile) eq 'mdwn') ? 1 : 0,
-       );
-       my $doc=Locale::Po4a::Chooser::new('text',%options);
+       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',
+               '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));
@@ -1151,12 +1200,65 @@ sub isvalidpo ($) {
        unlink $infile;
 
        if ($res) {
-           return IkiWiki::SuccessReason->new("valid gettext data");
+               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
 # `----
@@ -1191,7 +1293,7 @@ sub match_lang ($$;@) {
 
        my $regexp=IkiWiki::glob2re($wanted);
        my $lang=IkiWiki::Plugin::po::lang($page);
-       if ($lang !~ /^$regexp$/i) {
+       if ($lang !~ $regexp) {
                return IkiWiki::FailReason->new("file language is $lang, not $wanted");
        }
        else {
@@ -1217,4 +1319,32 @@ sub match_currentlang ($$;@) {
        }
 }
 
+sub match_needstranslation ($$;@) {
+       my $page=shift;
+       my $wanted=shift;
+
+       if (defined $wanted && $wanted ne "") {
+               if ($wanted !~ /^\d+$/) {
+                       return IkiWiki::FailReason->new("parameter is not an integer");
+               }
+               elsif ($wanted > 100) {
+                       return IkiWiki::FailReason->new("parameter is greater than 100");
+               }
+       }
+       else {
+               $wanted=100;
+       }
+
+       my $percenttranslated=IkiWiki::Plugin::po::percenttranslated($page);
+       if ($percenttranslated eq 'N/A') {
+               return IkiWiki::FailReason->new("file is not a translatable page");
+       }
+       elsif ($percenttranslated < $wanted) {
+               return IkiWiki::SuccessReason->new("file has $percenttranslated translated");
+       }
+       else {
+               return IkiWiki::FailReason->new("file is translated enough");
+       }
+}
+
 1