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.