X-Git-Url: https://sipb.mit.edu/gitweb.cgi/ikiwiki.git/blobdiff_plain/d1642e4cd9d77b2be6c20690912559b89b1dce2b..741c33576e358d555e623a49d42edd33924a19ab:/IkiWiki/Plugin/po.pm diff --git a/IkiWiki/Plugin/po.pm b/IkiWiki/Plugin/po.pm index 61331edeb..2939bcd9a 100644 --- a/IkiWiki/Plugin/po.pm +++ b/IkiWiki/Plugin/po.pm @@ -1,7 +1,7 @@ #!/usr/bin/perl # .po as a wiki page type # Licensed under GPL v2 or greater -# Copyright (C) 2008 intrigeri +# Copyright (C) 2008-2009 intrigeri # inspired by the GPL'd po4a-translate, # which is Copyright 2002, 2003, 2004 by Martin Quinson (mquinson#debian.org) package IkiWiki::Plugin::po; @@ -10,6 +10,7 @@ use warnings; use strict; use IkiWiki 3.00; use Encode; +use Locale::Po4a::Common qw(nowrapi18n !/.*/); use Locale::Po4a::Chooser; use Locale::Po4a::Po; use File::Basename; @@ -31,19 +32,19 @@ sub import { hook(type => "getsetup", id => "po", call => \&getsetup); hook(type => "checkconfig", id => "po", call => \&checkconfig); hook(type => "needsbuild", id => "po", call => \&needsbuild); - hook(type => "scan", id => "po", call => \&scan, last =>1); + hook(type => "scan", id => "po", call => \&scan, last => 1); hook(type => "filter", id => "po", call => \&filter); hook(type => "htmlize", id => "po", call => \&htmlize); hook(type => "pagetemplate", id => "po", call => \&pagetemplate, last => 1); - hook(type => "postscan", id => "po", call => \&postscan); 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 => "cansave", id => "po", call => \&cansave); + 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); + 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); @@ -53,8 +54,8 @@ sub import { inject(name => "IkiWiki::targetpage", call => \&mytargetpage); $origsubs{'urlto'}=\&IkiWiki::urlto; inject(name => "IkiWiki::urlto", call => \&myurlto); - $origsubs{'nicepagetitle'}=\&IkiWiki::nicepagetitle; - inject(name => "IkiWiki::nicepagetitle", call => \&mynicepagetitle); + $origsubs{'cgiurl'}=\&IkiWiki::cgiurl; + inject(name => "IkiWiki::cgiurl", call => \&mycgiurl); } @@ -66,7 +67,7 @@ sub import { # 2. Injected functions # 3. Blackboxes for private data # 4. Helper functions -# 5. PageSpec's +# 5. PageSpecs # ,---- @@ -115,23 +116,18 @@ sub getsetup () { safe => 1, rebuild => 1, }, - po_translation_status_in_links => { - type => "boolean", - example => 1, - description => "display translation status in links to translations", - safe => 1, - rebuild => 1, - }, } sub checkconfig () { foreach my $field (qw{po_master_language po_slave_languages}) { if (! exists $config{$field} || ! defined $config{$field}) { - error(sprintf(gettext("Must specify %s"), $field)); + error(sprintf(gettext("Must specify %s when using the %s plugin"), + $field, 'po')); } } if (! (keys %{$config{po_slave_languages}})) { - error(gettext("At least one slave language must be defined in po_slave_languages")); + error(gettext("At least one slave language must be defined ". + "in po_slave_languages when using the po plugin")); } map { islanguagecode($_) @@ -145,21 +141,15 @@ sub checkconfig () { ! defined $config{po_link_to}) { $config{po_link_to}='default'; } - elsif (! grep { - $config{po_link_to} eq $_ - } ('default', 'current', 'negotiated')) { - warn(sprintf(gettext('po_link_to=%s is not a valid setting, falling back to po_link_to=default'), - $config{po_link_to})); + elsif ($config{po_link_to} !~ /^(default|current|negotiated)$/) { + warn(sprintf(gettext('%s is not a valid value for po_link_to, falling back to po_link_to=default'), + $config{po_link_to})); $config{po_link_to}='default'; } elsif ($config{po_link_to} eq "negotiated" && ! $config{usedirs}) { warn(gettext('po_link_to=negotiated requires usedirs to be enabled, falling back to po_link_to=default')); $config{po_link_to}='default'; } - if (! exists $config{po_translation_status_in_links} || - ! defined $config{po_translation_status_in_links}) { - $config{po_translation_status_in_links}=1; - } push @{$config{wiki_file_prune_regexps}}, qr/\.pot$/; } @@ -188,8 +178,6 @@ sub scan (@) { my $page=$params{page}; my $content=$params{content}; - return unless UNIVERSAL::can("IkiWiki::Plugin::link", "import"); - if (istranslation($page)) { foreach my $destpage (@{$links{$page}}) { if (istranslatable($destpage)) { @@ -244,8 +232,8 @@ sub htmlize (@) { # force content to be htmlize'd as if it was the same type as the master page return IkiWiki::htmlize($page, $page, - pagetype(srcfile($pagesources{masterpage($page)})), - $content); + pagetype(srcfile($pagesources{masterpage($page)})), + $content); } sub pagetemplate (@) { @@ -272,22 +260,20 @@ sub pagetemplate (@) { $template->param(otherlanguages => [otherlanguagesloop($page)]); map add_depends($page, $_), (values %{otherlanguages($page)}); } - # Rely on IkiWiki::Render's genpage() to decide wether - # a discussion link should appear on $page; this is not - # totally accurate, though: some broken links may be generated - # when cgiurl is disabled. - # This compromise avoids some code duplication, and will probably - # prevent future breakage when ikiwiki internals change. - # Known limitations are preferred to future random bugs. - if ($template->param('discussionlink') && istranslation($page)) { - $template->param('discussionlink' => htmllink( - $page, - $destpage, - $masterpage . '/' . gettext("Discussion"), - noimageinline => 1, - forcesubpage => 0, - linktext => gettext("Discussion"), - )); + if ($config{discussion} && istranslation($page)) { + my $discussionlink=gettext("discussion"); + if ($page !~ /.*\/\Q$discussionlink\E$/i && + (length $config{cgiurl} || + exists $links{$masterpage."/".$discussionlink})) { + $template->param('discussionlink' => htmllink( + $page, + $destpage, + $masterpage . '/' . gettext("Discussion"), + noimageinline => 1, + forcesubpage => 0, + linktext => gettext("Discussion"), + )); + } } # Remove broken parentlink to ./index.html on home page's translations. # It works because this hook has the "last" parameter set, to ensure it @@ -299,59 +285,88 @@ sub pagetemplate (@) { } } # }}} -sub postscan (@) { - my %params = @_; - my $page = $params{page}; - - # backlinks involve back-dependencies, so that nicepagetitle effects, - # such as translation status displayed in links, are updated - use IkiWiki::Render; - map add_depends($page, $_), keys %{$IkiWiki::backlinks{$page}}; -} - # Add the renamed page translations to the list of to-be-renamed pages. -sub renamepages($$$) { - my ($torename, $cgi, $session) = (shift, shift, shift); +sub renamepages (@) { + my %params = @_; - # copy the initial array, so that we can iterate on it AND - # modify it at the same time, without iterating on the items we - # pushed on it ourselves - my @torename=@{$torename}; + 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 @torename is modified - # by other plugins. - $session->param(po_orig_torename => [ @torename ]); + # 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); - foreach my $rename (@torename) { - next unless istranslatable($rename->{src}); - my %otherpages=%{otherlanguages($rename->{src})}; - while (my ($lang, $otherpage) = each %otherpages) { - push @{$torename}, { - src => $otherpage, - srcfile => $pagesources{$otherpage}, - dest => otherlanguage($rename->{dest}, $lang), - destfile => $rename->{dest}.".".$lang.".po", - required => 0, - }; - } + 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(@) { +sub mydelete (@) { my @deleted=@_; map { deletetranslations($_) } grep istranslatablefile($_), @deleted; } -sub change(@) { +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("re-rendering all pages to fix meta titles"))); + resetalreadyfiltered(); + require IkiWiki::Render; + foreach my $file (@rendered) { + debug(sprintf(gettext("rendering %s"), $file)); + IkiWiki::render($file); + } + } + my $updated_po_files=0; # Refresh/create POT and PO files as needed. @@ -385,11 +400,11 @@ sub change(@) { } } -sub cansave ($$$$) { - my ($page, $content, $cgi, $session) = (shift, shift, shift, shift); +sub checkcontent (@) { + my %params=@_; - if (istranslation($page)) { - my $res = isvalidpo($content); + if (istranslation($params{page})) { + my $res = isvalidpo($params{content}); if ($res) { return undef; } @@ -400,19 +415,19 @@ sub cansave ($$$$) { return undef; } -sub canremove ($$$) { - my ($page, $cgi, $session) = (shift, shift, shift); +sub canremove (@) { + my %params = @_; - if (istranslation($page)) { + if (istranslation($params{page})) { return gettext("Can not remove a translation. Removing the master page, ". "though, removes its translations as well."); } return undef; } -sub canrename ($$@) { - my ($cgi, $session) = (shift, shift); +sub canrename (@) { my %params = @_; + my $session = $params{session}; if (istranslation($params{src})) { my $masterpage = masterpage($params{src}); @@ -423,7 +438,7 @@ sub canrename ($$@) { # 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 (scalar grep { $_->{src} eq $masterpage } @{$orig_torename}) { + unless (grep { $_ eq $masterpage } @{$orig_torename}) { return gettext("Can not rename a translation. Renaming the master page, ". "though, renames its translations as well."); } @@ -445,13 +460,57 @@ sub formbuilder_setup (@) { my $form=$params{form}; my $q=$params{cgi}; - return unless (defined $form->field("do") && $form->field("do") eq "create"); + return unless defined $form->field("do"); - my $template=template("pocreatepage.tmpl"); - $template->param(LANG => $config{po_master_language}{name}); - $form->tmpl_param(message => $template->output); + 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"; + if ($field->type eq 'select') { + # remove po from the list of types + my @types = grep { $_ ne 'po' } $field->options; + $field->options(\@types) if @types; + } + } + } +} # ,---- # | Injected functions @@ -535,13 +594,14 @@ sub myurlto ($$;$) { } } -sub mynicepagetitle ($;$) { - my ($page, $unescaped) = (shift, shift); +sub mycgiurl (@) { + my %params=@_; - my $res = $origsubs{'nicepagetitle'}->($page, $unescaped); - return $res unless istranslation($page); - return $res unless $config{po_translation_status_in_links}; - return $res.' ('.percenttranslated($page).' %)'; + # slave pages have no subpages + if (istranslation($params{'from'})) { + $params{'from'} = masterpage($params{'from'}); + } + return $origsubs{'cgiurl'}->(%params); } # ,---- @@ -555,8 +615,8 @@ sub mynicepagetitle ($;$) { my $page=shift; my $destpage=shift; - return ( exists $filtered{$page}{$destpage} - && $filtered{$page}{$destpage} eq 1 ); + return exists $filtered{$page}{$destpage} + && $filtered{$page}{$destpage} eq 1; } sub setalreadyfiltered($$) { @@ -596,8 +656,10 @@ sub istranslatablefile ($) { my $file=shift; return 0 unless defined $file; - return 0 if (defined pagetype($file) && pagetype($file) eq 'po'); + my $type=pagetype($file); + return 0 if ! defined $type || $type eq 'po'; return 0 if $file =~ /\.pot$/; + return 0 unless -e "$config{srcdir}/$file"; # underlay dirs may be read-only return 1 if pagespec_match(pagename($file), $config{po_translatable_pages}); return; } @@ -613,18 +675,19 @@ sub istranslatable ($) { sub _istranslation ($) { my $page=shift; + $page='' unless defined $page && length $page; my $hasleadingslash = ($page=~s#^/##); my $file=$pagesources{$page}; - return 0 unless (defined $file + return 0 unless defined $file && defined pagetype($file) - && pagetype($file) eq 'po'); + && pagetype($file) eq 'po'; return 0 if $file =~ /\.pot$/; my ($masterpage, $lang) = ($page =~ /(.*)[.]([a-z]{2})$/); - return 0 unless (defined $masterpage && defined $lang + return 0 unless defined $masterpage && defined $lang && length $masterpage && length $lang && defined $pagesources{$masterpage} - && defined $config{po_slave_languages}{$lang}); + && defined $config{po_slave_languages}{$lang}; return (maybe_add_leading_slash($masterpage, $hasleadingslash), $lang) if istranslatable($masterpage); @@ -638,7 +701,7 @@ sub istranslation ($) { $translations{$masterpage}{$lang}=$page unless exists $translations{$masterpage}{$lang}; return (maybe_add_leading_slash($masterpage, $hasleadingslash), $lang); } - return; + return ""; } sub masterpage ($) { @@ -662,7 +725,7 @@ sub lang ($) { sub islanguagecode ($) { my $code=shift; - return ($code =~ /^[a-z]{2}$/); + return $code =~ /^[a-z]{2}$/; } sub otherlanguage ($$) { @@ -677,7 +740,7 @@ sub otherlanguages ($) { my $page=shift; my %ret; - return \%ret unless (istranslation($page) || istranslatable($page)); + return \%ret unless istranslation($page) || istranslatable($page); my $curlang=lang($page); foreach my $lang ($config{po_master_language}{code}, keys %{$config{po_slave_languages}}) { @@ -720,9 +783,10 @@ sub refreshpot ($) { $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. + # 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 @@ -736,17 +800,23 @@ sub refreshpofiles ($@) { my @pofiles=@_; my $potfile=potfile($masterfile); - error("[po/refreshpofiles] POT file ($potfile) does not exist") unless (-e $potfile); + 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) { - system("msgmerge", "-U", "--backup=none", $pofile, $potfile) == 0 - or error("[po/refreshpofiles:$pofile] failed to update"); + system("msgmerge", "-q", "-U", "--backup=none", $pofile, $potfile) == 0 + or error("po(refreshpofiles) ". + sprintf(gettext("failed to update %s"), + $pofile)); } else { File::Copy::syscopy($potfile,$pofile) - or error("[po/refreshpofiles:$pofile] failed to copy the POT file"); + or error("po(refreshpofiles) ". + sprintf(gettext("failed to copy the POT file to %s"), + $pofile)); } } } @@ -793,7 +863,8 @@ sub percenttranslated ($) { 'file_in_name' => [ $masterfile ], 'file_in_charset' => 'utf-8', 'file_out_charset' => 'utf-8', - ) or error("[po/percenttranslated:$page]: failed to translate"); + ) or error("po(percenttranslated) ". + sprintf(gettext("failed to translate %s"), $page)); my ($percent,$hit,$queries) = $doc->stats(); $percent =~ s/\.[0-9]+$//; return $percent; @@ -833,10 +904,10 @@ sub otherlanguagesloop ($) { } } 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 -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; } sub homepageurl (;$) { @@ -867,7 +938,7 @@ sub deletetranslations ($) { } } @todelete; - if (scalar @todelete) { + if (@todelete) { commit_and_refresh( gettext("removed obsolete PO files"), "IkiWiki::Plugin::po::deletetranslations"); @@ -922,17 +993,17 @@ sub po_to_markup ($$;$) { DIR => File::Spec->tmpdir, UNLINK => 1)->filename; - sub failure ($) { - my $msg = '[po/po_to_markup:'.$page.'] ' . shift; + my $fail = sub ($) { + my $msg = "po(po_to_markup) - $page : " . shift; if ($nonfatal) { warn $msg; return undef; } error($msg, sub { unlink $infile, $outfile}); - } + }; writefile(basename($infile), File::Spec->tmpdir, $content) - or return failure("failed to write $infile"); + or return $fail->(sprintf(gettext("failed to write %s"), $infile)); my $masterfile = srcfile($pagesources{masterpage($page)}); my %options = ( @@ -944,10 +1015,12 @@ sub po_to_markup ($$;$) { 'file_in_name' => [ $masterfile ], 'file_in_charset' => 'utf-8', 'file_out_charset' => 'utf-8', - ) or return failure("failed to translate"); - $doc->write($outfile) or return failure("could not write $outfile"); + ) or return $fail->(gettext("failed to translate")); + $doc->write($outfile) + or return $fail->(sprintf(gettext("failed to write %s"), $outfile)); - $content = readfile($outfile) or return failure("could not read $outfile"); + $content = readfile($outfile) + or return $fail->(sprintf(gettext("failed to read %s"), $outfile)); # Unlinking should happen automatically, thanks to File::Temp, # but it does not work here, probably because of the way writefile() @@ -977,14 +1050,14 @@ sub isvalidpo ($) { DIR => File::Spec->tmpdir, UNLINK => 1)->filename; - sub failure ($) { + my $fail = sub ($) { my $msg = '[po/isvalidpo] ' . shift; unlink $infile; return IkiWiki::FailReason->new("$msg"); - } + }; writefile(basename($infile), File::Spec->tmpdir, $content) - or return failure("failed to write $infile"); + or return $fail->(sprintf(gettext("failed to write %s"), $infile)); my $res = (system("msgfmt", "--check", $infile, "-o", "/dev/null") == 0); @@ -996,17 +1069,15 @@ sub isvalidpo ($) { if ($res) { return IkiWiki::SuccessReason->new("valid gettext data"); } - return IkiWiki::FailReason->new("invalid gettext data"); + return IkiWiki::FailReason->new("invalid gettext data, go back ". + "to previous page to go on with edit"); } # ,---- -# | PageSpec's +# | PageSpecs # `---- package IkiWiki::PageSpec; -use warnings; -use strict; -use IkiWiki 2.00; sub match_istranslation ($;@) { my $page=shift; @@ -1036,7 +1107,7 @@ sub match_lang ($$;@) { my $regexp=IkiWiki::glob2re($wanted); my $lang=IkiWiki::Plugin::po::lang($page); - if ($lang!~/^$regexp$/i) { + if ($lang !~ /^$regexp$/i) { return IkiWiki::FailReason->new("file language is $lang, not $wanted"); } else {