NAME
Text::PO::Gettext - Object-oriented GNU Gettext-style implementation
SYNOPSIS
use Text::PO::Gettext;
# Basic usage: one domain, one locale
my $po = Text::PO::Gettext->new(
path => '/home/joe/locale', # path to where the list of locales directories are
domain => 'com.example.api',
locale => 'fr_FR', # optional, falls back to environment; _ or - does not matter
# use_json => 1, # if you use JSON exports from Text::PO
# category => 'LC_MESSAGES', # optional, defaults to LC_MESSAGES
) || die( Text::PO::Gettext->error );
# Simple lookup
my $hello = $po->gettext('Hello world');
# Text::PO::String objects stringify to the translated text
say $hello; # "Bonjour le monde"
say $hello->value; # same
say $hello->locale; # "fr_FR"
# Plural form lookup
my $n = 3;
my $apples = $po->ngettext(
'%d apple', # singular
'%d apples', # plural
$n,
);
printf "$apples\n", $n; # formatted with %d, still Text::PO::String
# Typical web service / PSGI pattern
my $msg = $po->gettext('Invalid parameter');
my $body = { error => "$msg" }; # stringification
my $res = [
200,
[
'Content-Type' => 'application/json; charset=utf-8',
'Content-Language' => $msg->locale, # <- effective locale
],
[ encode_json($body) ],
];
# Plural forms
my $count = 3;
my $files = $po->ngettext('%d file', '%d files', $count);
printf "$files\n", $count; # "3 fichiers"
# Per-object locale: two locales loaded simultaneously
my $fr = Text::PO::Gettext->new(
domain => 'com.example.api',
locale => 'fr_FR',
path => '/some/where/locale',
);
my $en = Text::PO::Gettext->new(
domain => 'com.example.api',
locale => 'en_GB',
path => '/some/where/locale',
);
my $msg_fr = $fr->gettext( 'Welcome' );
my $msg_en = $en->gettext( 'Welcome' );
say "FR: $msg_fr"; # "FR: Bienvenue"
say "EN: $msg_en"; # "EN: Welcome"
# Using Text::PO::String to set Content-Language in a web service
my $title = $po->gettext( 'Page title' );
$res->header( 'Content-Language' => $title->locale );
$res->body( $title->value );
# Switching locale on an existing object
$po->locale( 'fr_FR' ); # internally normalised
my $bye = $po->gettext( 'Goodbye' );
# JSON-based catalogues instead of PO/MO
my $json_po = Text::PO::Gettext->new(
domain => 'com.example.api',
locale => 'ja_JP',
path => '/var/www/locale',
use_json => 1,
);
my $label = $json_po->gettext( 'Log in' );
VERSION
v0.3.2
DESCRIPTION
This module provides an object-oriented interface to gettext-style localisation data stored as .po, .mo, or .json files.
The conventional way to use GNU gettext is to set the global environment variable LANGUAGE (not LANG by the way. GNU gettext only uses LANGUAGE), then set the "setlocale" in POSIX to the language such as:
use Locale::gettext;
use POSIX ();
POSIX::setlocale( &POSIX::LC_ALL, 'ja_JP' );
my $d = Locale::gettext->domain( 'com.example.api' );
And then in your application, you would write a statement like:
print $d->get( 'Hello!' );
Or possibly using direct access to the C function:
use Locale::gettext;
use POSIX ();
POSIX::setlocale( &POSIX::LC_ALL, 'ja_JP' );
textdomain( 'com.example.api' );
And then:
print gettext( 'Hello!' );
See Locale::gettext for more on this.
This works fine, but has the inconvenience that it uses the global LANGUAGE environment variable and makes it less than subpar as to the necessary flexibility when using multiple domains and flipping back and forth among locales.
Thus comes a more straightforward object-oriented interface offered by this module.
So, Text::PO::Gettext allows you to create multiple independent localisation objects, each with its own domain, locale, and data directory. This makes it straightforward to:
serve multiple users with different locales in the same process,
switch locale per request without touching global environment variables, and
work comfortably under threaded environments, mod_perl, daemons, etc.
A typical directory layout might look like:
/some/where/locale/
en_GB/
LC_MESSAGES/
com.example.api.po
com.example.api.mo
com.example.api.json
ja_JP/
LC_MESSAGES/
com.example.api.po
com.example.api.json
Based on the options you pass (domain, locale, path, use_json), the module will look for a suitable localisation file in the order:
JSON file (
.json) ifuse_jsonis true and such a file exists,PO file (
.po) if present,MO file (
.mo) as a fallback.
Thus, you instantiate an object, passing the domain, the locale and the filesystem path where the locale data resides.
my $po = Text::PO::Gettext->new(
domain => 'com.example.api',
locale => 'ja_JP',
path => '/some/where/locale'
);
print $po->gettext( 'Hello!' );
This will load into memory the locale data whether they are stored as .po, .mo or even .json file, thus making calls to "gettext" super fast since they are in memory.
More than one locale can be loaded, each with its own Text::PO::Gettext object
This distribution comes with its Javascript library equivalent. See the share folder alone with its own test units.
Also, there is a script in scripts that can be used to transcode .po or .mo files into json format and vice versa.
Still, it is better to convert the original .po files to json using the po.pl utility that comes in this Text::PO distribution since it would allow the standalone JavaScript library to read json-based po files. For example:
./po.pl --as-json --output /home/joe/www/locale/ja_JP/LC_MESSAGES/com.example.api.json ./ja_JP.po
This api supports locale that use hyphens or underscore in them such as en-GB or en_GB. You can use either, it will be converted internally.
All translated strings returned by this module are instances of Text::PO::String. Text::PO::String objects are tiny wrappers around the translated value. They:
Stringify transparently to the translated text.
Provide a
localemethod returning the effective locale used for that lookup (for exampleen_GBorfr_FR).Provide a
valuemethod returning the translated text as a plain scalar.
This is particularly handy for web services and APIs, where you may want to inspect the locale actually used for a given message and, for example, set the Content-Language response header accordingly:
my $msg = $po->gettext( 'Error: invalid input' );
$res->header( 'Content-Language' => $msg->locale );
$res->body( $msg->value );
You can safely treat Text::PO::String objects as regular strings in most contexts; they are designed to be drop-in replacements for plain scalars from the caller’s point of view, while still carrying useful metadata.
CONSTRUCTOR
new
my $po = Text::PO::Gettext->new(
domain => 'com.example.api',
locale => 'ja_JP',
path => '/some/where/locale',
category => 'LC_MESSAGES', # optional
use_json => 1, # optional
plural => [ $n, $expr ], # optional
debug => 1, # optional
);
Creates a new Text::PO::Gettext object.
Takes the following options and returns a Gettext object.
categoryThis is optional.
If category is defined, such as
LC_MESSAGES(by default), it will be used when building the path.Other possible category values are:
LC_CTYPE,LC_NUMERIC,LC_TIME,LC_COLLATE,LC_MONETARYSee GNU documentation for more information and "LOCALE CATEGORIES" in perllocale
On the web, using the path is questionable.
See the GNU documentation for more information on this.
domainThis is required.
The portable object domain, such as
com.example.apiThis typically corresponds to the base filename of your PO/MO/JSON files.
localeThis is required.
The locale, such as
ja_JP, oren, or it could even contain a hyphen instead of an underscore, such asen-GB. Internally, though, this will be converted to underscore.pathThis is required.
Base filesystem path where the localisation files are stored.
This is used to form a path along with the locale string. For example, with a locale of
ja_JPand a domain ofcom/example.api, if the path were/locale, the data po json data would be fetched from/locale/ ja_JP/LC_MESSAGES/com.example.api.jsonpluralThis is optional.
An array reference
[ $n, $expr ]defining plural forms for the current domain and locale. This is normally derived from the PO metadata (Plural-Forms) but can be overridden if needed.use_jsonOptional boolean value.
If true, PO data will be loaded from JSON files produced by Text::PO instead of regular PO/MO files.
This is particularly useful when working with the JavaScript companion library provided by the Text::PO distribution.
The constructor does not immediately load the catalogue; data is pulled in when you first call "textdomain" or any of the lookup methods, which internally ensure the domain has been loaded.
Returns the newly created Text::PO::Gettext object or upon error, it sets an error object, and returns undef in scalar context, or an empty list in list context.
METHODS
addItem
This takes a locale, a message id and its localised version and it will add this to the current dictionary for the current domain.
$po->addItem( 'ja_JP', 'Hello!' => "今日は!" );
category
The category to use. This defaults to LC_MESSAGES, but if you prefer you can nix its use by making it undefined, or empty:
my $po = Text::PO::Gettext->new(
category => '',
domain => 'com.example.api',
locale => 'ja_JP',
path => '/some/where/locale'
);
# Setting category to empty string will have the module get the po data
# under C</some/where/locale/ja_JP/com.example.api.json> for example.
print $po->gettext( 'Hello!' );
charset
Returns a string containing the value of the charset encoding as defined in the Content-Type header.
$po->charset()
contentEncoding
Returns a string containing the value of the header Content-Encoding.
$po->contentEncoding();
contentType
Returns a string containing the value of the header Content-Type.
$po->contentType(); # text/plain; charset=utf-8
currentLang
Return the current globally used locale. This is the value found in environment variables LANGUAGE or LANG. Note that GNU gettext only recognises LANGUAGE
and thus, this is different from the locale set in the Gettext class object using </setLocale> or upon class object instantiation.
dgettext
This is like "gettext", but takes a specific domain and a message ID and returns the equivalent localised string if any, otherwise the original message id.
$po->dgettext( 'com.example.auth', 'Please enter your e-mail address' );
# Assuming the locale currently set is ja_JP, this would return:
# 電子メールアドレスをご入力下さい。
dngettext
Same as "ngettext", but takes also a domain as first argument. For example:
$po->ngettext( 'com.example.auth', '%d comment awaiting moderation', '%d comments awaiting moderation', 12 );
# Assuming the locale is ru_RU, this would return:
# %d комментариев ожидают проверки
Note that as of version v0.5.0, this returns a Text::PO::String, which is lightweight and stringifies automatically. It provides the benefit of tagging the string with the locale attached to it.
Thus, in the example above, the resulting Text::PO::String would have its method locale value set to ru_RU, and you could do:
my $localised = $po->ngettext( 'com.example.auth', '%d comment awaiting moderation', '%d comments awaiting moderation', 12 );
say "Locale for this string is: ", $localised->locale;
If no locale string was found, locale would be undefined.
domain
Sets or gets the domain.
$po->domain( 'com.example.api' );
By doing so, this will call "textdomain" and load the associated data from file, if any are found.
exists
Provided with a locale, and this returns true if the locale exists in the current domain, or false otherwise.
fetchLocale
Given an original string (msgid), this returns an array of <span> html element each for one language and its related localised content. For example:
my $array = $po->fetchLocale( "Hello!" );
# Returns:
<span lang="de-DE">Grüß Gott!</span>
<span lang="fr-FR">Salut !</span>
<span lang="ja-JP">今日は!</span>
<span lang="ko-KR">안녕하세요!</span>
This is designed to be added to the html, and based on lang attribute of the html tag, and using the following css trick, this will automatically display the right localised data:
[lang=de-DE] [lang=en-GB],
[lang=de-DE] [lang=fr-FR],
[lang=de-DE] [lang=ja-JP],
[lang=de-DE] [lang=ko-KR],
[lang=en-GB] [lang=de-DE],
[lang=en-GB] [lang=fr-FR],
[lang=en-GB] [lang=ja-JP],
[lang=en-GB] [lang=ko-KR],
[lang=fr-FR] [lang=de-DE],
[lang=fr-FR] [lang=en-GB],
[lang=fr-FR] [lang=ja-JP],
[lang=fr-FR] [lang=ko-KR],
[lang=ja-JP] [lang=de-DE],
[lang=ja-JP] [lang=en-GB]
[lang=ja-JP] [lang=fr-FR],
[lang=ja-JP] [lang=ko-KR]
{
display: none !important;
visibility: hidden !important;
}
getDataPath
This takes no argument and will check for the environment variables TEXTDOMAINDIR. If found, it will use this in lieu of the path option used during object instantiation.
It returns the value found. This is just a helper method and does not affect the value of the path property set during object instantiation.
getDaysLong
Returns an array reference containing the 7 days of the week in their long representation.
my $ref = $po->getDaysLong();
# Assuming the locale is fr_FR, this would yield
print $ref->[0], "\n"; # dim.
getDaysShort
Returns an array reference containing the 7 days of the week in their short representation.
my $ref = $po->getDaysShort();
# Assuming the locale is fr_FR, this would yield
print $ref->[0], "\n"; # dimanche
getDomainHash
This takes an optional hash of parameters and return the global hash dictionary used by this class to store the localised data.
# Will use the default domain as set in po.domain
my $data = $po->getDomainHash();
# Explicitly specify another domain
my $data = $po->getDomainHash( domain => "net.example.api" );
# Specify a domain and a locale
my $po = $po->getDomainHash( domain => "com.example.api", locale => "ja_JP" );
Possible options are:
domainThe domain for the data, such ascom.example.apilocaleThe locale to return the associated dictionary.
getLangDataPath
Contrary to its JavaScript equivalent, this takes no parameter. It returns the value of the environment variable TEXTLOCALEDIR if found.
This is used internally during object instantiation when the path parameter is not provided.
getLanguageDict
Provided with a locale, such as ja_JP and this will return the dictionary for the current domain and the given locale.
getLocale
Returns the locale set for the current object, such as fr_FR or ja_JP
Locale returned are always formatted for the server-side, which means having an underscore rather than an hyphen like in the web environment.
getLocales
Provided with a msgid (i.e. an original text) and this will call "fetchLocale" and return those span tags as a string containing their respective localised content, joined by a new line
getLocalesf
This is similar to "getLocale", except that it does a sprintf internally before returning the resulting value.
getMetaKeys
Returns an array of the meta field names used.
getMetaValue
Provided with a meta field name and this returns its corresponding value.
getMonthsLong
Returns an array reference containing the 12 months in their long representation.
my $ref = $po->getMonthsLong();
# Assuming the locale is fr_FR, this would yield
print $ref->[0], "\n"; # janvier
getMonthsShort
Returns an array reference containing the 12 months in their short representation.
my $ref = $po->getMonthsShort();
# Assuming the locale is fr_FR, this would yield
print $ref->[0], "\n"; # janv.
getNumericDict
Returns an hash reference containing the following properties:
my $ref = $po->getNumericDict();
currencystringContains the usual currency symbol, such as
€, or$, or¥decimalstringContains the character used to separate decimal. In English speaking countries, this would typically be a dot.
int_currencystringContains the 3-letters international currency symbol, such as
USD, orEURorJPYnegative_signstringContains the negative sign used for negative number
precisionintegerAn integer whose value represents the fractional precision allowed for monetary context.
For example, in Japanese, this value would be 0 while in many other countries, it would be 2.
thousandstringContains the character used to group and separate thousands.
For example, in France, it would be a space, such as :
1 000 000,00While in English countries, including Japan, it would be a comma :
1,000,000.00
getNumericPosixDict
Returns the full hash reference returned by "lconv" in POSIX. It contains the following properties:
Here the values shown as example are for the locale en_US
currency_symbolstringThe local currency symbol:
$decimal_pointstringThe decimal point character, except for currency values, cannot be an empty string:
.frac_digitsintegerThe number of digits after the decimal point in the local style for currency value: 2
groupingThe sizes of the groups of digits, except for currency values. unpack( "C*", $grouping ) will give the number
int_curr_symbolstringThe standardized international currency symbol:
USDint_frac_digitsintegerThe number of digits after the decimal point in an international-style currency value: 2
int_n_cs_precedesintegerSame as n_cs_precedes, but for internationally formatted monetary quantities: 1
int_n_sep_by_spaceintegerSame as n_sep_by_space, but for internationally formatted monetary quantities: 1
int_n_sign_posnintegerSame as n_sign_posn, but for internationally formatted monetary quantities: 1
int_p_cs_precedesintegerSame as p_cs_precedes, but for internationally formatted monetary quantities: 1
int_p_sep_by_spaceintegerSame as p_sep_by_space, but for internationally formatted monetary quantities: 1
int_p_sign_posnintegerSame as p_sign_posn, but for internationally formatted monetary quantities: 1
mon_decimal_pointstringThe decimal point character for currency values:
.mon_groupingLike grouping but for currency values.
mon_thousands_sepstringThe separator for digit groups in currency values:
,n_cs_precedesintegerLike p_cs_precedes but for negative values: 1
n_sep_by_spaceintegerLike p_sep_by_space but for negative values: 0
n_sign_posnintegerLike p_sign_posn but for negative currency values: 1
negative_signstringThe character used to denote negative currency values, usually a minus sign:
-p_cs_precedesinteger1 if the currency symbol precedes the currency value for nonnegative values, 0 if it follows: 1
p_sep_by_spaceinteger1 if a space is inserted between the currency symbol and the currency value for nonnegative values, 0 otherwise: 0
p_sign_posnintegerThe location of the positive_sign with respect to a nonnegative quantity and the currency_symbol, coded as follows:
0 Parentheses around the entire string. 1 Before the string. 2 After the string. 3 Just before currency_symbol. 4 Just after currency_symbol.positive_signstringThe character used to denote nonnegative currency values, usually the empty string
thousands_sepstringThe separator between groups of digits before the decimal point, except for currency values:
,
getPlural
my $str = $po->getPlural( $singular, $plural, $count, [ $domain ] );
Calls "plural" in Text::PO and returns an array object (Module::Generic::Array) with 2 elements.
Internal helper used by "ngettext" and "dngettext". It applies the plural expression associated with the current (or given) domain/locale, selects the correct form, and returns a Text::PO::String.
See "plural" in Text::PO for more details.
getText
my $str = $po->getText( $msgid );
Internal workhorse used by "gettext" and domain-aware variants.
Provided with an original string, and this will return its localised equivalent if it exists, or by default, it will return the original string.
Returns a Text::PO::String as described elsewhere.
getTextf
my $str = $po->getTextf( $msgid, @args );
Provided with an original string, and this will get its localised equivalent that wil be used as a template for the sprintf function. The resulting formatted localised content will be returned.
gettext
Provided with a msgid represented by a string, and this return a localised version of the string, under the current domain and locale, if any is found and is translated, otherwise returns the msgid that was provided.
$po->gettext( "Hello" );
# With locale of fr_FR, this would return "Bonjour"
See the global function "_" for more information.
Note that as of version v0.5.0 of Text::PO, this returns a Text::PO::String, which is lightweight and stringifies automatically. It provides the benefit of tagging the string with the locale attached to it.
Thus, in the example above, the resulting Text::PO::String would have its method locale value set to fr_FR, and you could do:
my $localised = $po->gettext( "Hello" );
say "Locale for this string is: ", $localised->locale;
If no locale string was found, locale would be undefined.
gettextf
my $str = $po->gettextf( $format, @args );
This is an alias to "getTextf"
Like "gettext", but intended for printf-style formatting. It looks up $format as the message id, then applies sprintf to fill in @args.
The returned object is still a Text::PO::String; formatting is performed on the underlying string.
isSupportedLanguage
if( $po->isSupportedLanguage( 'ja_JP' ) )
{
# Do something
}
Provided with a locale such as fr-FR or ja_JP no matter whether an underscore or a dash is used, and this will return true if the locale has already been loaded and thus is supported. False otherwise.
language
Returns a string containing the value of the GNU PO file header Language.
$po->language();
languageTeam
Returns a string containing the value of the GNU PO file header Language-Team.
$po->languageTeam();
lastTranslator
Returns a string containing the value of the GNU PO file header Last-Translator.
$po->lastTranslator();
locale
$po->locale( 'fr-FR' ); # or 'fr_FR' either is the same
my $locale = $po->locale;
Returns the locale set in the object as a scalar object. if sets, this will trigger the (re)load of po data by calling "textdomain"
Note that this does not modify global environment variables such as LANGUAGE or any process-wide locale; the change is strictly per-object.
locale_unix
my $unix_locale = $po->locale_unix; # en_GB
my $norm = $po->locale_unix( 'en-gb' ); # becomes en_GB
Provided with a locale, such as en-GB and this will return its equivalent formatted for server-side such as en_GB
locale_web
my $unix_locale = $po->locale_web; # en-GB
my $norm = $po->locale_web( 'en_gb' ); # becomes en-GB
Provided with a locale, such as en_GB and this will return its equivalent formatted for the web such as en-GB
mimeVersion
Returns a string containing the value of the header MIME-Version.
$po->mimeVersion();
ngettext
This perform plural-aware lookup.
Takes an original string (a.k.a message id), the plural version of that string, and an unsigned integer representing the applicable count, and this selects the appropriate translation according to the plural rules for the current locale/domain. For example:
$po->ngettext( '%d comment awaiting moderation', '%d comments awaiting moderation', 12 );
# Assuming the locale is ru_RU, this would return:
# %d комментариев ожидают проверки
As with "gettext", the result is a Text::PO::String which you can print directly or pass to sprintf:
printf $po->ngettext(
'%d apple',
'%d apples',
$count,
), $count;
path
Sets or gets the filesystem path to the base directory containing the locale data:
$po->path( '/locale' ); # /locale contains en_GB/LC_MESSAGES/com.example.api.mo for example
plural
my $plural_def = $po->plural;
$po->plural( [ $n, $expr ] );
Sets or gets the definition for plural for the current domain and locale. Usually you do not need to set this manually; it is derived from the PO metadata (Plural-Forms).
It takes and returns an array reference of 2 elements:
- 0. An integer representing the various plural forms available, starting from 1
- 1. An expression to be evaluated resulting in an offset for the right plural form. For example:
-
n>1or more complex for Russian:
(n==1) ? 0 : (n%10==1 && n%100!=11) ? 3 : ((n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20)) ? 1 : 2)
pluralForms
Returns a string containing the value of the header Plural-Forms.
$po->pluralForms();
po_object
Returns the Text::PO object used.
poRevisionDate
Returns a string containing the value of the header PO-Revision-Date.
$po->poRevisionDate();
potCreationDate
Returns a string containing the value of the header POT-Creation-Date.
$po->potCreationDate();
projectIdVersion
Returns a string containing the value of the header Project-Id-Version.
$po->projectIdVersion();
reportBugsTo
Returns a string containing the value of the header Report-Msgid-Bugs-To.
$po->reportBugsTo();
textdomain
$po->textdomain( $domain );
Given a string representing a domain, such as com.example.api and this will load (or reload) the .json (if the "use_json" option is enabled), .po or .mo file found in that order.
Normally you do not need to call this directly; lookups such as "gettext" will ensure the relevant domain is available.
Internally, domain data is cached in a per-class repository (using Module::Generic::Global) so that repeated lookups for the same domain and locale are efficient and thread-safe.
use_json
my $flag = $po->use_json; # 1
$po->use_json(1);
Takes a boolean and if set, Text::PO::Gettext will use a json po data if it exists, otherwise it will use a .po file or a .mo file in that order of preference.
_get_po
Returns the Text::PO object used.
JAVASCRIPT COMPANION
This distribution provides a JavaScript companion library (see the share directory in the Text::PO distribution) that can read JSON-based PO data generated from your .po or .mo files. This makes it straightforward to share the same localisation data between Perl and browser-side code.
THREAD & PROCESS SAFETY
Text::PO::Gettext is designed to be fully thread-safe and process-safe, ensuring data integrity across Perl ithreads and mod_perl’s threaded Multi-Processing Modules (MPMs) such as Worker or Event. It combines system-level file locking (flock) with Perl-level synchronisation via Module::Generic::Global to provide robust, system-wide thread-safe, and process-safe file operations.
AUTHOR
Jacques Deguest <jack@deguest.jp>
SEE ALSO
Text::PO, Text::PO::String, Text::PO::Element, Text::PO::MO
https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html
COPYRIGHT & LICENSE
Copyright(c) 2021-2025 DEGUEST Pte. Ltd. DEGUEST Pte. Ltd.