From 06c126c03905cb8aec730659260add61e40e7fda Mon Sep 17 00:00:00 2001 From: Julian Maurice Date: Tue, 15 Dec 2015 09:03:20 +0100 Subject: [PATCH] Bug 15395: Plural translations in templates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Provides a way to handle translation of plural forms in templates Add Template::Toolkit plugin for Koha::I18N Use like this: [%# USE the plugin and define some macros %] [% PROCESS 'i18n.inc' %] [%# tn is the equivalent of __n %] [%# macro names can't start with underscore, t is for "translate" %] [% tn('item', 'items', num_items) %] Extraction of strings from templates is a bit complicated and use Template::Parser and PPI. Template is compiled into Perl code and then analyzed by PPI. It is slow, but should be correct even with complex constructions. Add dependency to PPI Test plan: 1. Open a template file (.tt or .inc) with your favorite text editor 2. Add the PROCESS directive mentioned above in the beginning of file 3. Use one of the t* macros defined in i18n.inc. They are used like their "__" equivalent, with one difference: the 'x' variants take a hashref instead of a hash as last parameter 4. cd misc/translator && ./translate update fr-FR 5. Open misc/translator/po/fr_FR/LC_MESSAGES/Koha.po and translate your string(s) 6. ./translate install fr-FR 7. Use your web browser to go to the page that should display the translation, change language and verify the translation is correct Signed-off-by: Marc VĂ©ron --- C4/Installer/PerlDependencies.pm | 5 ++ Koha/Template/Plugin/I18N.pm | 72 +++++++++++++++++++ koha-tmpl/intranet-tmpl/prog/en/includes/i18n.inc | 39 ++++++++++ koha-tmpl/opac-tmpl/bootstrap/en/includes/i18n.inc | 39 ++++++++++ misc/translator/LangInstaller.pm | 83 +++++++++++++++++++++- 5 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 Koha/Template/Plugin/I18N.pm create mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/i18n.inc create mode 100644 koha-tmpl/opac-tmpl/bootstrap/en/includes/i18n.inc diff --git a/C4/Installer/PerlDependencies.pm b/C4/Installer/PerlDependencies.pm index cbbc9cedac..ffdceee1ad 100644 --- a/C4/Installer/PerlDependencies.pm +++ b/C4/Installer/PerlDependencies.pm @@ -727,6 +727,11 @@ our $PERL_DEPS = { 'required' => '1', 'min_ver' => '1.20', }, + 'PPI' => { + 'usage' => 'I18N', + 'required' => '0', + 'min_ver' => '1.215', + }, 'LWP::Protocol::https' => { 'usage' => 'OverDrive integration', 'required' => '0', diff --git a/Koha/Template/Plugin/I18N.pm b/Koha/Template/Plugin/I18N.pm new file mode 100644 index 0000000000..8c95ea6d72 --- /dev/null +++ b/Koha/Template/Plugin/I18N.pm @@ -0,0 +1,72 @@ +package Koha::Template::Plugin::I18N; + +# Copyright BibLibre 2015 +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use base qw( Template::Plugin ); + +use C4::Context; +use Koha::I18N; + +sub t { + my ($self, $msgid) = @_; + return __($msgid); +} + +sub tx { + my ($self, $msgid, $vars) = @_; + return __x($msgid, %$vars); +} + +sub tn { + my ($self, $msgid, $msgid_plural, $count) = @_; + return __n($msgid, $msgid_plural, $count); +} + +sub tnx { + my ($self, $msgid, $msgid_plural, $count, $vars) = @_; + return __nx($msgid, $msgid_plural, $count, %$vars); +} + +sub txn { + my ($self, $msgid, $msgid_plural, $count, $vars) = @_; + return __xn($msgid, $msgid_plural, $count, %$vars); +} + +sub tp { + my ($self, $msgctxt, $msgid) = @_; + return __p($msgctxt, $msgid); +} + +sub tpx { + my ($self, $msgctxt, $msgid, $vars) = @_; + return __px($msgctxt, $msgid, %$vars); +} + +sub tnp { + my ($self, $msgctxt, $msgid, $msgid_plural, $count) = @_; + return __np($msgctxt, $msgid, $msgid_plural, $count); +} + +sub tnpx { + my ($self, $msgctxt, $msgid, $msgid_plural, $count, $vars) = @_; + return __np($msgctxt, $msgid, $msgid_plural, $count, %$vars); +} + +1; diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/i18n.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/i18n.inc new file mode 100644 index 0000000000..4b5da9231b --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/i18n.inc @@ -0,0 +1,39 @@ +[% + USE I18N; + + MACRO t(msgid) BLOCK; + I18N.t(msgid); + END; + + MACRO tx(msgid, vars) BLOCK; + I18N.tx(msgid, vars); + END; + + MACRO tn(msgid, msgid_plural, count) BLOCK; + I18N.tn(msgid, msgid_plural, count); + END; + + MACRO tnx(msgid, msgid_plural, count, vars) BLOCK; + I18N.tnx(msgid, msgid_plural, count, vars); + END; + + MACRO txn(msgid, msgid_plural, count, vars) BLOCK; + I18N.txn(msgid, msgid_plural, count, vars); + END; + + MACRO tp(msgctxt, msgid) BLOCK; + I18N.tp(msgctxt, msgid); + END; + + MACRO tpx(msgctxt, msgid, vars) BLOCK; + I18N.tpx(msgctxt, msgid, vars); + END; + + MACRO tnp(msgctxt, msgid, msgid_plural, count) BLOCK; + I18N.tnp(msgctxt, msgid, msgid_plural, count); + END; + + MACRO tnpx(msgctxt, msgid, msgid_plural, count, vars) BLOCK; + I18N.tnpx(msgctxt, msgid, msgid_plural, count, vars); + END; +%] diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/includes/i18n.inc b/koha-tmpl/opac-tmpl/bootstrap/en/includes/i18n.inc new file mode 100644 index 0000000000..4b5da9231b --- /dev/null +++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/i18n.inc @@ -0,0 +1,39 @@ +[% + USE I18N; + + MACRO t(msgid) BLOCK; + I18N.t(msgid); + END; + + MACRO tx(msgid, vars) BLOCK; + I18N.tx(msgid, vars); + END; + + MACRO tn(msgid, msgid_plural, count) BLOCK; + I18N.tn(msgid, msgid_plural, count); + END; + + MACRO tnx(msgid, msgid_plural, count, vars) BLOCK; + I18N.tnx(msgid, msgid_plural, count, vars); + END; + + MACRO txn(msgid, msgid_plural, count, vars) BLOCK; + I18N.txn(msgid, msgid_plural, count, vars); + END; + + MACRO tp(msgctxt, msgid) BLOCK; + I18N.tp(msgctxt, msgid); + END; + + MACRO tpx(msgctxt, msgid, vars) BLOCK; + I18N.tpx(msgctxt, msgid, vars); + END; + + MACRO tnp(msgctxt, msgid, msgid_plural, count) BLOCK; + I18N.tnp(msgctxt, msgid, msgid_plural, count); + END; + + MACRO tnpx(msgctxt, msgid, msgid_plural, count, vars) BLOCK; + I18N.tnpx(msgctxt, msgid, msgid_plural, count, vars); + END; +%] diff --git a/misc/translator/LangInstaller.pm b/misc/translator/LangInstaller.pm index 758f126dab..a5a492bffe 100644 --- a/misc/translator/LangInstaller.pm +++ b/misc/translator/LangInstaller.pm @@ -25,7 +25,13 @@ use C4::Context; use YAML::Syck qw( Dump LoadFile ); use Locale::PO; use FindBin qw( $Bin ); +use File::Basename; +use File::Find; use File::Path qw( make_path ); +use File::Slurp; +use File::Temp qw( tempdir ); +use Template::Parser; +use PPI; $YAML::Syck::ImplicitTyping = 1; @@ -507,6 +513,68 @@ sub update_messages { system "$self->{msgmerge} -U $pofile $self->{domain}.pot"; } +sub extract_messages_from_templates { + my ($self, $tempdir, @files) = @_; + + my $intranetdir = $self->{context}->config('intranetdir'); + my @keywords = qw(t tx tn txn tnx tp tpx tnp tnpx); + my $parser = Template::Parser->new(); + + foreach my $file (@files) { + my $template = read_file("$intranetdir/$file"); + my $data = $parser->parse($template); + my $document = PPI::Document->new(\$data->{BLOCK}); + + # [% t('foo') %] is compiled to $output .= $stash->get(['t', ['foo']]); + # We try to find all nodes corresponding to keyword (here 't') + my $nodes = $document->find(sub { + my ($topnode, $element) = @_; + + # Filter out non-valid keywords + return 0 unless ($element->isa('PPI::Token::Quote::Single')); + return 0 unless (grep {$element->content eq qq{'$_'}} @keywords); + + # keyword (e.g. 't') should be the first element of the arrayref + # passed to $stash->get() + return 0 if $element->sprevious_sibling; + + return 0 unless $element->snext_sibling + && $element->snext_sibling->snext_sibling + && $element->snext_sibling->snext_sibling->isa('PPI::Structure::Constructor'); + + # Check that it's indeed a call to $stash->get() + my $statement = $element->statement->parent->statement->parent->statement; + return 0 unless grep { $_->isa('PPI::Token::Symbol') && $_->content eq '$stash' } $statement->children; + return 0 unless grep { $_->isa('PPI::Token::Operator') && $_->content eq '->' } $statement->children; + return 0 unless grep { $_->isa('PPI::Token::Word') && $_->content eq 'get' } $statement->children; + + return 1; + }); + + next unless $nodes; + + # Write the Perl equivalent of calls to t* functions family, so xgettext + # can extract the strings correctly + make_path(dirname("$tempdir/$file")); + open my $fh, '>', "$tempdir/$file"; + + foreach my $node (@$nodes) { + my @args = map { + $_->significant && !$_->isa('PPI::Token::Operator') ? $_->content : () + } $node->snext_sibling->snext_sibling->find_first('PPI::Statement')->children; + + my $keyword = $node->content; + $keyword =~ s/^'t(.*)'$/__$1/; + + say $fh "$keyword(" . join(', ', @args) . ");"; + } + + close $fh; + } + + return $tempdir; +} + sub extract_messages { my $self = shift; @@ -529,11 +597,24 @@ sub extract_messages { } } + my @tt_files; + find(sub { + if ($File::Find::dir =~ m|/en/| && $_ =~ m/\.(tt|inc)$/) { + my $filename = $File::Find::name; + $filename =~ s|^$intranetdir/||; + push @tt_files, $filename; + } + }, "$intranetdir/koha-tmpl"); + + my $tempdir = tempdir('Koha-translate-XXXX', TMPDIR => 1, CLEANUP => 1); + $self->extract_messages_from_templates($tempdir, @tt_files); + push @files_to_scan, @tt_files; + my $xgettext_cmd = "$self->{xgettext} -L Perl --from-code=UTF-8 " . "-k -k__ -k__x -k__n:1,2 -k__nx:1,2 -k__xn:1,2 -k__p:1c,2 " . "-k__px:1c,2 -k__np:1c,2,3 -k__npx:1c,2,3 -kN__ -kN__n:1,2 " . "-kN__p:1c,2 -kN__np:1c,2,3 " - . "-o $Bin/$self->{domain}.pot -D $intranetdir"; + . "-o $Bin/$self->{domain}.pot -D $tempdir -D $intranetdir"; $xgettext_cmd .= " $_" foreach (@files_to_scan); if (system($xgettext_cmd) != 0) { -- 2.14.2