Translations
Every string the TUI shows can be presented in another language: the framework's own chrome (key hints, the help overlay, buttons, validation and error messages) and the questions you declare (field and panel labels, descriptions and option labels). A missing translation always falls back to the English source, so a partial catalog is safe.
Setting a translator
Translation is off by default. Turn it on by giving the form a Translator - the active language and the directories its catalogs live in - the same way you set a theme or key map:
use DrevOps\Tui\Builder\Form;
use DrevOps\Tui\Translation\Translator;
use DrevOps\Tui\Tui;
$tui = (new Tui(Form::create('My form')))
->translator(new Translator('es', [__DIR__ . '/translations']));
The facade activates the translator for the run, so labels and chrome resolve to the active language wherever they render - interactively and headlessly.
Choosing the language
The first argument selects the language:
| Intent | Argument | Result |
|---|---|---|
| Do not translate | omit ->translator(...), or '' | English source |
| Auto-detect | 'auto' | the environment's locale |
| Force a language | 'es', 'es_ES', ... | that language |
'auto' reads the POSIX locale variables LC_ALL, LC_MESSAGES and LANG (in that order) and strips the encoding, so LANG=es_ES.UTF-8 selects es_ES; a C or POSIX locale means English. A region locale falls back to its base language, so es_ES uses es.php when no es_ES.php exists.
Writing a catalog
A catalog is a plain PHP file named for its language that returns a source => translation map. The English source string is the key:
<?php
declare(strict_types=1);
// translations/es.php
return [
// Chrome.
'Submit' => 'Enviar',
'move' => 'mover',
'Passwords do not match.' => 'Las contraseñas no coinciden.',
// Questions - the labels you declared, keyed by their English source.
'Colour theme' => 'Tema de color',
'Pick a theme' => 'Elige un tema',
];
Your form keeps its English source strings; the catalog maps them to the target language, so one form definition serves every language.
Some strings carry @name placeholders (for example Enter a number @constraint. or @count items selected). Keep each placeholder verbatim in the translation - the library substitutes the value at render time.
Directories are searched in order and a later one wins, so you can layer a catalog that overrides individual strings on top of another.
Plural messages
A message whose wording depends on a count - "1 apple" versus "5 apples" - is rendered with Translator::formatPlural($count, $singular, $plural). The count picks the form and @count is bound to it:
use DrevOps\Tui\Translation\Translator;
Translator::formatPlural($count, '1 apple', '@count apples');
A translation supplies its forms as a list keyed by the plural source, and the active language's plural rule chooses one by the count. English needs only the two source forms, so a two-form language provides a list of two and no rule:
// translations/es.php
'@count apples' => ['1 manzana', '@count manzanas'],
A language with more forms supplies its own rule under the reserved Translator::PLURAL_RULE key - a fn(int $count): int returning the zero-based index of the form to use. Ukrainian has three forms (one/few/many):
<?php
declare(strict_types=1);
// translations/uk.php
use DrevOps\Tui\Translation\Translator;
return [
Translator::PLURAL_RULE => static function (int $count): int {
$mod10 = $count % 10;
$mod100 = $count % 100;
if ($mod10 === 1 && $mod100 !== 11) {
return 0; // one: 1, 21, 31 ...
}
if ($mod10 >= 2 && $mod10 <= 4 && ($mod100 < 12 || $mod100 > 14)) {
return 1; // few: 2-4, 22-24 ...
}
return 2; // many: 0, 5-20, 25-30 ...
},
'@count apples' => [
'@count яблуко',
'@count яблука',
'@count яблук',
],
];
Without a rule the default applies (the first form when the count is one, the second otherwise), and an index a translation does not cover falls back to the English plural, so a rendering is always defined.
The chrome catalog template
The package ships translations/en.php - the canonical list of every chrome string the library emits, as an English key => key map. Copy it to a locale file and translate the values to get a complete chrome catalog to start from:
cp vendor/drevops/tui/translations/en.php translations/de.php
A ready-to-use Ukrainian catalog, translations/uk.php, ships alongside it - point a Translator at the package's translations/ directory to use it directly.
Packaging
Catalogs are ordinary files loaded with require, so they bundle into a consumer-built PHAR with no extra configuration - reference them by a path relative to your application (for example __DIR__ . '/translations'), which resolves inside the archive.
A runnable demo in playground/12-translations.php localizes chrome and questions into Ukrainian - using the shipped uk.php for chrome plus a small local catalog for the form's own labels - and exercises pluralized rendering in its multi-select summary.