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

Laravel Chained Translator Laravel Package

statikbe/laravel-chained-translator

Chain multiple translation providers in Laravel to automatically fill missing translation keys. Configure a sequence of translators with fallbacks so untranslated strings are resolved across services and locales, keeping your language files complete with minimal manual effort.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Check your Laravel version before anything else

2.6.0 adds Laravel 12 support and targets Laravel 13, and ships "a major refactor of the translation manager." That combination usually means the supported-version floor moved. Before composer require, confirm your app's Laravel constraint is inside the package's composer.json range:

composer require statikbe/laravel-chained-translator:^2.6
composer show statikbe/laravel-chained-translator --all | grep -A5 require

If you're on Laravel 10 or 11, expect to either stay on ^2.5 or upgrade Laravel first. Read the compare link in the release notes (2.5.0...2.6.0) and diff src/ — the refactor touched the manager, so any custom code that subclassed or type-hinted the old classes needs a look.

2. Install and let discovery do its job

Laravel's package discovery registers the service provider automatically. The provider's job is to swap Laravel's translation.loader binding (the FileLoader by default) for the package's chained loader, so every __(), trans(), trans_choice(), @lang and Blade {{ __('...') }} call resolves through the chain. No application code changes required.

3. Publish and read the config — do not trust old snippets

php artisan vendor:publish --provider="Statikbe\ChainedTranslator\ChainedTranslatorServiceProvider"

The file you care about is config/chained-translator.php. Because 2.6.0 is a manager refactor, read the shipped config and the ChainedLoader source in vendor/ before copying any config example from a blog post or from a 2.5-era project — key names and the loader list shape are exactly the kind of thing a refactor renames. Confirm the ordered list of loaders, then read the loader to see how it decides to fall through.

4. Start with the smallest useful case

