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:

  1. URL prefix -- /de/blog sets locale to de
  2. Cookie -- reads the locale cookie
  3. Accept-Language header -- browser language preference
  4. 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.