X-Git-Url: https://sipb.mit.edu/gitweb.cgi/ikiwiki.git/blobdiff_plain/6c0f9c691c3df3a2ec30dec626c997623568a400..98cc9460ac67fee606437712882cfa1e5d259729:/IkiWiki/Plugin/po.pm diff --git a/IkiWiki/Plugin/po.pm b/IkiWiki/Plugin/po.pm index 2b2839f24..9cb047992 100644 --- a/IkiWiki/Plugin/po.pm +++ b/IkiWiki/Plugin/po.pm @@ -28,6 +28,7 @@ use UNIVERSAL; my %translations; my @origneedsbuild; my %origsubs; +my @slavelanguages; # orderer as in config po_slave_languages memoize("istranslatable"); memoize("_istranslation"); @@ -51,16 +52,22 @@ sub import { 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); + 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); + $origsubs{'rootpage'}=\&IkiWiki::rootpage; + inject(name => "IkiWiki::rootpage", call => \&myrootpage); + $origsubs{'isselflink'}=\&IkiWiki::isselflink; + inject(name => "IkiWiki::isselflink", call => \&myisselflink); + } } @@ -83,7 +90,8 @@ sub getsetup () { return plugin => { safe => 0, - rebuild => 1, + rebuild => 1, # format plugin + section => "format", }, po_master_language => { type => "string", @@ -97,11 +105,11 @@ sub getsetup () { }, po_slave_languages => { type => "string", - example => { + example => [ 'fr' => 'Français', 'es' => 'Español', 'de' => 'Deutsch' - }, + ], description => "slave languages (PO files)", safe => 1, rebuild => 1, @@ -131,6 +139,22 @@ sub checkconfig () { } } + if (ref $config{po_slave_languages} eq 'ARRAY') { + my %slaves; + for (my $i=0; $i<@{$config{po_slave_languages}}; $i = $i + 2) { + $slaves{$config{po_slave_languages}->[$i]} = $config{po_slave_languages}->[$i + 1]; + push @slavelanguages, $config{po_slave_languages}->[$i]; + } + $config{po_slave_languages} = \%slaves; + } + elsif (ref $config{po_slave_languages} eq 'HASH') { + @slavelanguages = sort { + $config{po_slave_languages}->{$a} cmp $config{po_slave_languages}->{$b}; + } keys %{$config{po_slave_languages}}; + } + + delete $config{po_slave_languages}{$config{po_master_language}{code}};; + map { islanguagecode($_) or error(sprintf(gettext("%s is not a valid language code"), $_)); @@ -171,7 +195,8 @@ sub checkconfig () { if ($config{po_master_language}{code} ne 'en') { # Add underlay containing translated source files # for the master language. - add_underlay("locale/$config{po_master_language}{code}/$underlay"); + add_underlay("locale/$config{po_master_language}{code}/$underlay") + if -d "$config{underlaydirbase}/locale/$config{po_master_language}{code}/$underlay"; } } } @@ -188,7 +213,7 @@ 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)}; } } @@ -220,7 +245,7 @@ sub scan (@) { # make sure any destpage's translations has # $page in its backlinks push @{$links{$page}}, - values %{otherlanguages($destpage)}; + values %{otherlanguages_pages($destpage)}; } } } @@ -278,7 +303,7 @@ sub pagetemplate (@) { } 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 && @@ -305,7 +330,7 @@ sub pagetemplate (@) { if (ishomepage($page) && $template->query(name => "title")) { $template->param(title => $config{wikiname}); } -} # }}} +} # Add the renamed page translations to the list of to-be-renamed pages. sub renamepages (@) { @@ -331,12 +356,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, }; @@ -384,31 +409,32 @@ sub change (@) { resetalreadyfiltered(); require IkiWiki::Render; foreach my $file (@rendered) { - debug(sprintf(gettext("building %s"), $file)); - IkiWiki::render($file); + IkiWiki::render($file, sprintf(gettext("building %s"), $file)); } } my $updated_po_files=0; # Refresh/create POT and PO files as needed. - # (But avoid doing so if they are in an underlay directory.) 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 - # $pagesources{$page} was changed: don't if only the HTML was + # the source was changed: don't if only the HTML was # refreshed, e.g. because of a dependency. - if ($masterfile eq "$config{srcdir}/$file" && - ((grep { $_ eq $pagesources{$page} } @origneedsbuild) - || ! -e potfile($masterfile))) { + 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 ! $updated_pot_file && -e $po; next if grep { $po=~/\Q$_\E/ } @{$config{underlaydirs}}; push @pofiles, $po; } @@ -421,8 +447,7 @@ sub change (@) { if ($updated_po_files) { commit_and_refresh( - gettext("updated PO files"), - "IkiWiki::Plugin::po::change"); + gettext("updated PO files")); } } @@ -529,10 +554,23 @@ sub formbuilder (@) { 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; + 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) ; } } } @@ -548,13 +586,15 @@ 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 - && ($config{po_link_to} eq "current" || $config{po_link_to} eq "negotiated") && istranslatable($res) - && istranslation($page)) { + && istranslation($page) + && !(exists $caller[3] && defined $caller[3] + && ($caller[3] eq "IkiWiki::PageSpec::match_link"))) { return $res . "." . lang($page); } return $res; @@ -564,7 +604,7 @@ sub mybeautify_urlpath ($) { my $url=shift; my $res=$origsubs{'beautify_urlpath'}->($url); - if ($config{po_link_to} eq "negotiated") { + 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 { @@ -637,6 +677,33 @@ sub mycgiurl (@) { return $origsubs{'cgiurl'}->(%params); } +sub myrootpage (@) { + my %params=@_; + + my $rootpage; + if (exists $params{rootpage}) { + $rootpage=$origsubs{'bestlink'}->($params{page}, $params{rootpage}); + if (!length $rootpage) { + $rootpage=$params{rootpage}; + } + } + else { + $rootpage=masterpage($params{page}); + } + return $rootpage; +} + +sub myisselflink ($$) { + my $page=shift; + my $link=shift; + + return 1 if $origsubs{'isselflink'}->($page, $link); + if (istranslation($page)) { + return $origsubs{'isselflink'}->(masterpage($page), $link); + } + return; +} + # ,---- # | Blackboxes for private data # `---- @@ -692,6 +759,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; } @@ -760,7 +828,7 @@ sub islanguagecode ($) { return $code =~ /^[a-z]{2}$/; } -sub otherlanguage ($$) { +sub otherlanguage_page ($$) { my $page=shift; my $code=shift; @@ -768,17 +836,31 @@ sub otherlanguage ($$) { 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}}) { + ($config{po_master_language}{code}, @slavelanguages) { next if $lang eq $curlang; - $ret{$lang}=otherlanguage($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; } @@ -809,18 +891,18 @@ 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)); @@ -901,15 +983,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(); @@ -931,30 +1011,25 @@ 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 => $config{po_master_language}{code}, + language => $config{po_master_language}{name}, + master => 1, + }; + } + foreach my $lang (@{otherlanguages_codes($page)}) { + next if $lang eq $config{po_master_language}{code}; + my $otherpage = otherlanguage_page($page, $lang); + push @ret, { + url => urlto_with_orig_beautiful_urlpath($otherpage, $page), + code => $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 (;$) { @@ -995,17 +1070,18 @@ sub deletetranslations ($) { 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(); } @@ -1023,11 +1099,8 @@ sub commit_and_refresh ($$) { IkiWiki::saveindex(); } -# on success, returns the filtered content. -# on error, if $nonfatal, warn and return undef; else, error out. -sub po_to_markup ($$;$) { +sub po_to_markup ($$) { my ($page, $content) = (shift, shift); - my $nonfatal = shift; $content = '' unless defined $content; $content = decode_utf8(encode_utf8($content)); @@ -1050,10 +1123,6 @@ sub po_to_markup ($$;$) { my $fail = sub ($) { my $msg = "po(po_to_markup) - $page : " . shift; - if ($nonfatal) { - warn $msg; - return undef; - } error($msg, sub { unlink $infile, $outfile}); }; @@ -1061,21 +1130,18 @@ 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)); - $content = readfile($outfile) - or return $fail->(sprintf(gettext("failed to read %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() @@ -1128,6 +1194,37 @@ sub isvalidpo ($) { "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; + } + elsif ($pagetype eq 'mdwn') { + $options{markdown} = 1; + } + else { + $options{markdown} = 0; + } + + return %options; +} + # ,---- # | PageSpecs # `---- @@ -1188,4 +1285,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