Skip to main content

Translations

Every string the TUI shows can be presented in another language - both 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 to ship.

Setting a translator

Translation is off by default. Turn it on by giving the Tui facade a Translator - the active language, and optionally the sources your own catalogs come from - the same way you'd set a theme or key map:

use DrevOps\Tui\Builder\Form;
use DrevOps\Tui\Translation\Translator;
use DrevOps\Tui\Tui;

// The bundled chrome catalogs load automatically, so this
// alone renders the chrome in Ukrainian:
$tui = (new Tui(Form::create('My form')))
->translator(new Translator('uk'));

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:

IntentArgumentResult
Do not translateomit ->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.

Adding and overriding strings

The package's bundled catalogs are always the implicit first source. Every source you pass merges on top of them, and a later source overrides an earlier one per key. Chrome strings and your form's strings share one key namespace, so a single source can cover any mix: only your own strings, your own plus a few chrome overrides, or a complete chrome replacement.

A source is one of three shapes:

Source shapeContentsLoads
DirectoryPer-locale catalog files (uk.php, es.php)The best candidate file for the active language
FileA single per-locale catalog fileOnly when its filename names the active language
Inline mapLocales keyed to catalogs, e.g. ['uk' => [...]]The best-matching locale key
// Your strings (and any chrome overrides) from one file:
new Translator('uk', [__DIR__ . '/translations/uk.php']);

// The same from an inline map - no file at all:
new Translator('uk', [
['uk' => ['Fruits' => 'Фрукти', 'Submit' => 'Готово']],
]);

// A directory of catalogs, plus a targeted override on top.
// 'auto' follows the terminal locale; each source contributes
// only when its locale matches the detected one:
new Translator('auto', [
__DIR__ . '/translations',
['uk' => ['Submit' => 'Готово']],
]);

A file or map section for a language other than the active one contributes nothing - the same per-locale semantics as a directory. A listed path that's neither a directory nor a file throws, so a typo surfaces instead of silently rendering English.

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.
'Fruits' => 'Frutas',
'Pick your fruits.' => 'Elige tus frutas.',
];

Your form keeps its English source strings; the catalog maps them to the target language. 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.

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, for example, 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. An index a translation doesn't cover falls back to the English plural, so a rendering is always defined. Plural-form lists and the rule work in every source shape - an inline map carries them exactly like a catalog file, and a consumer rule overrides the bundled one.

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, and you have 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 and loads automatically whenever the active language is Ukrainian.

Packaging

The bundled catalogs load from inside the package, and your own catalogs are ordinary files loaded with require - so both bundle into a consumer-built PHAR with no extra configuration. Reference yours 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 - the bundled uk.php supplies the chrome automatically, a small local catalog adds the form's own labels - and exercises pluralized rendering in its multi-select summary.