The canonical first use case is file translations with database overrides: keep lang/en/*.php as the source of truth, let a translations table override rows per key without redeploying. Configure DB-first, file-second (verify key names against the published config):

// config/chained-translator.php
'loaders' => [
    \App\Translation\DatabaseLoader::class,        // override layer
    \Illuminate\Translation\FileLoader::class,     // baseline from lang/
],

5. Verify it works

php artisan optimize:clear
php artisan tinker
>>> __('messages.welcome');

Insert a row for messages.welcome in the DB and repeat — the DB value should win. If it doesn't, dump app('translation.loader') and confirm it is the chained loader and not something else (see Gotchas).


Implementation Patterns

File-first, DB-second (read-only override layer)

The most conservative pattern for teams migrating off lang/ files: files stay authoritative, the DB loader sits after them. Ship the table, backfill it, prove coverage, then flip the order. Have the DB loader swallow its own exceptions and return [] so the chain degrades to files if the database is down.

DB-first, file fallback (override/copywriting workflow)

The opposite order — DB first, files second — gives editors a live override layer while missing keys still resolve from versioned files. This is the pattern that justifies the package most clearly: a runtime-overridable dictionary without abandoning file-based translations for CI, tests, or fresh installs.

Custom loader for an external source

Implement Illuminate\Contracts\Translation\Loader and register it in the chain:

class CmsLoader implements \Illuminate\Contracts\Translation\Loader
{
    public function load($locale, $group, $namespace = null): array
    {
        // Must return [] on miss — never null, never throw.
        return $this->client->fetch($locale, $namespace, $group) ?? [];
    }

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

Keep load() cheap and idempotent; Laravel calls it once per locale/group/namespace triple per request (cached in Translator::$loaded), but the chain multiplies that by the number of loaders. After 2.6.0, re-run php artisan vendor:publish --force comparison on any loader interface stubs the package ships, in case the refactor widened or narrowed the contract.

Environment-aware composition

Because the chain is just config, compose it per environment in a service provider:

// AppServiceProvider::register()
if (app()->environment('local')) {
    config(['chained-translator.loaders' => [
        \App\Translation\DebugLoader::class,
        \Illuminate\Translation\FileLoader::class,
    ]]);
}

Effective for adding a "dump every lookup" loader in local/staging without touching production. If the refactored manager resolves the chain earlier than before (e.g. in a singleton resolved before your provider's register()), move this into bootstrap/app.php or a booting callback.

Migration workflow

  1. Add the DB loader after the file loader (no behavior change).
  2. Backfill from lang/ with an artisan command that reads the loader output and upserts.
  3. Flip the order in config and deploy.
  4. Keep the file loader in the chain forever as a fallback — removing it is what causes "missing key" regressions when a DB row is deleted.

Upgrading 2.5 → 2.6

Treat the "major refactor of the translation manager" as a breaking change until proven otherwise, even at a minor version number:

  • Diff the published config against yours (vendor:publish --force into a temp dir and compare).
  • Grep your app for any reference to the package's namespaces/classes beyond the config (ChainedLoader, any manager class) and check they still exist.
  • Run your translation test suite plus a smoke test of fallback-locale behaviour — a manager refactor is most likely to break locale fallback and JSON/* group handling.
  • Larastan is now wired into the package's CI, so the new code is static-analysis clean; if you're on Larastan/PHPStan you can now meaningfully analyse code that touches these classes.

Testing

Bind a deterministic in-memory loader rather than hitting the real chain:

$this->app->bind(\Illuminate\Contracts\Translation\Loader::class, fn () => new ArrayLoader([
    'en' => ['messages' => ['welcome' => 'Hi']],
]));

If assertions flake across tests, clear the translator's in-memory cache: app('translator')->setLoaded([]);. After the 2.6.0 test refactor upstream, don't assume your local test helper still matches the package's own fixtures — they moved.


Gotchas and Tips

  • Version floor moved. 2.6.0 targets Laravel 13 and adds Laravel 12 support. If your composer.json pins laravel/framework below 12, you cannot take this release; stay on ^2.5 and plan the framework upgrade.
  • "Missing" is [], not null, not false. The chain falls through only on an empty array. A loader returning ['key' => null] or ['key' => ''] is a hit and stops the chain. Single most common "my fallback isn't working" bug.
  • JSON keys use the * pseudo-group. __('Welcome') with no dot calls load($locale, '*', '*'). Your loader must handle that explicitly or JSON translations silently bypass the chain.
  • Namespaces must be proxied. Lang::addNamespace('admin', ...) and package-namespaced translations only reach children if the chained loader forwards addNamespace() / addJsonPath() to every child. If namespaced keys resolve to the raw key string, the proxy is broken — check the loader source before blaming config.
  • Lang::addLines() is not part of the chain. Runtime-added lines live on the Translator, above the loader, and always win. Handy for tests; dangerous if you think your DB loader is authoritative.
  • Fallback locale is handled above the chain. Laravel's Translator iterates fallback_locale itself, so each loader receives both primary and fallback locale. A loader that throws on an unexpected locale breaks fallback in a hard-to-trace way. This is the first thing to test after the manager refactor.
  • config:cache is mandatory after editing the chain. php artisan config:cache bakes the loader list; forgetting it means deploy changes that "did nothing." Use php artisan optimize:clear locally when debugging.
  • Registration order decides who wins. Any other package that rebinds translation.loader (translation managers, some multitenancy packages) fights yours. If translations suddenly stop overriding, dump app('translation.loader') and confirm it's the chained loader.
  • DB schema matters more than loader code. Index on (locale, namespace, group, key). Without it every request re-queries once per group touched — and the chain doubles that by falling through on misses.
  • Debug by decorating, not dd(). A LoggingLoader wrapping each child, logging locale/namespace/group plus hit/miss and elapsed time, is the fastest way to understand a surprising resolution. Keep it out of production.
  • Order is priority. There's no per-key priority config — first loader returning a non-empty array wins. Design loaders accordingly (e.g. an override loader should only return rows where overridden = true).
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.
laravel2580/idehelpers
delatbabel/viewpages
anavel/gettext
muhamadrezaar/highcharts
delatbabel/keylists
frankkessler/salesforce-laravel-oauth2-rest
anavel/foundation
wdmtech/enviricons
eii/laravel-captcha
muka/shape-reader
tdt/json
refinery29/php-cs-fixer-config
hotmeteor/eco-env
mvaliolahi/hydrate
wearede/tbcpay-php
parsnick/steak
thibaud-dauce/mattermost-php
multilinguals/apollo-client
antaresbr/collection
kr0lik/phpstan-rules