#!/usr/local/bin/perl
##----------------------------------------------------------------------------
## PO Files Manipulation - ~/scripts/po.pl
## Version v0.1.1
## Copyright(c) 2021 DEGUEST Pte. Ltd.
## Author: Jacques Deguest <jack@deguest.jp>
## Created 2021/07/24
## Modified 2021/07/30
## All rights reserved
##
## This program is free software; you can redistribute it and/or modify it
## under the same terms as Perl itself.
##----------------------------------------------------------------------------
BEGIN
{
use strict;
use warnings;
# use lib './lib';
use DateTime;
use Getopt::Class;
use IO::File;
use Nice::Try;
use Pod::Usage;
use Text::PO;
use Text::PO::MO;
use Text::Wrap ();
our $PLURALS = {};
our $VERSION = 'v0.1.1';
};
{
our $DEBUG = 0;
our $VERBOSE = 0;
our $LOG_LEVEL = 0;
our $PROG_NAME = 'po';
our $out = IO::File->new;
$out->fdopen( fileno( STDOUT ), 'w' );
$out->binmode( ':utf8' );
$out->autoflush( 1 );
our $err = IO::File->new;
$err->autoflush( 1 );
$err->fdopen( fileno( STDERR ), 'w' );
$err->binmode( ":utf8" );
&_load_plurals();
my $dict =
{
# Actions
as_json => { type => 'boolean' },
as_po => { type => 'boolean' },
add => { type => 'boolean' },
compile => { type => 'boolean' },
dump => { type => 'boolean' },
init => { type => 'boolean' },
sync => { type => 'boolean' },
# Attributes
bugs_to => { type => 'string', class => [qw( init meta )] },
charset => { type => 'string', class => [qw( init meta )], default => 'utf-8' },
created_on => { type => 'datetime', class => [qw( init meta )] },
domain => { type => 'string' },
encoding => { type => 'string', class => [qw( init meta )], default => '8bit' },
header => { type => 'string' },
lang => { type => 'string', alias => [qw( language )], class => [qw( init meta )], re => qr/^[a-z]{2}(?:_[A-Z]{2})?$/ },
msgid => { type => 'string', class => [qw( edit )] },
msgstr => { type => 'string', class => [qw( edit )] },
output => { type => 'string' },
output_dir => { type => 'string' },
overwrite => { type => 'boolean', default => 0 },
po_debug => { type => 'integer', default => 0 },
# Used as a template to create the po file with --init
pot => { type => 'string', class => [qw( init )] },
project => { type => 'string', class => [qw( init meta )] },
revised_on => { type => 'datetime', class => [qw( init meta )] },
team => { type => 'string', class => [qw( init meta )], alias => [qw( language-team )] },
settings => { type => 'string' },
translator => { type => 'string', class => [qw( init meta )] },
tz => { type => 'string', alias => [qw( time_zone timezone )], class => [qw( init meta )] },
version => { type => 'string', class => [qw( init meta )] },
## Generic options
quiet => { type => 'boolean', default => 0 },
debug => { type => 'integer', alias => [qw(d)], default => \$DEBUG },
verbose => { type => 'integer', default => \$VERBOSE },
v => { type => 'code', code => sub{ printf( STDOUT "2f\n", $VERSION ); } },
help => { type => 'code', alias => [qw(?)], code => sub{ pod2usage(1); } },
man => { type => 'code', code => sub{ pod2usage( -exitstatus => 0, -verbose => 2 ); } },
};
our $opt = Getopt::Class->new({ dictionary => $dict }) || die( "Error instantiating Getopt::Class object: ", Getopt::Class->error, "\n" );
$opt->usage( sub{ pod2usage(2) } );
our $opts = $opt->exec || die( "An error occurred executing Getopt::Class: ", $opt->error, "\n" );
## Unless the log level has been set directly with a command line option
unless( $LOG_LEVEL )
{
$LOG_LEVEL = 1 if( $VERBOSE );
$LOG_LEVEL = ( 1 + $DEBUG ) if( $DEBUG );
}
my @errors = ();
my $opt_errors = $opt->configure_errors;
push( @errors, @$opt_errors ) if( $opt_errors->length );
if( $opts->{quiet} )
{
$DEBUG = $VERBOSE = 0;
}
$out->print( @errors ? " not ok\n" : " ok\n" ) if( $LOG_LEVEL );
if( @errors )
{
my $error = join( "\n", map{ "\t* $_" } @errors );
substr( $error, 0, 0, "\n\tThe following arguments are mandatory and missing.\n" );
$out->print( <<EOT ) if( !$opts->{ 'quiet' } );
$error
Please, use option '-h' or '--help' to find out and properly call
this program in interactive mode:
$PROG_NAME -h
EOT
exit(1);
}
if( $opts->{compile} && $opts->{output} )
{
my $f = shift( @ARGV ) || bailout( "No po file to read was provided.\n" );
&compile( in => $f, out => $opts->{output} );
}
elsif( $opts->{init} )
{
my $out = $opts->{output} || shift( @ARGV ) || bailout( "No po file path was specified to initiate.\n" );
&init_po( $out );
}
elsif( $opts->{as_json} && $opts->{output} )
{
my $f = shift( @ARGV ) || bailout( "No po file to read was provided.\n" );
_message( 3, "Reading file \"$f\" and writing to \"$opts->{output}\"." );
&to_json( in => $f, out => $opts->{output} );
}
elsif( $opts->{as_po} && $opts->{output} )
{
my $f = shift( @ARGV ) || bailout( "No (json) po file to read was provided.\n" );
_message( 3, "Reading file \"$f\" and writing to \"$opts->{output}\"." );
&to_po( in => $f, out => $opts->{output} );
}
elsif( $opts->{add} )
{
my $f = shift( @ARGV ) || bailout( "No po file to read was provided.\n" );
&add( in => $f );
}
elsif( $opts->{sync} && $opts->{output} )
{
my $f = shift( @ARGV ) || bailout( "No (json) po file to read was provided.\n" );
_message( 3, "Reading file \"$f\" and writing to \"$opts->{output}\"." );
&sync( in => $f, out => $opts->{output} );
}
else
{
foreach my $f ( @ARGV )
{
$out->print( "Processing file \"$f\"\n" );
my $po = Text::PO->new( debug => $opts->{po_debug} );
# $po->debug( 3 );
$po->parse( $f ) || bailout( $po->error, "\n" );
if( $opts->{dump} )
{
_messagec( 3, "Dumping file <green>$f</>" );
$po->dump( $out );
next;
}
elsif( $opts->{as_json} )
{
my $new = $opt->new_file( $f );
$new->extension( 'po.json' );
&to_json( in => $f, out => $new );
}
elsif( $opts->{compile} && $opts->{output_dir} )
{
my $file = $opt->new_file( $f );
my $parent = $file->parent;
# my $domain = $opts->{domain} ? $opts->{domain} : $file->basename( qr/\.(.*?)$/ );
my $domain = $po->domain || bailout( "Unable to get the domain from the po file \"$f\"\n" );
my $out = $file->join( $parent, "${domain}.mo" );
&compile( in => $f, out => $out );
}
}
}
exit(0);
}
sub add
{
my $p = $opt->_get_args_as_hash( @_ );
my $f = $p->{in} || bailout( "No po file to read was specified.\n" );
$f = $opt->new_file( $f );
if( $f->extension eq 'po' )
{
my $p =
{
debug => $opts->{debug},
};
$p->{domain} = $opts->{domain} if( length( $opts->{domain} ) );
$po = Text::PO->new( %$p ) || bailout( Text::PO->error );
$po->parse( $f ) || bailout( $po->error );
}
elsif( $f->extension eq 'json' )
{
my $p =
{
use_json => 1,
debug => $opts->{debug},
};
$p->{domain} = $opts->{domain} if( length( $opts->{domain} ) );
$po = Text::PO->new( %$p ) || bailout( Text::PO->error );
$po->parse2object( $f ) || bailout( $po->error );
}
else
{
bailout( "Unknown source file \"$f\"" );
}
_messagec( 3, "Adding id \"<green>$opts->{msgid}</>\" -> \"<green>$opts->{msgstr}</>\"" );
$po->add_element(
msgid => "$opts->{msgid}",
msgstr => "$opts->{msgstr}",
) || bailout( $po->error );
_messagec( 3, "Synchronisation back to \"<green>$f</>\"" );
$po->sync( $f ) || bailout( $po->error );
_messagec( 3, "<green>Done.</>" );
return(1);
}
sub bailout
{
$err->print( @_, "\n" );
exit(1);
}
sub compile
{
my $p = $opt->_get_args_as_hash( @_ );
my $f = $p->{in} || bailout( "No po file to read was specified.\n" );
my $o = $p->{out} || bailout( "No mo file to write to was specified.\n" );
$f = $opt->new_file( $f );
my $po;
if( $f->extension() eq 'mo' )
{
&bailout( "The source file \"$f\" is already a mo file. You can simply copy it yourself." );
}
elsif( $f->extension eq 'po' )
{
my $p =
{
debug => $opts->{po_debug},
};
$p->{domain} = $opts->{domain} if( length( $opts->{domain} ) );
$po = Text::PO->new( $p ) || bailout( Text::PO->error );
$po = $po->parse( $f );
bailout( "This does not look like a po file" ) if( !$po->elements->length );
}
elsif( $f->extension eq 'json' )
{
my $p =
{
use_json => 1,
debug => $opts->{po_debug},
};
$p->{domain} = $opts->{domain} if( length( $opts->{domain} ) );
$po = Text::PO->new( %$p ) || bailout( Text::PO->error );
$po->parse2object( $f ) || bailout( $po->error );
}
else
{
bailout( "Unknown source file \"$f\"" );
}
# Exchange a string for a Module::Generic::File object
$o = $opt->new_file( $o );
_message( 3, "Saving data to mo file \"$o\"." );
my $mo = Text::PO::MO->new( $o, debug => $opts->{debug} );
$o->parent->mkpath;
$mo->write( $po ) || bailout( "Unable to write to \"$o\": ", $mo->error, "\n" );
return(1);
}
sub init_po
{
my $out = shift( @_ );
$out = $opt->new_file( $out );
if( $out->exists && !$opts->{overwrite} )
{
bailout( "An output file with the same name \"$out\" already exists. If you want to overwrite it, please use the --overwrite option\n" );
}
if( !$opts->{lang} )
{
bailout( "No language code was specified.\n" );
}
elsif( !$opts->{domain} )
{
bailout( "No domain for the po file was provided." );
}
my $p = {};
my $fields = [qw( bugs_to charset created_on encoding header lang project revised_on team translator tz version )];
my $maps =
{
bugs_to => 'Report-Msgid-Bugs-To',
created_on => 'POT-Creation-Date',
revised_on => 'PO-Revision-Date',
translator => 'Last-Translator',
team => 'Language-Team',
lang => 'Language',
plural => 'Plural-Forms',
content_Type => 'Content-Type',
transfer_encoding => 'Content-Transfer-Encoding',
};
if( $opts->{settings} )
{
my $f = $opt->new_file( $opts->{settings} );
bailout( "Settings json file specified \"$opts->{settings}\" does not exist.\n" ) if( !$f->exists );
try
{
my $data = $f->load;
my $j = JSON->new->utf8->relaxed;
my $json = $j->decode( $data );
# Make sure all fields are normalised
foreach my $k ( keys( %$json ) )
{
( my $k2 = $k ) =~ tr/-/_/;
$json->{ $k2 } = CORE::delete( $json->{ $k } );
}
foreach my $k ( @$fields )
{
# command line options take priority
next if( defined( $opts->{ $k } ) && length( $opts->{ $k } ) );
$opts->{ $k } = $json->{ $k } if( exists( $json->{ $k } ) );
}
}
catch( $e )
{
warn( "An error occurred while trying to decode json data from file \"$opts->{settings}\": $e\n" );
return;
}
}
my $po = Text::PO->new( debug => $opts->{debug} );
if( $opts->{pot} )
{
my $pot = $opt->new_file( $opts->{pot} );
bailout( "The pot file specified \"$pot\" does not exist.\n" ) if( !$pot->exists );
$po->parse( $pot ) ||
bailout( "Error while reading pot file \"$pot\": ", $po->error, "\n" );
}
if( $opts->{header} )
{
local $Text::Wrap::columns = 80;
my $lines = [split( /\n/, $opts->{header} )];
for( my $i = 0; $i < scalar( @$lines ); $i++ )
{
substr( $lines->[$i], 0, 0, '# ' ) unless( substr( $lines->[$i], 0, 1 ) eq '#' );
if( length( $lines->[$i] ) > 80 )
{
my $new = Text::Wrap::wrap( '', '', $lines->[$i] );
my $newLines = [split( /\n/, $new )];
splice( @$lines, $i, 1, @$newLines );
$i += scalar( @$newLines ) - 1;
}
}
$po->header( $lines );
}
my $vers = $opts->{version} ? $opts->{version} : '1.0';
$po->meta( 'Project-Id-Version' => sprintf( '%s %.1f', ( $opts->{project} || 'PROJECT' ), $vers ) );
if( $opts->{charset} )
{
$po->meta( content_type => sprintf( 'text/plain; charset=%s', ( $opts->{charset} || 'utf-8' ) ) );
}
my $plur;
if( exists( $PLURALS->{ $opts->{lang} } ) )
{
$plur = $PLURALS->{ $opts->{lang} };
}
elsif( exists( $PLURALS->{ substr( $opts->{lang}, 0, 2 ) } ) )
{
$plur = $PLURALS->{ substr( $opts->{lang}, 0, 2 ) };
}
else
{
warn( "Unknow language \"$opts->{lang}\" to find out about its plural form\n" );
}
$po->meta( $maps->{plural} => sprintf( 'nplurals=%d; plural=%s;', @$plur ) );
$po->domain( $opts->{domain} ) if( $opts->{domain} );
foreach my $t ( qw( created_on revised_on ) )
{
my $dt;
if( $opts->{ $t } )
{
$dt = $opts->{ $t };
}
else
{
$dt = DateTime->now( time_zone => ( $opts->{tz} || 'local' ) );
}
$po->meta( $maps->{ $t } => $dt->strftime( '%F %T%z' ) );
}
foreach my $k ( @$fields )
{
next unless( length( $opts->{ $k } ) );
if( !exists( $maps->{ $k } ) )
{
# warn( "Field \"$k\" does not exist in our map table. This is a bug.\n" );
next;
}
$po->meta( $maps->{ $k } => $opts->{ $k } );
}
$po->dump if( $opts->{debug} );
my $binmode = ( $opts->{charset} || 'utf-8' );
$binmode = 'utf8' if( lc( $binmode ) eq 'utf-8' );
my $fh = $out->open( '>', { binmode => $binmode } ) || bailout( "Unable to open the output file in write mode: ", $out->error, "\n" );
$fh->autoflush(1);
$po->dump( $fh );
$fh->close;
return(1);
}
sub sync
{
my $p = $opt->_get_args_as_hash( @_ );
my $f = $p->{in} || bailout( "No po file to read was specified.\n" );
my $o = $p->{out} || bailout( "No mo file to write to was specified.\n" );
$f = $opt->new_file( $f );
my $po;
if( $f->extension eq 'po' )
{
my $p =
{
debug => $opts->{debug},
};
$p->{domain} = $opts->{domain} if( length( $opts->{domain} ) );
$po = Text::PO->new( %$p ) || bailout( Text::PO->error );
_messagec( 3, "Reading po file <green>$f</>" );
$po->parse( $f ) || bailout( $po->error );
}
elsif( $f->extension eq 'mo' )
{
my $p =
{
debug => $opts->{debug},
};
$p->{domain} = $opts->{domain} if( length( $opts->{domain} ) );
my $mo = Text::PO::MO->new( $f, $p );
_messagec( 3, "Reading mo file <green>$f</>" );
$po = $mo->as_object;
}
elsif( $f->extension eq 'json' )
{
my $p =
{
use_json => 1,
debug => $opts->{debug},
};
$p->{domain} = $opts->{domain} if( length( $opts->{domain} ) );
$po = Text::PO->new( %$p ) || bailout( Text::PO->error );
_messagec( 3, "Reading json po file <green>$f</>" );
$po->parse2object( $f ) || bailout( $po->error );
}
_messagec( 3, "Synchronising against po file <green>$o</>" );
$po->sync( $o ) || bailout( $po->error );
}
sub to_json
{
my $p = $opt->_get_args_as_hash( @_ );
my $f = $p->{in} || bailout( "No po file to read was specified.\n" );
my $o = $p->{out} || bailout( "No mo file to write to was specified.\n" );
$f = $opt->new_file( $f );
my $po;
if( $f->extension() eq 'json' )
{
&bailout( "The source file \"$f\" is already a json file. You can simply copy it yourself." );
}
elsif( $f->extension eq 'mo' )
{
my $p =
{
debug => $opts->{debug},
};
$p->{domain} = $opts->{domain} if( length( $opts->{domain} ) );
my $mo = Text::PO::MO->new( $f, $p );
$po = $mo->as_object;
}
elsif( $f->extension eq 'po' )
{
my $p =
{
debug => $opts->{debug},
};
$p->{domain} = $opts->{domain} if( length( $opts->{domain} ) );
$po = Text::PO->new( %$p ) || bailout( Text::PO->error );
$po->parse( $f ) || bailout( $po->error );
}
else
{
bailout( "Unknown source file \"$f\"" );
}
my $json = $po->as_json({ pretty => 1, canonical => 1 });
_messagec( 3, "<red>", $po->error, "</>" ) if( !$json );
my $fh;
if( $o eq '-' )
{
$fh = IO::File->new;
$fh->fdopen( fileno( STDOUT ), 'w' );
$fh->binmode( ":utf8" );
$fh->autoflush(1);
}
else
{
_messagec( 3, "<green>", $po->elements->length, "</> elements found." );
_messagec( 3, "Saving as json file to <green>${o}</>" );
$o = $opt->new_file( $o );
$o->parent->mkpath;
$fh = $o->open( '>', { binmode => ':utf8' }) || bailout( "Unable to open output file \"$o\" in write mode: $!\n" );
$fh->autoflush(1);
}
# _message( 3, "Saving json '$json'" );
$fh->print( $json );
$fh->close unless( $o eq '-' );
return(1);
}
sub to_po
{
my $p = $opt->_get_args_as_hash( @_ );
my $f = $p->{in} || bailout( "No po file to read was specified.\n" );
my $o = $p->{out} || bailout( "No mo file to write to was specified.\n" );
$f = $opt->new_file( $f );
my $po;
if( $f->extension() eq 'po' )
{
&bailout( "The source file \"$f\" is already a po file. You can simply copy it yourself." );
}
elsif( $f->extension eq 'mo' )
{
my $p = {};
$p->{domain} = $opts->{domain} if( length( $opts->{domain} ) );
my $mo = Text::PO::MO->new( $f, $p );
$po = $mo->as_object || _messagec( 3, "<red>", $mo->error, "</>" );
}
elsif( $f->extension eq 'json' )
{
my $p =
{
use_json => 1,
debug => $opts->{debug},
};
$p->{domain} = $opts->{domain} if( length( $opts->{domain} ) );
$po = Text::PO->new( %$p ) || bailout( Text::PO->error );
$po->parse2object( $f ) || bailout( $po->error );
}
else
{
bailout( "Unknown source file \"$f\"" );
}
my $fh;
if( $o eq '-' )
{
$fh = IO::File->new;
$fh->fdopen( fileno( STDOUT ), 'w' );
$fh->binmode( ":utf8" );
$fh->autoflush(1);
}
else
{
_messagec( 3, "Saving as json file to <green>${o}</>" );
$o = $opt->new_file( $o );
$o->parent->mkpath;
$fh = $o->open( '>', { binmode => ':utf8' }) || bailout( "Unable to open output file \"$o\" in write mode: $!\n" );
}
$po->dump( $fh );
$fh->close unless( $o eq '-' );
return(1);
}
sub _load_plurals
{
# Ref: <http://www.gnu.org/software/gettext/manual/html_node/Plural-forms.html>
# <https://www.fincher.org/Utilities/CountryLanguageList.shtml>
# <http://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html>
our $PLURALS =
{
# Afrikaans
af => [2, "(n != 1)"],
# Akan
ak => [2, "(n > 1)"],
# Aragonese
an => [2, "(n != 1)"],
# Arabic
ar => [6, "n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5"],
# Assamese
as => [2, "(n != 1)"],
# Aymará
ay => [2, 0],
# Azerbaijani
az => [2, "(n != 1)"],
# Belarusian
be => [2, "(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)"],
# Belarusian
be_BY => [3, "n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2"],
# Bulgarian
bg => [2, "(n != 1)"],
# Bengali
bn => [2, ""],
# Tibetan
bo => [1,0],
# Breton
br => [2, "(n > 1)"],
# Bosnian
bs => [3, "(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)"],
# Catalan
ca => [2, "(n != 1)"],
# Czech
cs_CZ => [3, "plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2"],
# Slavic Bulgarian
cu_BG => [2, "n != 1"],
# Welsh
cy => [4, "(n==1) ? 0 : (n==2) ? 1 : (n != 8 && n != 11) ? 2 : 3"],
# Danish
da_DK => [2, "n != 1"],
de => [2, "n != 1"],
de_DE => [2, "n != 1"],
# Dzongkha
dz => [1,0],
# Greece
el_GR => [2, "n != 1"],
en => [2, "n != 1"],
en_GB => [2, "n != 1"],
en_US => [2, "n != 1"],
# Esperanto
eo => [2, "n != 1"],
es => [2, "n != 1"],
es_ES => [2, "n != 1"],
# Estonian
et_EE => [2, "n != 1"],
# Basque
eu => [2, "(n != 1)"],
# Persian
fa => [2, "(n > 1)"],
# Fulah
ff => [2, "(n != 1)"],
# Finland
fi_FI => [2, "n != 1"],
# Faroese
fo_FO => [2, "n != 1"],
fr => [2, "n>1"],
fr_FR => [2, "n>1"],
# Frisian
fy => [2, "(n != 1)"],
# Irish in UK
ga_GB => [3, "n==1 ? 0 : n==2 ? 1 : 2"],
# Irish in Ireland
ga_IE => [3, "n==1 ? 0 : n==2 ? 1 : 2"],
# Galician
gl => [2, "(n != 1)"],
# Gujarati
gu => [2, "(n != 1)"],
# Hausa
ha => [2, "(n != 1)"],
he_IL => [2, "n != 1"],
# Hindi
hi => [2, "(n != 1)"],
# Croatian
hr_HR => [3, "n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2"],
# Hungarian (Finno-Ugric family)
hu_HU => [2, "n != 1"],
# Armenian
hy => [2, "(n != 1)"],
# Interlingua
ia => [2, "(n != 1)"],
# Bahasa Indonesian
id_ID => [2, "n != 1"],
# Icelandic
is => [2, "(n%10!=1 || n%100==11)"],
it => [2, "n != 1"],
it_IT => [2, "n != 1"],
ja_JP => [1, 0],
# Javanese
jv => [2, "(n != 0)"],
# Kazakh
kk => [2, "(n != 1)"],
# Greenlandic
kl => [2, "(n != 1)"],
# Khmer
km => [1, 0],
# Kannada
kn => [2, "(n != 1)"],
ko_KR => [1, 0],
# Kurdish
ku => [2, "(n != 1)"],
# Cornish
kw => [4, "(n==1) ? 0 : (n==2) ? 1 : (n == 3) ? 2 : 3"],
# Kyrgyz
ky => [2, "(n != 1)"],
# Letzeburgesch
lb => [2, "(n != 1)"],
# Lingala
ln => [2, "(n > 1)"],
# Lao
lo => [1, 0],
# Lithuanian (Baltic family)
lt_LT => [3, "n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2"],
# Latvia
lv_LV => [3, "n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2"],
# Montenegro
me => [3, "n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2"],
# Malagasy
mg => [2, "(n > 1)"],
# Maori
mi => [2, "(n > 1)"],
# Macedonian
mk => [2, "n==1 || n%10==1 ? 0 : 1"],
# Malayalam
ml => [2, "(n != 1)"],
# Mongolian
mn => [2, "(n != 1)"],
# Marathi
mr => [2, "(n != 1)"],
# Malay
ms => [1, 0],
# Maltese
mt => [4, "(n==1 ? 0 : n==0 || ( n%100>1 && n%100<11) ? 1 : (n%100>10 && n%100<20 ) ? 2 : 3)"],
# Burmese
my => [1, 0],
# Norwegian Bokmal
nb => [2, "(n != 1)"],
# Nepali
ne => [2, "(n != 1)"],
nl => [2, "n != 1"],
nl_NL => [2, "n != 1"],
# Norwegian Nynorsk
nn => [2, "(n != 1)"],
# Norwegian
no_NO => [2, "n != 1"],
# Occitan
oc => [2, "(n > 1)"],
# Oriya
or => [2, "(n != 1)"],
# Punjabi
pa => [2, "(n != 1)"],
# Polish
pl_PL => [3, "n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2"],
# Pashto
ps => [2, "(n != 1)"],
# Brazilian Portugese
pt => [2, "n != 1"],
pt_BR => [2, "n>1"],
pt_PT => [2, "n != 1"],
# Romansh
rm => [2, "(n != 1)"],
# Romanian
ro_RO => [3, "n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2"],
# Russian
ru => [3, "n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2"],
ru_RU => [3, "n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2"],
# Kinyarwanda
rw => [2, "(n != 1)"],
# Sindhi
sd => [2, "(n != 1)"],
# Northern Sami
se => [2, "(n != 1)"],
# Sinhala
si => [2, "(n != 1)"],
# Slovak
sk_SK => [3, "plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2"],
# Slovenian
sl_SI => [4, "n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3"],
# Somali
so => [2, "(n != 1)"],
# Albanian
sq => [2, "(n != 1)"],
# Serbian
sr_RS => [3, "n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2"],
# Sundanese
su => [1, 0],
# Sweden
sv => [2, "n != 1"],
sv_SE => [2, "n != 1"],
# Swedish
sw => [2, "(n != 1)"],
# Tamil
ta => [2, "(n != 1)"],
# Telugu
te => [2, "(n != 1)"],
# Tajik
tg => [2, "(n > 1);"],
th_TH => [1, 0],
# Tigrinya
ti => [2, "(n > 1)"],
# Turkmen
tk => [2, "(n != 1)"],
# Turkey
tr_TR => [2, "n != 1"],
# Tatar
tt => [1, 0],
# Uyghur
ug => [1, 0],
# Ukrainian
uk_UA => [3, "n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2"],
# Urdu
ur => [2, "(n != 1)"],
# Uzbek
uz => [2, "(n > 1)"],
# Vietnamese
vi_VN => [1, 0],
# Walloon
wa => [2, "(n > 1)"],
# Wolof
wo => [1, 0],
# Yoruba
yo => [2, "(n != 1)"],
# Chinese
zh => [1, 0],
};
}
sub _message
{
my $required_level;
if( $_[0] =~ /^\d{1,2}$/ )
{
$required_level = shift( @_ );
}
else
{
$required_level = 0;
}
return if( !$LOG_LEVEL || $LOG_LEVEL < $required_level );
my $msg = join( '', map( ref( $_ ) eq 'CODE' ? $_->() : $_, @_ ) );
my $frame = 0;
$frame++ if( (caller(1))[3] =~ /_messagec/ );
my( $pkg, $file, $line ) = caller( $frame );
my $sub = ( caller( $frame + 1 ) )[3];
my $sub2 = substr( $sub, rindex( $sub, '::' ) + 2 );
return( $err->print( "${pkg}::${sub2}() [$line]: $msg\n" ) );
}
sub _messagec
{
my $required_level;
if( $_[0] =~ /^\d{1,2}$/ )
{
$required_level = shift( @_ );
}
else
{
$required_level = 0;
}
return( _message( $required_level, $opt->colour_parse( @_ ) ) );
}
__END__
=encoding utf8
=head1 NAME
po - GNU PO file manager
=head1 SYNOPSIS
po [ --debug|--nodebug, --verbose|--noverbose, -v, --help, --man]
Options
Basic options:
--add Add an msgsid/msgstr entry in the po file
--as-po Write the file as a po file
--as-json Write the po file as json on the STDOUT
--compile Create a machine object file (.mo)
--domain The po file domain
--dump Dump the PO file in a format suitable for a .po file
--init Create an initial po file such as .pot
--bugs-to Sets the value for the meta field C<Report-Msgid-Bugs-To>
--charset Sets the character encoding value in C<Content-Type>
--created-on Sets the value for the meta field C<POT-Creation-Date>
--domain The domain, such as C<com.example.api>
--encoding Sets the value for the meta field C<Content-Transfer-Encoding>
--header The string to be used as the header for the C<.po> file only.
--lang The locale to use, such as en_US
--msgid The C<msgid> to add
--msgstr The localised text to add for the given C<msgid>
--output The output file
--output-dir Output directory
--overwrite Boolean. If true, this will allow overwriting existing file
--po-debug Integer representing the debug value to be passed to L<Text::PO>
--pot The C<.pot> file to be used as a template in conjonction with --init
--project Sets the value for the meta field C<Project-Id-Version>
--revised-on Sets the value for the meta field C<PO-Revision-Date>
--settings The settings json file containing default values
--team Sets the value for the meta field C<Language-Team>
--translator Sets the value for the meta field C<Last-Translator>
--tz, --time-zone, --timezone Sets the time zone to use for the date in C<PO-Revision-Date> and C<POT-Creation-Date>
--version Sets the version to be used in the meta field C<Project-Id-Version>
Standard options:
-h, --help display this help and exit
-v display version information and exit
--debug Enable debug mode
--nodebug Disable debug mode
--help, -? Show this help
--man Show this help as a man page
--verbose Enable verbose mode
--noverbose Disable verbose mode
=head1 VERSION
v0.1.1
=head1 OPTIONS
=head2 --add
Adds an C<msgid> and C<msgstr> pair to the po file
po --add --msgid "Hello!" --msgstr "Salut !" --output fr_FR/LC_MESSAGES/com.example.api.po
=head2 --as-json
Takes a po file and transcode it as a json po file
po --as-json --output fr_FR/LC_MESSAGES/com.example.api.json fr_FR.po
=head2 --as-po
Takes a C<.mo> or C<.json> file and transcode it to a po file
po --as-po --output fr_FR.po ./fr_FR/com.example.api.json
=head2 --dump
Dump the data contained as a GNU PO file to the STDOUT
po --dump /some/file.po >new_file.po
# Maybe?
diff /some/file.po new_file.po
=head2 --output-dir
The output directory. For example to read multiple po file and create their related mo files under a given directory:
po --compile --output-dir ./en_GB/LC_MESSAGES en_GB.*.po
This will read all the po files for language en_GB as selected in write their related mo files under C<./en_GB/LC_MESSAGES>. This directory will be created if it does not exist. The domain will be derived from the po file.
=head2 --help
Print a short help message.
=head2 --debug
Enable debug mode with considerable verbosity
=head2 --nodebug
Disable debug mode.
=head2 --verbose
Enable verbose mode.
=head2 --noverbose
Disable verbose mode.
=head2 --man
Print this help as man page.
=head1 DESCRIPTION
B<This program> takes optional parameters and process GNU PO files.
GNU PO files are localisation or l10n files. They can be used as binary after been compiled, or they can be converted to json using this utility which then can read the json data instead of parsing the po files, making it faster to load.
=head1 EXAMPLE
po [--dump, --debug|--nodebug, --verbose|--noverbose, -v, --help, --man] /some/file.po
=head1 AUTHOR
Jacques Deguest E<lt>F<jack@deguest.jp>E<gt>
=head1 COPYRIGHT
Copyright (c) 2020-2021 DEGUEST Pte. Ltd.
=cut