Skip to main content

Translations

Every string the TUI shows can be presented in another language - both the library's own chrome (key hints, buttons, validation and error messages) and the questions you declare (field and panel labels, descriptions, option labels and help text). 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\PhpTui\Builder\Form;
use DrevOps\PhpTui\Translation\Translator;
use DrevOps\PhpTui\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\PhpTui\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\PhpTui\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/phptui/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.