Localization (Multilang)

Festi uses GNU gettext .mo binary files for translations. The locale system is initialized automatically by Core::onInitLocale() on startup, so all translation functions are available everywhere — in plugins, templates, DGS XML, and object classes.

Directory Structure

your-project/
├── locale/
│   ├── en.mo          # compiled English translations
│   ├── en.po          # English source (edit this)
│   ├── ua.mo
│   └── ua.po
└── plugins/
    └── MyPlugin/
        └── locale/
            ├── MyPlugin_en.mo   # plugin-specific overrides
            └── MyPlugin_ua.mo

By default, Festi looks for locale files in {root}/locale/. This can be changed via the OPTION_LOCALE_PATH configuration option.

Configuration

Set the active language and locale path when initializing Core:

<?php

$options = array(
    Core::OPTION_LANG        => 'ua',           // language code (default: 'en')
    Core::OPTION_LOCALE_PATH => '/path/to/locale/', // default: {root}/locale/
);

$core = Core::getInstance($options);

If no .mo file is found for the configured language, Core throws a SystemException at startup.

Translation Functions

All phrases shown to users must be wrapped in a translation function. Three functions are available globally (defined in src/locale/Utils.php):

Function Purpose
__(string $msg, ...$params) Translate a string (alias for __l)
__l(string $msg, ...$params) Translate a string
__p(string $singular, string $plural, int $count, ...$params) Translate with plural forms

All functions fall back to the original message key when no translation is found.

Basic translation

<?php

echo __('Customer');        // 'Клиент' (if ru locale is active)
echo __l('Customer');       // same result

Parameter substitution

Additional arguments are passed through vsprintf, matching sprintf conversion specifiers:

<?php

echo __('You get %s points on the subject of %s', 5, 'mathematics');
// → "Вы получили 5 баллов по предмету математика"

echo __('%s is required field', 'Caption');
// → "'Caption' является обязательным полем"

Plural forms

<?php

echo __p('lesson', 'lessons', 1);  // → "урок"
echo __p('lesson', 'lessons', 3);  // → "урока"
echo __p('lesson', 'lessons', 11); // → "уроков"

// With parameter substitution (third param = count, rest = sprintf args)
echo __p('%s hour ago', '%s hours ago', 5, 5);
// → "5 часов назад"

Using in Templates (.phtml)

Wrap every user-visible string in __():

<!-- templates/header.phtml -->
<a href="/logout"><?php echo __('Logout'); ?></a>
<h1><?php echo __('Welcome, %s', $userName); ?></h1>

Using in DGS XML (tblDefs/*.xml)

Field captions and select option labels must also use translation functions. XML attribute values support inline PHP:

<field
    name="status"
    type="select"
    caption="<?php echo __l('Status')?>"
>
    <values>
        <option id="active"><?php echo __('Active')?></option>
        <option id="inactive"><?php echo __('Inactive')?></option>
    </values>
</field>

<field
    name="school_id"
    type="foreignKey"
    caption="<?php echo __l('School')?>"
    table="schools"
/>

Plugin-Specific Locale Files

A plugin can ship its own translations in plugins/MyPlugin/locale/MyPlugin_{lang}.mo. Festi loads this file automatically when the plugin is initialized. The naming convention is:

{PluginName}_{lang}.mo

For example, for a plugin named Catalog with Russian translations:

plugins/Catalog/locale/Catalog_ua.mo

Note: There is a known issue (FIXME in CorePluginTrait::_setPluginLocale) where loading a plugin locale replaces the entire system locale instead of merging with it. Keep plugin .mo files complete (include all system strings the plugin uses) or rely solely on the global locale file.

Creating and Compiling Translations

Festi provides a CLI command (from the Festi CLI package) to scan source files and generate a .po template:

./vendor/bin/festi-locale

This collects all __(), __l(), and __p() calls and writes a .po file. Edit the .po file with Poedit or any gettext editor, then compile it to .mo:

# Using GNU gettext msgfmt
msgfmt locale/ua.po -o locale/ua.mo

PO file format

msgid ""
msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Plural-Forms: nplurals=3; plural=((n%10==1 && n%100!=11) ? 0 \
: ((n%10>=2 && n%10<=4 && (n%100<12 || n%100>14)) ? 1 : 2));\n"

msgid "Customer"
msgstr "Клиент"

msgid "lesson"
msgid_plural "lessons"
msgstr[0] "урок"
msgstr[1] "урока"
msgstr[2] "уроков"

Plural-Forms controls which msgstr[N] index is returned for a given count. The expression varies by language — Poedit sets this automatically when you choose the target language.

Accessing the Locale Model Directly

For programmatic use (e.g. inside an Object class), retrieve the LocaleModel from Core:

<?php

$locale = Core::getInstance()->getLocaleModel();
$word   = $locale->get('Customer');
$plural = $locale->getPlural('lesson', 'lessons', 5);

You can also inject a custom locale model (useful in tests):

<?php

$dictionary  = new MoDictionaryLocale('/path/to/test.mo');
$localeModel = new LocaleModel($dictionary);
Core::getInstance()->setLocaleModel($localeModel);