From b0c987e0ab9d3840464c2908f0fb89d5857a6864 Mon Sep 17 00:00:00 2001 From: Julian Maurice Date: Tue, 15 Dec 2015 02:31:03 +0100 Subject: [PATCH] Bug 15395: Allow correct handling of plural translation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Locale::Maketext does not allow correct handling of plural translation for languages that have more than one plural forms. Locale::Messages does. So Koha::I18N is now a wrapper around Locale::Messages, just like Locale::TextDomain, and export the same symbols as Locale::TextDomain. You can refer to documentation of Locale::TextDomain to know how to use exported subroutines. Example usage: __("Hi") __x("Hi {name}", name => 'Bob') __n("item", "items", $num_items) __nx("one item", "{count} items", $num_items, count => $num_items) __p("Bibliographic record", "item") This patch also brings Koha::I18N power to Template::Toolkit templates by adding a TT plugin. This plugin can be used 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. Remove dependency to Locale::Maketext and Locale::Maketext::Lexicon Add dependency to Locale::Messages and PPI Test plan for translation in Perl code: 1. Open a .pl script or .pm module with your favorite text editor 2. Add 'use Koha::I18N;' in the beginning of file 3. Use one of the subroutines exported by Koha::I18N and be sure to have a way to visualize the result (pass result to the template for example, or simply warn and watch the log file) 4. cd misc/translator && ./translate update fr-FR # try other languages 5. Open misc/translator/po/fr-FR-messages.po and translate your string(s) You may need to change the "Plural-Forms" header. See https://localization-guide.readthedocs.org/en/latest/l10n/pluralforms.html 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 8. prove t/Koha/I18N.t Test plan for translation in templates: 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-messages.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 Signed-off-by: Jonathan Druart --- C4/Installer/PerlDependencies.pm | 12 +- Koha/I18N.pm | 191 +++++++++++++++++--- 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 | 201 ++++++++++++++++++--- t/Koha/I18N.t | 62 +++++++ t/Koha/I18N/po/xx_XX/LC_MESSAGES/Koha.mo | Bin 0 -> 1370 bytes 8 files changed, 557 insertions(+), 59 deletions(-) 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 create mode 100755 t/Koha/I18N.t create mode 100644 t/Koha/I18N/po/xx_XX/LC_MESSAGES/Koha.mo diff --git a/C4/Installer/PerlDependencies.pm b/C4/Installer/PerlDependencies.pm index bf8599162b..ab37c150df 100644 --- a/C4/Installer/PerlDependencies.pm +++ b/C4/Installer/PerlDependencies.pm @@ -727,15 +727,15 @@ our $PERL_DEPS = { required => 1, min_ver => '2.125', }, - 'Locale::Maketext' => { + 'Locale::Messages' => { 'usage' => 'Core', 'required' => '1', - 'min_ver' => '1.19', + 'min_ver' => '1.20', }, - 'Locale::Maketext::Lexicon' => { - 'usage' => 'Core', - 'required' => '1', - 'min_ver' => '0.91', + 'PPI' => { + 'usage' => 'I18N', + 'required' => '0', + 'min_ver' => '1.215', }, 'LWP::Protocol::https' => { 'usage' => 'OverDrive integration', diff --git a/Koha/I18N.pm b/Koha/I18N.pm index 33730f99b8..44c47132ff 100644 --- a/Koha/I18N.pm +++ b/Koha/I18N.pm @@ -18,41 +18,186 @@ package Koha::I18N; # along with Koha; if not, see . use Modern::Perl; -use base qw(Locale::Maketext Exporter); use CGI; use C4::Languages; +use C4::Context; -use Locale::Maketext::Lexicon { - 'en' => ['Auto'], - '*' => [ - Gettext => - C4::Context->config('intranetdir') - . '/misc/translator/po/*-messages.po' - ], - '_AUTO' => 1, - '_style' => 'gettext', -}; +use Encode; +use Locale::Messages qw(:locale_h nl_putenv setlocale LC_MESSAGES); +use Koha::Cache::Memory::Lite; -our @EXPORT = qw( gettext ); +use parent 'Exporter'; +our @EXPORT = qw( + __ + __x + __n + __nx + __xn + __p + __px + __np + __npx + N__ + N__n + N__p + N__np +); -my %language_handles; +our $textdomain = 'Koha'; -sub get_language_handle { - my $cgi = new CGI; - my $language = C4::Languages::getlanguage; +sub init { + my $cache = Koha::Cache::Memory::Lite->get_instance(); + my $cache_key = 'i18n:initialized'; + unless ($cache->get_from_cache($cache_key)) { + my @system_locales = grep { chomp; not (/^C/ || $_ eq 'POSIX') } qx/locale -a/; + if (@system_locales) { + # LANG needs to be set to a valid locale, + # otherwise LANGUAGE is ignored + nl_putenv('LANG=' . $system_locales[0]); + setlocale(LC_MESSAGES, ''); - if (not exists $language_handles{$language}) { - $language_handles{$language} = __PACKAGE__->get_handle($language) - or die "No language handle for '$language'"; + my $langtag = C4::Languages::getlanguage; + my @subtags = split /-/, $langtag; + my ($language, $region) = @subtags; + if ($region && length $region == 4) { + $region = $subtags[2]; + } + my $locale = $language; + if ($region) { + $locale .= '_' . $region; + } + + nl_putenv("LANGUAGE=$locale"); + nl_putenv('OUTPUT_CHARSET=UTF-8'); + + my $directory = _base_directory(); + textdomain($textdomain); + bindtextdomain($textdomain, $directory); + } else { + warn "No locale installed. Localization cannot work and is therefore disabled"; + } + + $cache->set_in_cache($cache_key, 1); + } +} + +sub __ { + my ($msgid) = @_; + + $msgid = Encode::encode_utf8($msgid); + + return _gettext(\&gettext, [ $msgid ]); +} + +sub __x { + my ($msgid, %vars) = @_; + + $msgid = Encode::encode_utf8($msgid); + + return _gettext(\&gettext, [ $msgid ], %vars); +} + +sub __n { + my ($msgid, $msgid_plural, $count) = @_; + + $msgid = Encode::encode_utf8($msgid); + $msgid_plural = Encode::encode_utf8($msgid_plural); + + return _gettext(\&ngettext, [ $msgid, $msgid_plural, $count ]); +} + +sub __nx { + my ($msgid, $msgid_plural, $count, %vars) = @_; + + $msgid = Encode::encode_utf8($msgid); + $msgid_plural = Encode::encode_utf8($msgid_plural); + + return _gettext(\&ngettext, [ $msgid, $msgid_plural, $count ], %vars); +} + +sub __xn { + return __nx(@_); +} + +sub __p { + my ($msgctxt, $msgid) = @_; + + $msgctxt = Encode::encode_utf8($msgctxt); + $msgid = Encode::encode_utf8($msgid); + + return _gettext(\&pgettext, [ $msgctxt, $msgid ]); +} + +sub __px { + my ($msgctxt, $msgid, %vars) = @_; + + $msgctxt = Encode::encode_utf8($msgctxt); + $msgid = Encode::encode_utf8($msgid); + + return _gettext(\&pgettext, [ $msgctxt, $msgid ], %vars); +} + +sub __np { + my ($msgctxt, $msgid, $msgid_plural, $count) = @_; + + $msgctxt = Encode::encode_utf8($msgctxt); + $msgid = Encode::encode_utf8($msgid); + $msgid_plural = Encode::encode_utf8($msgid_plural); + + return _gettext(\&npgettext, [ $msgctxt, $msgid, $msgid_plural, $count ]); +} + +sub __npx { + my ($msgctxt, $msgid, $msgid_plural, $count, %vars) = @_; + + $msgctxt = Encode::encode_utf8($msgctxt); + $msgid = Encode::encode_utf8($msgid); + $msgid_plural = Encode::encode_utf8($msgid_plural); + + return _gettext(\&npgettext, [ $msgctxt, $msgid, $msgid_plural, $count], %vars); +} + +sub N__ { + return @_; +} + +sub N__n { + return @_; +} + +sub N__p { + return @_; +} + +sub N__np { + return @_; +} + +sub _base_directory { + return C4::Context->config('intranetdir') . '/misc/translator/po'; +} + +sub _gettext { + my ($sub, $args, %vars) = @_; + + init(); + + my $text = Encode::decode_utf8($sub->(@$args)); + if (%vars) { + $text = _expand($text, %vars); } - return $language_handles{$language}; + return $text; } -sub gettext { - my $lh = get_language_handle; - $lh->maketext(@_); +sub _expand { + my ($text, %vars) = @_; + + my $re = join '|', map { quotemeta $_ } keys %vars; + $text =~ s/\{($re)\}/defined $vars{$1} ? $vars{$1} : "{$1}"/ge; + + return $text; } 1; 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 ff42dcf779..7a621c19ba 100644 --- a/misc/translator/LangInstaller.pm +++ b/misc/translator/LangInstaller.pm @@ -25,6 +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; @@ -66,13 +73,17 @@ sub new { $self->{process} = "$Bin/tmpl_process3.pl " . ($verbose ? '' : '-q'); $self->{path_po} = "$Bin/po"; $self->{po} = { '' => $default_pref_po_header }; - $self->{domain} = 'messages'; + $self->{domain} = 'Koha'; $self->{cp} = `which cp`; $self->{msgmerge} = `which msgmerge`; + $self->{msgfmt} = `which msgfmt`; + $self->{msginit} = `which msginit`; $self->{xgettext} = `which xgettext`; $self->{sed} = `which sed`; chomp $self->{cp}; chomp $self->{msgmerge}; + chomp $self->{msgfmt}; + chomp $self->{msginit}; chomp $self->{xgettext}; chomp $self->{sed}; @@ -456,30 +467,134 @@ sub create_tmpl { } } +sub locale_name { + my $self = shift; + + my ($language, $region, $country) = split /-/, $self->{lang}; + $country //= $region; + my $locale = $language; + if ($country && length($country) == 2) { + $locale .= '_' . $country; + } + + return $locale; +} + sub create_messages { my $self = shift; - print "Create messages ($self->{lang})\n" if $self->{verbose}; - system - "$self->{cp} $self->{domain}.pot " . - "$self->{path_po}/$self->{lang}-$self->{domain}.po"; + my $pot = "$self->{domain}.pot"; + my $po = "$self->{path_po}/$self->{lang}-messages.po"; + + unless ( -f $pot ) { + $self->extract_messages(); + } + + say "Create messages ($self->{lang})" if $self->{verbose}; + my $locale = $self->locale_name(); + system "$self->{msginit} -i $pot -o $po -l $locale --no-translator"; + + # If msginit failed to correctly set Plural-Forms, set a default one + system "$self->{sed} --in-place $po " + . "--expression='s/Plural-Forms: nplurals=INTEGER; plural=EXPRESSION/Plural-Forms: nplurals=2; plural=(n != 1)/'"; } sub update_messages { my $self = shift; - my $pofile = "$self->{path_po}/$self->{lang}-$self->{domain}.po"; - print "Update messages ($self->{lang})\n" if $self->{verbose}; - if ( not -f $pofile ) { - print "File $pofile does not exist\n" if $self->{verbose}; + my $pot = "$self->{domain}.pot"; + my $po = "$self->{path_po}/$self->{lang}-messages.po"; + + unless ( -f $pot ) { + $self->extract_messages(); + } + + if ( -f $po ) { + say "Update messages ($self->{lang})" if $self->{verbose}; + system "$self->{msgmerge} -U $po $pot"; + } else { $self->create_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) { + say "Extract messages from $file" if $self->{verbose}; + my $template = read_file("$intranetdir/$file"); + my $data = $parser->parse($template); + unless ($data) { + warn "Error at $file : " . $parser->error(); + next; + } + + make_path(dirname("$tempdir/$file")); + open my $fh, '>', "$tempdir/$file"; + + my @blocks = ($data->{BLOCK}, values %{ $data->{DEFBLOCKS} }); + foreach my $block (@blocks) { + my $document = PPI::Document->new(\$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 + 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; + say "Extract messages into POT file" if $self->{verbose}; + my $intranetdir = $self->{context}->config('intranetdir'); my @files_to_scan; my @directories_to_scan = ('.'); @@ -499,26 +614,53 @@ sub extract_messages { } } - my $xgettext_cmd = "$self->{xgettext} -L Perl --from-code=UTF-8 " . - "-o $Bin/$self->{domain}.pot -D $intranetdir"; + 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 " + . "--package-name=Koha --package-version='' " + . "-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 $tempdir -D $intranetdir"; $xgettext_cmd .= " $_" foreach (@files_to_scan); if (system($xgettext_cmd) != 0) { die "system call failed: $xgettext_cmd"; } - if ( -f "$Bin/$self->{domain}.pot" ) { - my $replace_charset_cmd = "$self->{sed} --in-place " . - "$Bin/$self->{domain}.pot " . - "--expression='s/charset=CHARSET/charset=UTF-8/'"; - if (system($replace_charset_cmd) != 0) { - die "system call failed: $replace_charset_cmd"; - } - } else { - print "No messages found\n" if $self->{verbose}; - return; + my $replace_charset_cmd = "$self->{sed} --in-place " . + "$Bin/$self->{domain}.pot " . + "--expression='s/charset=CHARSET/charset=UTF-8/'"; + if (system($replace_charset_cmd) != 0) { + die "system call failed: $replace_charset_cmd"; } - return 1; +} + +sub install_messages { + my ($self) = @_; + + my $locale = $self->locale_name(); + my $modir = "$self->{path_po}/$locale/LC_MESSAGES"; + my $pofile = "$self->{path_po}/$self->{lang}-messages.po"; + my $mofile = "$modir/$self->{domain}.mo"; + + if ( not -f $pofile ) { + $self->create_messages(); + } + say "Install messages ($locale)" if $self->{verbose}; + make_path($modir); + system "$self->{msgfmt} -o $mofile $pofile"; } sub remove_pot { @@ -532,6 +674,8 @@ sub install { return unless $self->{lang}; $self->install_tmpl($files) unless $self->{pref_only}; $self->install_prefs(); + $self->install_messages(); + $self->remove_pot(); } @@ -547,14 +691,13 @@ sub get_all_langs { sub update { my ($self, $files) = @_; my @langs = $self->{lang} ? ($self->{lang}) : $self->get_all_langs(); - my $extract_ok = $self->extract_messages(); for my $lang ( @langs ) { $self->set_lang( $lang ); $self->update_tmpl($files) unless $self->{pref_only}; $self->update_prefs(); - $self->update_messages() if $extract_ok; + $self->update_messages(); } - $self->remove_pot() if $extract_ok; + $self->remove_pot(); } @@ -563,10 +706,8 @@ sub create { return unless $self->{lang}; $self->create_tmpl($files) unless $self->{pref_only}; $self->create_prefs(); - if ($self->extract_messages()) { - $self->create_messages(); - $self->remove_pot(); - } + $self->create_messages(); + $self->remove_pot(); } diff --git a/t/Koha/I18N.t b/t/Koha/I18N.t new file mode 100755 index 0000000000..6524e66e80 --- /dev/null +++ b/t/Koha/I18N.t @@ -0,0 +1,62 @@ +#!/usr/bin/perl + +use Modern::Perl; +use Test::More tests => 35; +use Test::MockModule; +use FindBin qw($Bin); +use Encode; + +BEGIN { + use_ok('Koha::I18N'); +} + +my $koha_i18n = Test::MockModule->new('Koha::I18N'); +$koha_i18n->mock('_base_directory', sub { "$Bin/I18N/po" }); + +my $c4_languages = Test::MockModule->new('C4::Languages'); +$c4_languages->mock('getlanguage', sub { 'xx-XX' }); + +# If you need to modify the MO file to add new tests +# 1) msgunfmt -o Koha.po t/Koha/I18N/po/xx_XX/LC_MESSAGES/Koha.mo +# 2) Edit Koha.po +# 3) msgfmt -o t/Koha/I18N/po/xx_XX/LC_MESSAGES/Koha.mo Koha.po +my @tests = ( + [ __('test') => 'test ツ' ], + [ __x('Hello {name}', name => 'World') => 'Hello World ツ' ], + [ __n('Singular', 'Plural', 0) => 'Zero ツ' ], + [ __n('Singular', 'Plural', 1) => 'Singular ツ' ], + [ __n('Singular', 'Plural', 2) => 'Plural ツ' ], + [ __n('Singular', 'Plural', 3) => 'Plural ツ' ], + [ __nx('one item', '{count} items', 0, count => 0) => 'no item ツ' ], + [ __nx('one item', '{count} items', 1, count => 1) => 'one item ツ' ], + [ __nx('one item', '{count} items', 2, count => 2) => '2 items ツ' ], + [ __nx('one item', '{count} items', 3, count => 3) => '3 items ツ' ], + [ __xn('one item', '{count} items', 0, count => 0) => 'no item ツ' ], + [ __xn('one item', '{count} items', 1, count => 1) => 'one item ツ' ], + [ __xn('one item', '{count} items', 2, count => 2) => '2 items ツ' ], + [ __xn('one item', '{count} items', 3, count => 3) => '3 items ツ' ], + [ __p('biblio', 'title') => 'title (biblio) ツ' ], + [ __p('patron', 'title') => 'title (patron) ツ' ], + [ __px('biblio', 'Remove item {id}', id => 42) => 'Remove item 42 (biblio) ツ' ], + [ __px('list', 'Remove item {id}', id => 42) => 'Remove item 42 (list) ツ' ], + [ __np('ctxt1', 'singular', 'plural', 0) => 'zero (ctxt1) ツ' ], + [ __np('ctxt1', 'singular', 'plural', 1) => 'singular (ctxt1) ツ' ], + [ __np('ctxt1', 'singular', 'plural', 2) => 'plural (ctxt1) ツ' ], + [ __np('ctxt1', 'singular', 'plural', 3) => 'plural (ctxt1) ツ' ], + [ __np('ctxt2', 'singular', 'plural', 0) => 'zero (ctxt2) ツ' ], + [ __np('ctxt2', 'singular', 'plural', 1) => 'singular (ctxt2) ツ' ], + [ __np('ctxt2', 'singular', 'plural', 2) => 'plural (ctxt2) ツ' ], + [ __np('ctxt2', 'singular', 'plural', 3) => 'plural (ctxt2) ツ' ], + [ __npx('biblio', 'one item', '{count} items', 0, count => 0) => 'no item (biblio) ツ' ], + [ __npx('biblio', 'one item', '{count} items', 1, count => 1) => 'one item (biblio) ツ' ], + [ __npx('biblio', 'one item', '{count} items', 2, count => 2) => '2 items (biblio) ツ' ], + [ __npx('biblio', 'one item', '{count} items', 3, count => 3) => '3 items (biblio) ツ' ], + [ __npx('list', 'one item', '{count} items', 0, count => 0) => 'no item (list) ツ' ], + [ __npx('list', 'one item', '{count} items', 1, count => 1) => 'one item (list) ツ' ], + [ __npx('list', 'one item', '{count} items', 2, count => 2) => '2 items (list) ツ' ], + [ __npx('list', 'one item', '{count} items', 3, count => 3) => '3 items (list) ツ' ], +); + +foreach my $test (@tests) { + is($test->[0], decode_utf8($test->[1]), $test->[1]); +} diff --git a/t/Koha/I18N/po/xx_XX/LC_MESSAGES/Koha.mo b/t/Koha/I18N/po/xx_XX/LC_MESSAGES/Koha.mo new file mode 100644 index 0000000000000000000000000000000000000000..99be22f268b3e872ac10b2d2e3e538b1fd8293d6 GIT binary patch literal 1370 zcmV-g1*Q7l5(Je10000D0000S0001l0000H0002&000000000m0RR9D0000n0RR9G z0000!0RR9O0000^0RR9U0001H0RR9D0001l0RR9M0001y0RR9M0001|0RR9M0002J z0RR9S0002f0RR9N0002*0RR9D000060ssI60000J0ssJm0RR9P0ssII0001-0{{RZ z000230{{RW0002a0{{R;0002&0{{RL0000o1ONay0000*1ONay0001f1ONaV0002D z1ONa(0002f1ONak0000J1pojN0000!1pojD0000{1poj600000000000000D00006 z000030000C0000A000020000B0000000009000000000400005000070000800000 zNM&qnZy?_60DEI^b#8QhAZc`EZF2x(X<}??Zv=E{bZlh+V{~|QF$8mI zZfA9DVR8U)Y;|&BYye|)cyuxZb7^j8b!=gB0B~$|a$#%$Y-w|J1X5*fZ+2xMX>?_6 zAbV+KeE@7}b94l6Ze<{8bY*P-dt+~PZghPhX>?_6a{zB{WguyEWo-a^V{dhCbbTOc zbY*RG0B~V+a&K+~bZK;KWdL+#b94Yua&Ky7V{|P^WGz-@a&u{KZaN@KZ)jl(Qe|*& za&#?Cb7yH}Ekbo?b1hVFIv@&APgE^Ka%Ew3X>V>VL}7GgIv_GIF*hwRG%YbOATcyL zFf%JKGB7X-P){vVWp-(EX>V>VL}7GgIv_GIF*hwRG%YbOATcyLFf%JKGB7X-Oks0$ zEmU%0ZgXs5bZ>GxAWC&?X<=?4O<{F%X=7y|JZg1pX<=?IZDDnCX=7zTVrgP*X<~9^ zE@N+PJ_<}>ZfA92XJsu^WnpbPAb5BnJa~9OY-uiUa%VmYOkr+kbzx^^Iv{v>UszZQ zO-W5fEmmc6b7^mGIv_DFFbYF&ZggdCbS+eQaAi6mbY*ySFK}#OX>L0pV`yP=b7gcr zRa8bTI0{2=ZggdCbS+eJVQzC~WpXV=Zewp`X>MmaAUI-abP7;xb#h^BEk?_6AbV+KeIO`eX<}??Zz&+-gMTbY?_6 zAShyKVr*$|DInv6gaCVEZ*^{TeIRLcWo>gHC}L@1Y-w*PAmf9C0CZ_|Y-J!QVrgP* zX>TbYXLW30av&&Uba-?zDInv6gaB}Cb#h^B zASh#Wcyuu-Amf9C0D5I|Zy+dRba-?!DInv6gaC7CZfA9DVR9fSV{~|QGASVAgM)WbAmf9C0B>$(AZc`EZ6GLYX>)WbAmf9C0DEI^b#8QhAZc`EZF3+fY-w|J zDInv6gaB@DAZc`EZ6M==gaB`DWguyEWo;nigM;M1& literal 0 KcmV+b0RR6000031 -- 2.11.0