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 (
FIXMEinCorePluginTrait::_setPluginLocale) where loading a plugin locale replaces the entire system locale instead of merging with it. Keep plugin.mofiles 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);