preflow/i18n
Translations, locale detection, and pluralization for Preflow applications.
Installation
composer require preflow/i18n
What It Does
- Loads PHP translation files from
lang/{locale}/{group}.php - Resolves dot-notation keys with
:paramreplacement and locale fallback - ICU-style pluralization with exact, range, and simple two-form rules
- PSR-15 middleware for locale detection (URL prefix, cookie, Accept-Language)
- Twig functions for global and component-scoped translations
Configuration
config/i18n.php:
return [
'default' => 'en',
'available' => ['en', 'de'],
'fallback' => 'en',
'url_strategy' => 'prefix', // 'prefix' | 'none'
];
Translation Files
lang/en/blog.php:
return [
'title' => 'My Blog',
'published' => 'Published on :date',
'post_count' => '{0} No posts|{1} One post|[2,*] :count posts',
];
Translator
$translator = new Translator(
langPath: __DIR__ . '/lang',
locale: 'en',
fallbackLocale: 'en',
);
$translator->get('blog.title'); // "My Blog"
$translator->get('blog.published', ['date' => '2026-01-01']); // "Published on 2026-01-01"
$translator->choice('blog.post_count', 5); // "5 posts"
$translator->setLocale('de');
$translator->getLocale(); // "de"
PluralResolver
Supports three formats:
{0} No posts|{1} One post|[2,*] :count posts // exact + range
[2,10] Some|[11,*] Many // range only
One post|:count posts // simple two-form (1 = first, else second)
LocaleMiddleware
PSR-15 middleware. Detects locale in priority order: URL prefix, cookie, Accept-Language header, configured default. Strips the locale prefix from the request path and sets a locale cookie on the response.
$middleware = new LocaleMiddleware(
translator: $translator,
availableLocales: ['en', 'de'],
defaultLocale: 'en',
urlStrategy: 'prefix',
);
TranslationExtension (Twig)
Register with your Twig environment:
$twig->addExtension(new TranslationExtension($translator));
{# Simple key #}
{{ t('blog.title') }}
{# With parameters #}
{{ t('blog.published', { date: '2026-01-01' }) }}
{# Pluralization -- third arg is the count #}
{{ t('blog.post_count', { count: 5 }, 5) }}
{# Component-scoped -- resolves "my-component.label" #}
{{ tc('label', 'MyComponent') }}
tc() converts the component name from PascalCase to kebab-case and uses it as the translation group.