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',
# along with Koha; if not, see <http://www.gnu.org/licenses>.
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;
--- /dev/null
+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 <http://www.gnu.org/licenses>.
+
+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;
--- /dev/null
+[%
+ 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;
+%]
--- /dev/null
+[%
+ 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;
+%]
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;
$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};
}
}
+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 = ('.');
}
}
- 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 {
return unless $self->{lang};
$self->install_tmpl($files) unless $self->{pref_only};
$self->install_prefs();
+ $self->install_messages();
+ $self->remove_pot();
}
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();
}
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();
}
--- /dev/null
+#!/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]);
+}