NAME
App::WebPerimeterScan::Messages - the message catalogue
VERSION
Version 0.001.0
SYNOPSIS
Read one message, without the placeholders filled in:
use App::WebPerimeterScan::Messages;
my $en = $App::WebPerimeterScan::Messages::CATALOGUE{en};
print $en->{'csp-missing.title'}, "\n";
Add a partial German catalogue. Every key it does not have falls back to English:
$App::WebPerimeterScan::Messages::CATALOGUE{de} = {
'csp-missing.title' => 'Content-Security-Policy fehlt',
'report.no_findings' => 'Keine Befunde.',
};
Add a message with a plural form:
$App::WebPerimeterScan::Messages::CATALOGUE{de}{'source-routes-unvisited.title'} = {
plural => 'count',
forms => {
one => '{count} Endpunkt wurde nicht gescannt',
other => '{count} Endpunkte wurden nicht gescannt',
},
};
Translate a saved JSON report afterwards, using the rule ids in it:
my $i18n = 'App::WebPerimeterScan::I18N';
my $report = JSON::PP::decode_json($json_text);
for my $finding (@{ $report->{findings} }) {
my $key = "$finding->{id}.title";
# A title may need values (such as {url}) that the report does not
# keep, so fall back to the English title already in the report.
my $title = $i18n->has_message($key) ? $i18n->i18n($key, $finding) : $finding->{title};
print "$title\n";
}
DESCRIPTION
This module holds every piece of text that the distribution shows to a person: finding titles and advice, error messages, warnings, report labels and the command-line help. No other module contains English sentences.
The text is in one hash, %App::WebPerimeterScan::Messages::CATALOGUE. Its keys are language codes (en), and each value is a hash of message key to message. How a message is written (placeholders, plural and select forms) and how the language is chosen are described in App::WebPerimeterScan::I18N.
Message keys
Keys are short, stable names made of words joined by dots, such as scope.empty or report.no_findings. The first word is the part of the program that uses the message.
The text of a finding uses the finding's rule id as the first part:
<rule id>.title required; one line
<rule id>.detail required; why the problem matters
<rule id>.fix optional; what to do about it
<rule id>.evidence optional; a template for the evidence line
The rule id is the id field of each finding, so a saved JSON report can be translated later by looking its ids up here. The one exception is the port rule: its ids are port-open-22, port-open-3306 and so on, and all of them share the messages under port-open.
This module has no subroutines.
COMMON PITFALLS
Do not replace the English catalogue. English is the fallback for every other language. A key removed from
enmakes every language die when that message is needed. Add to it, or add a new language.A new language replaces nothing until it is chosen. Adding
$CATALOGUE{de}does not change the output for a user whose environment saysen_GB. See "language" in App::WebPerimeterScan::I18N.Keep every placeholder. A translation that leaves out
{host}loses that value, with no warning. A translation that adds a placeholder the code does not pass shows it as{name}.A finding needs both
.titleand.detail. The test t/i18n.t fails when the code uses a rule id that does not have them.%CATALOGUEis a normal hash. Any code can change it at any time, and the change is seen by every later message.
ENCODING
This file is ASCII, and the English catalogue is ASCII.
A translation may use any UTF-8 text, including emoji. Write it as UTF-8 bytes, as Messages.pm does: do not add
use utf8. The reason is explained in "ENCODING" in App::WebPerimeterScan::I18N.A translation added at run time from a decoded source (for example a JSON file read with
decode_json) is made of character strings. Encode each string to UTF-8 bytes before you add it.
LIMITATIONS
Only English (en) is provided.
FORMAL SPECIFICATION
The catalogue is data, not code. This schema states the rules it must follow. t/i18n.t checks the last two for every key the code uses. The symbols are explained in "FORMAL SPECIFICATION" in App::WebPerimeterScan::I18N. MESSAGE ::= ... defines a type with three kinds of value; the double angle brackets hold the data each kind carries.
CATALOGUE
┌─ Catalogue ─────────────────────────────────────────
│ CATALOGUE : LANGUAGE ⇸ (KEY ⇸ MESSAGE)
│ MESSAGE ::= text⟨⟨STRING⟩⟩
│ | plural⟨⟨NAME × (FORM ⇸ MESSAGE)⟩⟩
│ | select⟨⟨NAME × (FORM ⇸ MESSAGE)⟩⟩
├─────────────────────────────────────────────────────
│ en ∈ dom CATALOGUE
│ ∀ k : used_keys • k ∈ dom CATALOGUE(en)
│ ∀ r : rule_ids • r.title ∈ dom CATALOGUE(en)
│ ∧ r.detail ∈ dom CATALOGUE(en)
└─────────────────────────────────────────────────────
STATE DIAGRAM
The catalogue is filled once, when the module is loaded. After that it changes only when some code assigns to it.
use App::WebPerimeterScan::Messages
|
v
+----------------+
| ENGLISH ONLY | CATALOGUE = { en => { ... } }
+----------------+
|
| code assigns $CATALOGUE{xx} = { ... }
v
+----------------+
| MORE LANGUAGES | CATALOGUE = { en => ..., xx => ... }
+----------------+
| ^
+------+ code adds or changes a key, or another language
Every i18n() call reads the catalogue as it is at that moment.
AUTHOR
Nigel Horne, <njh at nigelhorne.com>
LICENCE AND COPYRIGHT
Copyright 2026 Nigel Horne.
Usage is subject to the GPL2 licence terms. If you use it, please let me know.