phpdot / i18n
Internationalization with ICU MessageFormat, pluggable loaders, PSR-16 caching.
Requires
- php: >=8.5
- ext-intl: *
- phpdot/contracts: ^0.2
- psr/simple-cache: ^3.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.94
- phpdot/container: ^0.2
- phpstan/phpstan: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/phpunit: ^13.0
Suggests
- phpdot/container: Autowires the container-attribute services and #[Config] DTOs declared in src (the attributes stay inert until reflected, so standalone consumers don't need it installed).
README
Internationalization built on ICU MessageFormat (via ext-intl): one syntax for interpolation,
pluralization, and selection. Translations load through pluggable loaders (PHP arrays, JSON, or a chain),
compiled maps are cached through any PSR-16 cache, and the translator implements the
PHPdot\Contracts\I18n\MessageTranslatorInterface so the rest of the ecosystem depends only on the
contract.
Table of Contents
Requirements
| Requirement | Constraint |
|---|---|
| PHP | >= 8.5 |
ext-intl |
* |
phpdot/contracts |
^0.2 |
psr/simple-cache |
^3.0 |
phpdot/container is a dev-only suggestion — the #[Config('i18n')] / binding attributes are inert
until a phpdot application reflects them.
Installation
composer require phpdot/i18n
Usage
use PHPdot\I18n\I18nConfig; use PHPdot\I18n\Translator; use PHPdot\I18n\Loader\PhpArrayLoader; $config = new I18nConfig( default: 'en', supported: ['en', 'ar', 'fr'], paths: ['/app/lang'], ); $translator = new Translator( loader: new PhpArrayLoader($config), cache: $cache, // any PSR-16 implementation config: $config, ); $translator->setLocale('ar_JO'); echo $translator->translate('messages.welcome', ['name' => 'Omar']);
Translation files
Keys are prefixed by filename, so lang/en/messages.php gives messages.welcome. Nesting flattens
onto that with dots, so group however reads best — the two save keys below are identical:
// lang/en/messages.php return [ 'welcome' => 'Welcome, {name}!', 'items' => '{count, plural, one {# item} other {# items}}', 'buttons' => [ 'save' => 'Save', // messages.buttons.save ], 'buttons.save' => 'Save', // the same key, written flat ];
Every configured path is scanned, in order, and a later one wins a duplicate key — which is how an
app overrides a catalog it ships with. JsonLoader reads the same shape from .json files;
ChainLoader layers loaders of different kinds (say PHP files plus database overrides). Every
template is ICU MessageFormat — interpolation, plural, and select all share one syntax — and
ICUValidator can check a template before it ships.
Architecture
Translator resolves a key to its ICU template through the configured LoaderInterface, formats it
with the current locale using ext-intl's MessageFormatter, and caches the compiled per-language map in
a PSR-16 cache. Unknown keys are recorded so missing translations can be audited.
graph TD
APP["Application / Contracts MessageTranslatorInterface"]
TRANSLATOR["Translator<br/><br/>resolve key → ICU template → format for locale"]
LOADER["LoaderInterface<br/><br/>PhpArrayLoader / JsonLoader / ChainLoader"]
CACHE["PSR-16 cache<br/><br/>compiled per-language maps"]
ICU["ext-intl MessageFormatter<br/><br/>interpolation, plural, select"]
APP --> TRANSLATOR
TRANSLATOR --> LOADER
TRANSLATOR --> CACHE
TRANSLATOR --> ICU
Loading
Testing
composer install composer test # PHPUnit composer analyse # PHPStan, level max + strict rules composer cs-check # PHP-CS-Fixer composer check # All three
License
MIT.
This repository is a read-only mirror, generated by CI from phpdot/monorepo. Pull requests and issues belong in the monorepo.