NAME
App::Access2CSV::I18N - Message catalog and translated error messages for App::Access2CSV
VERSION
Version 0.001.0
SYNOPSIS
use App::Access2CSV::I18N;
# 1. Get a message as text
my $text = App::Access2CSV::I18N->i18n('missing_database');
# "Missing database filename"
# 2. Fill in values (the message has %s and %d placeholders)
print App::Access2CSV::I18N->i18n('progress', { params => [1, 3, 'Customers'] }), "\n";
# "[1/3] Customers"
# 3. Let the count choose between singular and plural
print App::Access2CSV::I18N->i18n('summary', { params => [$n, 0], count => $n }), "\n";
# $n == 1: "Processed 1 table, 0 failed"
# $n == 4: "Processed 4 tables, 0 failed"
# 4. Add a translation. Keys you do not translate stay in English.
$App::Access2CSV::I18N::MESSAGES{de}{missing_database} =
'Name der Datenbankdatei fehlt';
$ENV{LANG} = 'de_DE.UTF-8';
# 5. Use it as a base class, to get i18n() and the error helpers
package My::Tool;
use parent 'App::Access2CSV::I18N';
sub check {
my ($self, $file) = @_;
$self->_croak_i18n('database_not_file', { params => [$file] }) unless -f $file;
return $self;
}
DESCRIPTION
Every message that App::Access2CSV prints, logs or throws is looked up here, by a short name called a key (for example output_exists). This keeps all the text in one place, so the program can be translated.
The text for each key is a template. A template is usually a sprintf format: %s is replaced by a text value and %d by a whole number, in order. A template can also have different forms:
Plural forms, chosen by a count:
onefor one item,otherfor any other number. Some languages use more forms (zero,two,few,many).Context forms, chosen by a word you give, such as
maleorfemale. A context form can itself contain plural forms.
The templates live in the hash %App::Access2CSV::I18N::MESSAGES:
%MESSAGES = (
en => {
missing_database => 'Missing database filename',
summary => {
one => 'Processed %d table, %d failed',
other => 'Processed %d tables, %d failed',
},
...
},
);
Only English (en) is included.
Which language is used
- 1. The
languagefield of the object, if you calli18non an object that has one (for exampleApp::Access2CSV::Exporter->new(language => 'en')). - 2. Otherwise the first of these environment variables that is set and is not
CorPOSIX:LANGUAGE,LC_ALL,LC_MESSAGES,LANG. Only the language part is used:de_DE.UTF-8meansde. Upper or lower case does not matter.LANGUAGEcan hold a list such asfr:de; only the first entry is used. An empty value, or an empty first entry (:de), counts as "not set", so the next variable is tried. - 3. If there is no catalog for that language, English is used.
Inside a language, a key that has no translation falls back to the English text, one key at a time.
Helpers for subclasses
Subclasses get two protected methods. They can be called only from this class and its subclasses:
$self->_croak_i18n($key, \%args)- throws an exception (with "croak" in Carp) with the translated message. It never returns.$self->_carp_i18n($key, \%args)- warns (with "carp" in Carp) with the translated message, with control characters escaped. It returns$self.$self->_printable($text)- returns$textwith control characters (C0 except tab, DEL, C1, and text-direction controls) shown as escapes such as\x1B, so that text from an untrusted database is safe to print or log.
ENCODING
The English catalog is plain ASCII.
Values in params are copied into the message as they are. Byte strings (such as UTF-8 file names from the command line) stay byte strings, so non-ASCII text and emoji come out unchanged when printed.
Translations with non-ASCII text (for example German
uewritten as one letter, or Japanese) must be Perl character strings: write them in a source file withuse utf8;. Do not mix them with UTF-8 byte strings inparams, or the bytes will be encoded a second time. When you print such messages, give the output handle an encoding, for examplebinmode(STDERR, ':encoding(UTF-8)'), or Perl warns "Wide character in print".
COMMON PITFALLS
Percent signs. A template with no
paramsis returned exactly as written, so100%is safe. But when you giveparams, the template goes throughsprintf, so a literal percent sign must be written%%.Number of values. Give exactly as many
paramsas the template has placeholders. Too few gives a Perl "Missing argument" warning;undefinparamsgives a "Use of uninitialized value" warning.No count means plural. If you do not give
count, theotherform is used, even if the number inparamsis 1.Replacing a key replaces all of its forms.
%MESSAGESis not merged in depth. If you set$MESSAGES{en}{summary} = 'Done', theoneandotherforms ofsummaryare gone. If a translation gives a plural hash, it should have anotherform: when no form fits, the English text for that key is used instead.Unknown context. A
contextthat the template does not have is ignored; the plural forms (or the plain text) are used instead.Unknown keys are fatal. A key that is not in the English catalog is a programming error:
i18ncallsconfess, which stops the program and prints a stack trace.undef arguments.
undefforargs, or for a field inside the hashref form, is treated as "not given".undefas the key is reported as a missing key.Load with use, not require. The protection of
_croak_i18nand_carp_i18nis set up at compile time. After a run-timerequireit is missing, and Perl prints "Too late to run CHECK block".
METHODS
i18n
Purpose
Turn a message key into text in the user's language. Choose the right context and plural form, then fill in the values.
Arguments
You can call i18n on the class or on an object. There are two ways to give the arguments:
$obj->i18n($key, \%args);
$obj->i18n({ key => $key, args => \%args });
key(string, required)-
The message key, for example
'output_exists'. args(hash reference, optional)
Returns
The message as a string, without a newline at the end.
Side Effects
None. It only reads %MESSAGES and %ENV. Your $@, $! and $_ are left as they were.
Usage
my $text = $self->i18n('summary', { params => [3, 0], count => 3 });
EXAMPLE
# "Output file already exists: out/Orders.csv (use --overwrite to replace it)"
my $msg = App::Access2CSV::I18N->i18n('output_exists', { params => ['out/Orders.csv'] });
# A message with context and plural forms
$App::Access2CSV::I18N::MESSAGES{en}{greeting} = {
female => { one => 'She sent %d letter', other => 'She sent %d letters' },
other => 'They sent %d letters',
};
print App::Access2CSV::I18N->i18n('greeting',
{ params => [2], count => 2, context => 'female' }), "\n";
# "She sent 2 letters"
API SPECIFICATION
Input
{
key => {
type => 'string',
min => 1,
optional => 0,
},
args => {
type => 'hashref',
optional => 1,
schema => {
params => { type => 'arrayref', optional => 1 },
count => { type => 'integer', optional => 1, min => 0 },
context => { type => 'string', optional => 1 },
},
},
}
Valid and invalid values (tested in t/domain.t):
key valid: a key in the English catalog
invalid: "" (1 character is the minimum), undef, a
reference, an unknown key (fatal: "Unknown message
key"), a known key with extra characters
count valid: whole numbers from 0 up (0 is the minimum; 2**53
works); undef means "no count"
invalid: -1 and below, fractions (1.5), words
edges: English and German: 1 is singular, 0 and 2 plural.
French: 0 and 1 singular. Japanese, Korean,
Chinese: always the "other" form
params any number of values, including none; each value is
copied into the text exactly, whether it is a Perl
character string or UTF-8 bytes (non-ASCII letters,
emoji, joined emoji, combining marks, right-to-left text)
context any string; one the template does not have (including "")
is ignored; a reference is invalid
language (from the environment) the first 2 or 3 letters, in any
case; 1 letter, non-ASCII letters, C and POSIX all mean
"no language"
Output
{
type => 'string',
}
MESSAGES
+-----------------------------+-------------------------------+---------------------------------+
| Message | Meaning | What to do |
+-----------------------------+-------------------------------+---------------------------------+
| Unknown message key: KEY | KEY is not in the English | Programming error: add KEY to |
| (fatal, with stack trace) | catalog | $MESSAGES{en} |
| Required parameter 'key' is | No key was given | Give a key |
| missing (fatal) | | |
| Unknown parameter 'X' | args has a field that is not | Use only params, count and |
| (fatal) | params, count or context | context |
| Parameter 'count' (X) must | count is negative or not a | Give a whole number, 0 or more |
| be ... (fatal) | whole number | |
| Parameter 'params' must be | params is not an array | Give an array reference |
| an arrayref (fatal) | reference | |
+-----------------------------+-------------------------------+---------------------------------+
PSEUDOCODE
check key and args
lang := the object's language, or the language from the environment,
or English if there is no catalog for it
for try in (lang, then en if lang is not en): # at most 2 tries, no recursion
entry := catalog[try][key], or else catalog[en][key]
if entry has forms and one matches args.context:
entry := that form
if entry still has forms:
entry := the form for plural_category(try, count),
or else the "other" form
stop if entry is now a plain string
if no try gave a plain string: confess (the catalog is broken)
if there are params: return sprintf(entry, params)
else: return entry unchanged
LIMITATIONS
Only an English catalog is included. Other languages fall back to English, one key at a time.
Plural rules exist only for a few languages (
en,de,fr,ja,ko,zh). Other languages use the English rule.Messages from other modules (for example Params::Validate::Strict and autodie) are not translated.
Sub::Private and Sub::Protected set up their protection at
CHECKtime. If this module is first loaded at run time (withrequireafter the program has been compiled), Perl warns "Too late to run CHECK block" and the private and protected helpers are not protected.
SEE ALSO
App::Access2CSV, App::Access2CSV::Exporter
AUTHOR
Nigel Horne, <njh at nigelhorne.com>
LICENSE AND COPYRIGHT
Copyright 2026 Nigel Horne.
Usage is subject to the GPL2 licence terms. If you use it, please let me know.
FORMAL SPECIFICATION
These schemas use the Z notation. ? marks an input and ! an output. You do not need to read this section to use the module.
[KEY, LANG, CTX, VALUE]
CATEGORY ::= zero | one | two | few | many | other
TEMPLATE ::= text⟨⟨seq CHAR⟩⟩
| forms⟨⟨(CTX ∪ CATEGORY) ⇸ TEMPLATE⟩⟩
┌─ Catalog ──────────────────────────────────────────────────
│ MESSAGES : LANG ⇸ (KEY ⇸ TEMPLATE)
│ plural : LANG × ℕ → CATEGORY
├────────────────────────────────────────────────────────────
│ en ∈ dom MESSAGES
└────────────────────────────────────────────────────────────
i18n
┌─ I18n ─────────────────────────────────────────────────────
│ ΞCatalog
│ key? : KEY ; params? : seq VALUE ; count? : ℕ ; context? : CTX
│ userLang : LANG ; lang : LANG ; msg! : seq CHAR
├────────────────────────────────────────────────────────────
│ key? ∈ dom MESSAGES(en)
│ lang = (if userLang ∈ dom MESSAGES then userLang else en)
│ t₀ = (if key? ∈ dom MESSAGES(lang)
│ then MESSAGES(lang)(key?) else MESSAGES(en)(key?))
│ t₁ = (if t₀ = forms(f) ∧ context? ∈ dom f then f(context?) else t₀)
│ t₂ = (if t₁ = forms(g)
│ then (if plural(lang, count?) ∈ dom g
│ then g(plural(lang, count?)) else g(other))
│ else t₁)
│ msg! = (if params? = ⟨⟩ then t₂ else sprintf(t₂, params?))
└────────────────────────────────────────────────────────────
┌─ I18nUnknownKey ───────────────────────────────────────────
│ ΞCatalog
│ key? : KEY ; error! : seq CHAR
├────────────────────────────────────────────────────────────
│ key? ¬in; dom MESSAGES(en)
│ error! = "Unknown message key: " ⁀ key?
└────────────────────────────────────────────────────────────
STATE DIAGRAM
This module keeps no state between calls: i18n only reads %MESSAGES and %ENV. The diagram shows the steps of one call. Each box is a step. Each arrow shows what decides the next step.
i18n($key, \%args)
|
v
+------------------+ key missing, or args
| VALIDATING | has a bad field
+------------------+-----------------------------+
| OK |
v |
+------------------+ |
| CHOOSING LANGUAGE| object language, else |
| | LANGUAGE/LC_ALL/ |
| | LC_MESSAGES/LANG, else en |
+------------------+ |
| |
v v
+------------------+ key not in +------------------+
| LOOKING UP KEY | English either | FATAL |
| (language, then |------------------>| croak / confess |
| English) | +------------------+
+------------------+
| template found
v
+------------------+ plain text
| NARROWING FORMS |-----------------------+
| 1. context form | |
| 2. plural form | |
| (else other) | |
+------------------+ |
| one plain template left |
v v
+------------------+ no params +------------------+
| FORMATTING |-------------->| return text |
| sprintf(params) |-------------->| unchanged / |
+------------------+ params | formatted |
+------------------+