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 :param replacement 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.