+ my %torename = %{$params{torename}};
+ my $session = $params{session};
+
+ # Save the page(s) the user asked to rename, so that our
+ # canrename hook can tell the difference between:
+ # - a translation being renamed as a consequence of its master page
+ # being renamed
+ # - a user trying to directly rename a translation
+ # This is why this hook has to be run first, before the list of pages
+ # to rename is modified by other plugins.
+ my @orig_torename;
+ @orig_torename=@{$session->param("po_orig_torename")}
+ if defined $session->param("po_orig_torename");
+ push @orig_torename, $torename{src};
+ $session->param(po_orig_torename => \@orig_torename);
+ IkiWiki::cgi_savesession($session);
+
+ return () unless istranslatable($torename{src});
+
+ my @ret;
+ my %otherpages=%{otherlanguages($torename{src})};
+ while (my ($lang, $otherpage) = each %otherpages) {
+ push @ret, {
+ src => $otherpage,
+ srcfile => $pagesources{$otherpage},
+ dest => otherlanguage($torename{dest}, $lang),
+ destfile => $torename{dest}.".".$lang.".po",
+ required => 0,
+ };
+ }
+ return @ret;
+}
+
+sub mydelete (@) {
+ my @deleted=@_;
+
+ map { deletetranslations($_) } grep istranslatablefile($_), @deleted;
+}
+
+sub change (@) {
+ 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.
+ foreach my $file (grep {istranslatablefile($_)} @rendered) {
+ my $masterfile=srcfile($file);
+ my $page=pagename($file);
+ my $updated_pot_file=0;
+
+ # Avoid touching underlay files.
+ next if $masterfile ne "$config{srcdir}/$file";
+
+ # Only refresh POT file if it does not exist, or if
+ # the source was changed: don't if only the HTML was
+ # refreshed, e.g. because of a dependency.
+ if ((grep { $_ eq $pagesources{$page} } @origneedsbuild) ||
+ ! -e potfile($masterfile)) {
+ refreshpot($masterfile);
+ $updated_pot_file=1;
+ }
+ my @pofiles;
+ foreach my $po (pofiles($masterfile)) {
+ next if ! $updated_pot_file && -e $po;
+ next if grep { $po=~/\Q$_\E/ } @{$config{underlaydirs}};
+ push @pofiles, $po;
+ }
+ if (@pofiles) {
+ refreshpofiles($masterfile, @pofiles);
+ map { s/^\Q$config{srcdir}\E\/*//; IkiWiki::rcs_add($_) } @pofiles if $config{rcs};
+ $updated_po_files=1;
+ }
+ }
+
+ if ($updated_po_files) {
+ commit_and_refresh(
+ gettext("updated PO files"),
+ "IkiWiki::Plugin::po::change");
+ }
+}
+
+sub checkcontent (@) {
+ my %params=@_;
+
+ if (istranslation($params{page})) {
+ my $res = isvalidpo($params{content});
+ if ($res) {
+ return undef;
+ }
+ else {
+ return "$res";
+ }
+ }
+ return undef;
+}
+
+sub canremove (@) {
+ my %params = @_;
+
+ if (istranslation($params{page})) {
+ return gettext("Can not remove a translation. If the master page is removed, ".
+ "however, its translations will be removed as well.");
+ }
+ return undef;
+}
+
+sub canrename (@) {
+ my %params = @_;
+ my $session = $params{session};
+
+ if (istranslation($params{src})) {
+ my $masterpage = masterpage($params{src});
+ # Tell the difference between:
+ # - a translation being renamed as a consequence of its master page
+ # being renamed, which is allowed
+ # - a user trying to directly rename a translation, which is forbidden
+ # by looking for the master page in the list of to-be-renamed pages we
+ # saved early in the renaming process.
+ my $orig_torename = $session->param("po_orig_torename");
+ unless (grep { $_ eq $masterpage } @{$orig_torename}) {
+ return gettext("Can not rename a translation. If the master page is renamed, ".
+ "however, its translations will be renamed as well.");
+ }
+ }
+ 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};
+ my $q=$params{cgi};
+
+ return unless defined $form->field("do");
+
+ 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});
+ $form->tmpl_param(message => $template->output);
+ }
+ elsif ($form->field("do") eq "edit") {
+ # Remove the rename/remove buttons on slave pages.
+ # This has to be done after the rename/remove plugins have added
+ # 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"))) {
+ map {
+ for (my $i = 0; $i < @{$params{buttons}}; $i++) {
+ if (@{$params{buttons}}[$i] eq $_) {
+ delete @{$params{buttons}}[$i];
+ last;
+ }
+ }
+ } qw(Rename Remove);
+ }
+ }
+}
+
+sub formbuilder (@) {
+ my %params=@_;
+ my $form=$params{form};
+ my $q=$params{cgi};
+
+ return unless defined $form->field("do");
+
+ # Do not allow to create pages of type po: they are automatically created.
+ # The main reason to do so is to bypass the "favor the type of linking page
+ # on page creation" logic, which is unsuitable when a broken link is clicked
+ # on a slave (PO) page.
+ # 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) {
+ next unless "$field" eq "type";
+ next unless $field->type eq 'select';
+ my $orig_value = $field->value;
+ # remove po from the list of types
+ my @types = grep { $_->[0] ne 'po' } $field->options;
+ $field->options(\@types) if @types;
+ # favor the type of linking page's masterpage
+ if ($orig_value eq 'po') {
+ my ($from, $type);
+ if (defined $form->field('from')) {
+ ($from)=$form->field('from')=~/$config{wiki_file_regexp}/;
+ $from = masterpage($from);
+ }
+ if (defined $from && exists $pagesources{$from}) {
+ $type=pagetype($pagesources{$from});
+ }
+ $type=$config{default_pageext} unless defined $type;
+ $field->value($type) ;
+ }
+ }
+ }
+}
+
+# ,----
+# | Injected functions
+# `----
+
+# Implement po_link_to 'current' and 'negotiated' settings.
+sub mybestlink ($$) {
+ my $page=shift;
+ my $link=shift;
+
+ return $origsubs{'bestlink'}->($page, $link)
+ 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)
+ && istranslation($page)
+ && !(exists $caller[3] && defined $caller[3]
+ && ($caller[3] eq "IkiWiki::PageSpec::match_link"))) {
+ return $res . "." . lang($page);
+ }
+ return $res;
+}
+
+sub mybeautify_urlpath ($) {
+ my $url=shift;
+
+ my $res=$origsubs{'beautify_urlpath'}->($url);
+ if (defined $config{po_link_to} && $config{po_link_to} eq "negotiated") {
+ $res =~ s!/\Qindex.$config{po_master_language}{code}.$config{htmlext}\E$!/!;
+ $res =~ s!/\Qindex.$config{htmlext}\E$!/!;
+ map {
+ $res =~ s!/\Qindex.$_.$config{htmlext}\E$!/!;
+ } (keys %{$config{po_slave_languages}});
+ }
+ return $res;
+}
+
+sub mytargetpage ($$) {
+ my $page=shift;
+ my $ext=shift;
+
+ if (istranslation($page) || istranslatable($page)) {
+ my ($masterpage, $lang) = (masterpage($page), lang($page));
+ if (! $config{usedirs} || $masterpage eq 'index') {