Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

I18N Bundle Laravel Package

snowcap/i18n-bundle

Internationalization bundle for PHP applications that centralizes locale and translation handling, with configuration-driven setup for managing multiple languages across your app. Integrates with the Snowcap bundle ecosystem.

View on GitHub
Deep Wiki
Context7

Getting Started

First, a reality check — this is not a Laravel package. snowcap/i18n-bundle is a Symfony bundle (last release July 2015, 1 star, 0 dependents, no maintainer activity for a decade). You cannot composer require it into a Laravel app: its composer.json pins Symfony 2.x components (symfony/symfony, symfony/framework-bundle) that will collide with the symfony/* versions Illuminate pulls in, and there is no Illuminate\Support\ServiceProvider anywhere in the tree. Treat it as reference material to read, not code to install.

Minimal steps to actually get value from it:

  1. Clone the repo outside your project (or composer require --dev into a throwaway Symfony skeleton) so you can read it without poisoning your app's composer.lock:
    git clone https://github.com/snowcap/SnowcapI18nBundle /tmp/i18n-ref
    
  2. Read in this order — this is where the design lives:
    • SnowcapI18nBundle.php (the bundle class) — tells you which container extensions and compiler passes are registered.
    • DependencyInjection/Configuration.php + DependencyInjection/*Extension.php — the full config surface and defaults. This is the closest thing to a "package README".
    • Resources/config/services.xml (or .yml) — the actual service graph: locale listeners, translation loaders, route decorators.
    • EventListener/ (or EventListener/RequestListener.php) — how the locale is resolved per request.
    • Resources/translations/ — what message domains and file formats it expects.
  3. There is effectively no docs site; the source is the documentation. Grep for getLocale, setLocale, kernel.request, and TranslatorInterface to find the three or four moving parts.
  4. First use case to evaluate: locale negotiation + persistence — does the bundle read Accept-Language, a session key, a URL prefix, or a user column, and in what priority? That single listener is usually the only piece worth reimplementing.

If you only want a working i18n setup today, skip the bundle entirely and use mcamara/laravel-localization (routes) + Laravel's built-in translator (messages). Come back to the Snowcap source only when you need to understand why someone would put the locale in session vs. URL — the bundle's listener chain is a decent worked example of the tradeoffs.

Implementation Patterns

Because you cannot run the bundle under Laravel, "implementation" means porting its patterns into idiomatic Laravel. Below are the four patterns the bundle encodes and their Laravel equivalents.

1. Locale resolution as request middleware. The bundle resolves the locale before the controller runs. Mirror that with a middleware registered in bootstrap/app.php (Laravel 11) or app/Http/Kernel.php:

final class SetLocale
{
    public function __construct(private array $supported) {}

    public function handle(Request $request, Closure $next): Response
    {
        $locale = $request->session()->get('locale')
            ?? $this->fromHeader($request)
            ?? config('app.fallback_locale');

        if (! in_array($locale, $this->supported, true)) {
            $locale = config('app.fallback_locale');
        }

        app()->setLocale($locale);
        Carbon::setLocale($locale);

        return $next($request);
    }
}

Note ordering: session → Accept-Language → fallback is the bundle's rough priority; keep it, because URL-prefix locale (see below) should win before session. Register the URL-aware variant first and let it short-circuit.

2. Locale in the URL for SEO. The bundle's routing integration exists because locale prefixes beat session switching for indexable content. In Laravel, don't fight the router — wrap it:

Route::group(['prefix' => '{locale}', 'where' => ['locale' => 'en|nl|de']], function () {
    Route::get('/', HomeController::class)->name('home');
    Route::get('/products/{product}', [ProductController::class, 'show'])->name('products.show');
});

Then a UrlDefaults middleware calling URL::defaults(['locale' => app()->getLocale()]) so route('home') works without passing the parameter everywhere. Guard against missing locale with a fallback route group outside the prefix, or a redirect route.

3. A translation loader backed by storage. The bundle's loader/compiler wiring is the seed for a database-backed translator — useful when marketing edits copy without a deploy. Register a custom loader with Laravel's translator in a service provider:

// app/Providers/TranslationServiceProvider.php
public function boot(): void
{
    $this->app['translation.loader']->addLoader('db', $this->app->make(DbLoader::class));
}

// config/app.php or a dedicated config/translations.php
'loader' => ['db' => ['database', 'translations']],
final class DbLoader implements LoaderInterface
{
    public function load($locale, $group, $namespace = null): array
    {
        return Cache::remember("i18n:{$locale}:{$group}", 600, fn () => Translation::query()
            ->where('locale', $locale)
            ->where('group', $group)
            ->pluck('value', 'key')
            ->all());
    }

    public function addNamespace($namespace, $hint) {}
    public function namespaces(): array { return []; }
}

Merge DB results over file results (array_replace(parent::load(...), $dbRows)) so files remain the safe baseline and the DB only overrides. Warm the cache on write via an observer; never let a request miss on cold cache and hit the DB per string.

4. Locale switching endpoint. The bundle exposes a switch route that writes the locale and redirects back. Keep that and make it explicit rather than implicit:

Route::post('/locale/{locale}', function (string $locale) {
    abort_unless(in_array($locale, config('app.supported_locales'), true), 404);
    session(['locale' => $locale]);
    return back();
})->middleware('throttle:30,1')->name('locale.switch');

Workflows that pay off

  • Keep a config/app.php-adjacent supported_locales array as the single source of truth for middleware, the switch route, and the where() constraint. Hard-coded three-way duplication is the most common drift bug.
  • Port tests, not code: the bundle's listener is easy to characterize with a handful of Pest tests over Accept-Language strings (nl-NL,nl;q=0.9,en;q=0.5).
  • For per-model translated content, reach for spatie/laravel-translatable rather than imitating the bundle's entity translation — that package is maintained and covers the case.

Gotchas and Tips

  • Composer will refuse to install it. Symfony 2.x constraints (symfony/symfony: ~2.3, PHP >=5.3.3 era) conflict with Laravel's symfony/http-foundation|translation|routing requirements. If you genuinely need to inspect it running, use a separate Symfony 2.8 skeleton — do not try composer require in your app and then --ignore-platform-reqs, or you'll spend an afternoon resolving transitively downgraded Symfony components.
  • It's abandoned, and the score reflects that. Opportunity score 2.6, 1 star, 0 dependents, last tag 2015. No security patch will ever land. Any pattern you copy, you own forever — that's the correct expectation going in.
  • Symfony 2 idioms don't map 1:1. You'll see $request->getSession()->get(...), getContainer()->getParameter(...), XML service definitions with %locale% parameters, and kernel.request listeners with priority integers. In Laravel: $request->session(), injected config, plain PHP service providers, and middleware ordering. Don't transliterate the container-aware style — it fights Laravel's DI.
  • Locale leakage is the classic bug. App::setLocale() is per-request in PHP-FPM but persists across requests under Octane/Swoole. If you port the middleware, reset in a terminate() or use Octane's sandboxed globals; otherwise a Dutch visitor's locale bleeds into the next request served by that worker.
  • Accept-Language parsing is deceptively hard. The bundle-era regexes miss q=0 (explicit reject), wildcards, and script subtags (zh-Hant-TW). Use symfony/http-foundation's Request::getPreferredLanguage($locales) — it's already in your vendor dir and handles the common cases correctly.
  • Fallback ≠ empty. Laravel falls back to app.fallback_locale; the bundle-era behaviour often returned the raw key. Decide explicitly and test it: __('products.empty') returning products.empty on screen is a shipping bug, not a feature.
  • Pluralization and formats diverge. If you copy message files, check ICU vs. Symfony's |-pipe plural syntax. Laravel uses | too, but trans_choice ranges ({0} none|{1} one|[2,*] many) can trip you up. Dates/numbers: set Carbon::setLocale() and use NumberFormatter for currency rather than number_format.
  • Cache invalidation is the real integration cost. Every DB-backed translation loader needs write-through invalidation (Cache::forget per locale/group, or a versioned key i18n:v{N}:{locale}:{group} bumped on save). Tag-based invalidation only works on Redis/Memcached/array — not the file or database cache drivers most apps start with.
  • Extension points worth knowing (before you decide to port anything): the translator service's addLoader, the kernel.request listener priority, and the routing decorator/URL::defaults equivalent. Those three are where nearly all behaviour lives; everything else is glue.
  • Debugging tip: when a locale "doesn't apply", dump app()->getLocale() at the top of the middleware, inside the controller, and in a Blade view. The bundle's listener chain has the same three layers; if the value changes mid-request, something downstream is calling setLocale() — commonly a Carbon::setLocale() side effect or a queued job bootstrapping a fresh app.
  • Verdict for day-to-day use: near-zero. Read it once for the locale-resolution and loader patterns, port ~100 lines of middleware + a loader into your app, and take a maintained dependency (mcamara/laravel-localization, spatie/laravel-translatable) for everything else.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console