NAME
App::Syslogd::I18N - Localised messages for App::Syslogd
VERSION
Version 0.002.0
SYNOPSIS
1. Get a message in the user's language
use App::Syslogd::I18N;
my $lh = App::Syslogd::I18N->handle(); # language from the environment
print $lh->text('listening', { address => '0.0.0.0', port => 514 }), "\n";
# Syslog server listening on 0.0.0.0 UDP port 514
2. Ask for one language
my $lh = App::Syslogd::I18N->handle('en-gb');
print $lh->text('shutdown', { count => 2 }), "\n";
# Syslog server shutting down after recording 2 messages
3. Add a translation
Put this in lib/App/Syslogd/I18N/de.pm. Inherit from the English lexicon, so that any message you have not translated yet is still shown in English instead of causing an error.
package App::Syslogd::I18N::de;
use parent 'App::Syslogd::I18N::en';
our %Lexicon = (
listening => 'Syslog-Server wartet auf [_1], UDP-Port [_2]',
shutdown => 'Syslog-Server endet nach [quant,_1,Nachricht,Nachrichten]',
);
1;
DESCRIPTION
A program shows messages to people: "listening on port 514", "could not open the file", and so on. This module keeps those messages in one place, so that they can be translated into other languages.
Each message has a key (a short name, such as listening) and some values (such as the port number). You give the key and the values; you get back the finished sentence in the chosen language.
Each language is a small Perl package called App::Syslogd::I18N::xx (where xx is the language code) with a hash called %Lexicon. The hash maps each key to its text. The text uses the "bracket notation" of Locale::Maketext:
[_1],[_2], ... are replaced by the values, in the order given by the table in "text".[quant,_1,message,messages]chooses the singular or plural form for the number in[_1], and writes the number.[sprintf,%05d,_1]formats a value, as Perl'ssprintfdoes.[gender,_1,his,her,their]chooses a word by gender; see "gender".A real square bracket is written
~[or~].
ENCODING
Language tags must be ASCII, for example
enoren-gb.Lexicon text may contain any Unicode characters, including non-ASCII letters and emoji, if the language file starts with
use utf8;. The English lexicon is pure ASCII.Values are copied into the message unchanged. They may be byte strings or character strings. Square brackets and
~in a value are not special: only the lexicon text is parsed.The result is a Perl string. If it contains characters above 255 (for example from a translation with emoji), set an output layer before printing it:
binmode(STDOUT, ':encoding(UTF-8)'). Otherwise Perl prints a "Wide character" warning.
COMMON PITFALLS
A translation must inherit from the English lexicon. If
App::Syslogd::I18N::deinherits only fromApp::Syslogd::I18N, any key it does not translate makes Locale::Maketext die with "maketext doesn't know how to say". Inherit fromApp::Syslogd::I18N::en, as in the SYNOPSIS, and missing keys fall back to English.Missing values become empty strings.
text('open_failed', {})gives "Could not open log file : " with no warning. This is on purpose (an error message should never fail), but check your values if a message looks incomplete.undef or non-numbers in a plural become 0.
text('shutdown', { count => undef })says "0 messages".An unknown key does not die.
text('no_such_key', { a => 1 })returnsno_such_key (a=1). This keeps the information in an error path, but it means a misspelt key is not reported. Test the messages you use.Values are matched to positions by name, not by order. The order of the
[_1],[_2]slots comes from a fixed table (see "text"), not from the order you write the hash. A new key must be added to that table too, or it is treated as unknown.gender() knows only "male" and "female". Any other value, including
m,fandundef, gives the neutral form. Upper and lower case are the same.The language comes from the environment when you give none:
LANGUAGE,LC_ALL,LC_MESSAGES, thenLANG. A language with no lexicon falls back to English without a warning.Operating-system errors stay in English. A value such as
"$!"is in the C locale unless the code that made it useduse locale, whateverLC_ALLsays.
METHODS
handle
Purpose: get a "language handle", the object that makes messages in one language.
Args: optional: a language tag, such as de or en-gb. Without one, the language is taken from the environment (LANGUAGE, LC_ALL, LC_MESSAGES, LANG).
Returns: an object of a App::Syslogd::I18N subclass. Never undef: if there is no lexicon for the language, you get the English one.
Side Effects: may load the language's module file.
Usage:
my $lh = App::Syslogd::I18N->handle();
EXAMPLE
my $lh = App::Syslogd::I18N->handle('fr'); # there is no French yet...
print ref($lh), "\n"; # ...so: App::Syslogd::I18N::en
API SPECIFICATION
INPUT
{
language => { type => 'string', optional => 1, position => 0 },
}
Domains: a supported tag (en, en-gb, EN) gives that language; an unsupported (fr), malformed (../x, en;x) or very long tag gives English; undef or "" reads the environment.
OUTPUT
{ type => 'object', isa => 'App::Syslogd::I18N' }
MESSAGES
None.
text
Purpose: make one finished message.
Args:
- 1. The message key.
- 2. Optional: a hash reference of named values. A missing value becomes an empty string.
The keys, and the values each one uses (in slot order [_1], [_2], ...):
+---------------+----------------------+
| Key | Values, in order |
+---------------+----------------------+
| usage | program |
| listening | address, port |
| shutdown | count |
| socket_failed | address, port, error |
| open_failed | file, error |
| unsafe_file | file |
| write_failed | file, error |
| recv_failed | error |
| no_log_open | (none) |
| not_a_datagram| type |
| missing_key | (none) |
| bad_values | type |
| no_progress | (none) |
| not_cgi | (none) |
| not_a_log | file |
| already_running | (none) |
+---------------+----------------------+
Returns: the message, as a string. An unknown key does not die: it returns the key, followed by its values in brackets if there are any. The most likely caller is reporting an error, and losing that error would be worse than showing an untranslated key.
Side Effects: none.
Usage:
my $msg = $lh->text('open_failed', { file => '/x', error => "$!" });
EXAMPLE
my $lh = App::Syslogd::I18N->handle('en');
print $lh->text('shutdown', { count => 1 }), "\n"; # "... 1 message"
print $lh->text('shutdown', { count => 2 }), "\n"; # "... 2 messages"
print $lh->text('no_such_key', { a => 1 }), "\n"; # "no_such_key (a=1)"
API SPECIFICATION
INPUT
{
key => { type => 'string', min => 1, position => 0 },
args => { type => 'hashref', optional => 1, position => 1 },
}
Domains: key as in the table below (undef, "" or a reference dies; an unknown key comes back as text). args: a hash reference or undef; an array, code, glob or plain string dies, including "" and 0. Values: any text, including non-ASCII characters, emoji and right-to-left text, copied unchanged; missing ones become "".
OUTPUT
{ type => 'string' }
MESSAGES
+-----------------------------------+----------------------------+------------------------------+
| Message (dies) | Meaning | What to do |
+-----------------------------------+----------------------------+------------------------------+
| maketext doesn't know how to say: | A translation has no text | Make the language inherit |
| KEY | for KEY and does not | from App::Syslogd::I18N::en|
| | inherit from English | (see COMMON PITFALLS) |
| A message key is needed | KEY was undef, empty or a | Pass a key from the table |
| | reference | |
| Message values must be a hash | The values were not a hash | Pass { name => value, ... } |
| reference (the type given was T)| reference | |
+-----------------------------------+----------------------------+------------------------------+
The English text of every key is in "i18n" in App::Syslogd.
gender
Purpose: choose a word by grammatical gender inside a message. You do not call it directly; a lexicon uses it with bracket notation: [gender,_1,male form,female form,neutral form].
Args: the gender value, then the male, female and neutral forms.
Returns: the male form for male, the female form for female (upper or lower case), and the neutral form for anything else, including undef.
Side Effects: none.
Usage:
# In a lexicon:
owner_changed => '[_1] changed [gender,_2,his,her,their] password',
EXAMPLE
my $lh = App::Syslogd::I18N->handle('en');
print $lh->gender('Female', 'his', 'her', 'their'), "\n"; # her
print $lh->gender(undef, 'his', 'her', 'their'), "\n"; # their
API SPECIFICATION
INPUT
{
gender => { type => 'string', optional => 1, position => 0 },
male => { type => 'string', position => 1 },
female => { type => 'string', position => 2 },
neutral => { type => 'string', position => 3 },
}
Domains of gender: "male" or "female" in any case pick their form; everything else (undef, "", "m", "f", references) picks the neutral form.
OUTPUT
{ type => 'string' }
MESSAGES
None.
LIMITATIONS
Only English is included. Messages that contain an operating-system error ($!) show it in English, as explained in "COMMON PITFALLS".
AUTHOR
Nigel Horne, <njh at nigelhorne.com>
LICENSE AND COPYRIGHT
Copyright 2026 Nigel Horne.
This program is released under the GNU General Public License, version 2 (see the LICENSE file). If you use it, please let me know.
FORMAL SPECIFICATION
This section describes each method exactly, in the Z notation. You do not need it to use the module.
handle
[LANGTAG, KEY, NAME, VALUE, STRING]
lexicons : LANGTAG ⇸ (KEY ⇸ STRING)
en ∈ dom lexicons
Handle
lang? : LANGTAG ∪ {⊥}
h! : HANDLE
─────────
let l == (if lang? = ⊥ then fromEnvironment else lang?) •
(l ∈ dom lexicons ⇒ language(h!) = l) ∧
(l ¬in; dom lexicons ⇒ language(h!) = en)
text
ARGUMENT_ORDER : KEY ⇸ seq NAME
Text
h? : HANDLE ; key? : KEY ; args? : NAME ⇸ VALUE
out! : STRING
─────────
key? ∈ dom ARGUMENT_ORDER ⇒
out! = render(lexicon(language(h?), key?),
⟨ n : ran ARGUMENT_ORDER(key?) •
if n ∈ dom args? ∧ args?(n) ≠ ⊥ then args?(n) else "" ⟩)
key? ¬in; dom ARGUMENT_ORDER ∧ args? = ∅ ⇒ out! = key?
key? ¬in; dom ARGUMENT_ORDER ∧ args? ≠ ∅ ⇒
out! = key? ⁀ " (" ⁀ join(", ", sorted(args?)) ⁀ ")"
gender
Gender
g? : STRING ∪ {⊥} ; m?, f?, n? : STRING ; out! : STRING
─────────
(g? ≠ ⊥ ∧ lower(g?) = "male" ⇒ out! = m?) ∧
(g? ≠ ⊥ ∧ lower(g?) = "female" ⇒ out! = f?) ∧
(g? = ⊥ ∨ lower(g?) ¬in; {"male", "female"} ⇒ out! = n?)
STATE DIAGRAM
A language handle has no states that change. handle() makes it, and after that text() and gender() only read it.
handle(LANG)
| [load LANG's lexicon, or English if there is none]
v
+-----------+
| READY |<-- text(KEY, VALUES) [return a message; no change]
| |<-- gender(...) [return a word; no change]
+-----------+
+--------+-------------------+--------+--------------------------------------+
| From | Trigger | To | Action / side effect |
+--------+-------------------+--------+--------------------------------------+
| (none) | handle(LANG) | READY | language chosen; module may be |
| | | | loaded |
| READY | text(KEY, VALUES) | READY | message returned |
| READY | gender(...) | READY | word returned |
| READY | text() for a key | READY | dies "maketext doesn't know how to |
| | the language | | say" (only if the language does |
| | lacks | | not inherit from English) |
+--------+-------------------+--------+--------------------------------------+