Localization (i18n)
Unlike most PHP translation libraries, I18n\Translator reads
language files as PHP arrays (not flat JSON/YAML) — meaning each
entry can be an actual Closure, and therefore supports any
arbitrary expression (pluralization, gender matching, computed
formatting), not just simple placeholder substitution.
Language Folder Structure
lang/
├── en/
│ └── order.php
└── fa/
└── order.php
Each file is a logical namespace (e.g. order), and the final
translation key is read as {file}.{key} (e.g. order.created).
Writing a Language File
// lang/en/order.php return [ // A simple string with :name-style placeholders 'greeting' => 'Hello :name, welcome!', // A Closure with custom parameters — real pluralization, not just singular/plural 'created' => fn (int $count) => match (true) { $count === 1 => 'One order was placed successfully ✅', $count > 1 => "{$count} orders were placed successfully ✅", default => 'No orders were placed', }, // A Closure for computed formatting 'total_price' => fn (int $amount) => sprintf('Total: $%s', number_format($amount)), ];
Usage
use Aicrion\Tandroid\I18n\Translator; $translator = Translator::create(langPath: __DIR__ . '/lang', defaultLocale: 'en'); $translator->trans('order.greeting', ['name' => 'Alex']); // => "Hello Alex, welcome!" $translator->trans('order.created', [3]); // => "3 orders were placed successfully ✅" (Closure parameters are passed positionally) $translator->trans('order.created', [3], locale: 'fa'); // => "3 سفارش با موفقیت ثبت شد ✅"
Key Resolution Rules
- If an entry is a Closure, the
trans()params array is spread (...$params) into it — so parameter order must match the Closure's signature. - If an entry is a string, every
:keyin it is replaced with$params['key'](interpolate()). - If the key isn't found in the requested locale, the Translator
automatically falls back to its constructor's
defaultLocale. - If the key isn't found in any locale, the key itself
(
order.created) is returned — not an exception — so a missing translation doesn't break the whole request.
Using It in an Activity
Since Translator is a regular class, type-hint it in your
Activity's constructor (or register a shared instance yourself as a
service in the DI Container):
final class OrderActivity extends BotActivity { public function __construct( private readonly Translator $translator, ) {} public function onCreate(Intent $intent): ?NavigationRequest { $locale = $this->update->raw['message']['from']['language_code'] ?? 'en'; $this->setContentView( View::message($this->translator->trans('order.created', [1], $locale)), ); return null; } }
Note:
Translatorisn't automatically registered in the DI Container by default (because thelang/path is host-project specific, not something the framework itself owns). In your own host project, add$container->set(Translator::class, Translator::create(...)), or for simplicity, build a singleton inside your own wrapper service.