Internationalization
Preflow's i18n package provides translations, locale detection, and pluralization. Translation files are plain PHP arrays organized by locale and group.
Configuration
Configure available locales in config/i18n.php:
return [
'default' => 'en',
'available' => ['en', 'de'],
'fallback' => 'en',
'url_strategy' => 'prefix', // 'prefix' | 'none'
];
With the prefix strategy, URLs like /de/blog automatically set the locale to German. The locale prefix is stripped from the request path before routing.
Translation Files
Translations live in lang/{locale}/{group}.php and return associative arrays:
// lang/en/blog.php
return [
'title' => 'My Blog',
'published' => 'Published on :date',
'post_count' => '{0} No posts|{1} One post|[2,*] :count posts',
];
// lang/de/blog.php
return [
'title' => 'Mein Blog',
'published' => 'Veröffentlicht am :date',
'post_count' => '{0} Keine Beiträge|{1} Ein Beitrag|[2,*] :count Beiträge',
];
Translator API
The Translator resolves dot-notation keys with parameter replacement and locale fallback:
$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"
Parameters use the :param syntax and are replaced from the associative array.
Template Functions
Use translations in Twig templates with t() and tc():
{# Simple key #}
{{ t('blog.title') }}
{# With parameters #}
{{ t('blog.published', { date: '2026-01-01' }) }}
{# Pluralization -- third argument is the count #}
{{ t('blog.post_count', { count: 5 }, 5) }}
{# Component-scoped translation #}
{{ tc('label', 'MyComponent') }}
tc() converts the component name from PascalCase to kebab-case and uses it as the translation group. So tc('label', 'PostCard') resolves the key post-card.label.
Pluralization
The PluralResolver supports three formats:
Exact and Range
{0} No posts|{1} One post|[2,*] :count posts
Range Only
[2,10] Some|[11,*] Many
Simple Two-Form
One post|:count posts
When the count is 1, the first form is used; otherwise the second.
Locale Detection
The LocaleMiddleware detects the user's locale in priority order:
- URL prefix --
/de/blogsets locale tode - Cookie -- reads the
localecookie - Accept-Language header -- browser language preference
- Default -- falls back to the configured default locale
The middleware 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',
);
Add it to your global middleware stack in config/middleware.php.