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

Translation Bundle Laravel Package

lexik/translation-bundle

Symfony bundle to import translation files (xliff, yml, php) into a database, edit and add translations through a GUI, export them back to files, and check that every translation domain is fully translated. It overrides the translator service with a database loader.

View on GitHub
Deep Wiki
Context7

Getting Started

First, set expectations: this is a Symfony bundle, not a Laravel package. There is no service provider, no Illuminate\Translation integration, no Eloquent models, and composer require lexik/translation-bundle into a Laravel app accomplishes nothing. Nothing in the repo is a drop-in. Treat it as one of two things: (a) a reference architecture you port to Laravel, or (b) a standalone Symfony admin sidecar you run next to Laravel against a shared database. Pick that before writing code — it's a 30-minute decision that determines whether you spend a day or a sprint.

Read these files first, in this order. Skip the README (it's a teaser) and go to:

  1. Resources/doc/index.md — the actual install/config reference.
  2. Translation/Loader/DatabaseLoader.php — the whole trick: a Symfony LoaderInterface implementation that reads from the DB and is registered after the file loaders so it wins on merge.
  3. Entity/TransUnit.php, Entity/Translation.php, Entity/File.php — the data model you'll be reimplementing. Note TransUnit is the key+domain, Translation is (trans_unit, locale, content), File tracks which imported file a unit came from.
  4. Command/ImportTranslationsCommand.php and ExportTranslationsCommand.php — the round-trip semantics, especially what --force does.
  5. Controller/TranslationController.php + Resources/views/ — the GUI surface, and how thin it is.

If you want to actually run it (recommended as a one-hour spike even if you plan to port), use a throwaway Symfony skeleton, not your Laravel app:

composer require lexik/translation-bundle
# config/bundles.php -> Lexik\Bundle\TranslationBundle\LexikTranslationBundle::class => ['all' => true]
# config/packages/lexik_translation.yaml
lexik_translation:
    managed_locales: ['en', 'fr', 'de']
    fallback_locale: en
    storage:
        type: orm
        object_manager: doctrine.orm.default_entity_manager
    base_layout: 'layout.html.twig'
# config/routes/lexik_translation.yaml
lexik_translation:
    resource: "@LexikTranslationBundle/Resources/config/routing.yml"
    prefix: /admin/translations

Then create the three tables (doctrine:schema:update --force in a throwaway, migrations in anything real), run bin/console lexik:translations:import, and open /admin/translations. You'll have a working mental model in under an hour, which is cheaper than reverse-engineering it from the schema.

If you're porting to Laravel, the minimal viable slice is: a translation_keys / translation_values / translation_files schema, a FileLoader subclass that overlays DB rows on file content, and two artisan commands. That's the 80% of the value. The GUI is a Filament resource. Map it like this:

Lexik / Symfony Laravel equivalent
Translator service override + DatabaseLoader FileLoader subclass bound over translation.loader
TransUnit / Translation / File entities translation_keys / translation_values / translation_files
lexik:translations:import / :export php artisan translations:import / :export
Doctrine event listener (cache invalidation) Eloquent observer + Cache::forget
Twig grid GUI Filament resource / Nova / Livewire table
managed_locales config config('app.supported_locales')

The overlay loader is genuinely small:

class DatabaseOverlayLoader extends FileLoader
{
    public function load($locale, $group, $namespace = null): array
    {
        $file = parent::load($locale, $group, $namespace);
        if ($group === '*') {
            return $file;
        }

        $db = Cache::rememberForever("tr:{$namespace}:{$group}:{$locale}", fn () => TranslationValue::query()
            ->where('locale', $locale)->where('group', $group)->where('namespace', $namespace)
            ->pluck('value', 'key')->all());

        return array_replace_recursive($file, $db); // DB wins, same precedence as Lexik
    }
}
// AppServiceProvider::register()
$this->app->extend('translation.loader', fn ($loader, $app) =>
    new DatabaseOverlayLoader($app['files'], $app['path.lang']));

Implementation Patterns

The daily loop is import → edit → export, not import → edit → done. The productive workflow is: files are the source of truth for keys, the database is the source of truth for copy. Engineers add new keys in lang/en/*.php and commit. A deploy step runs the import, which inserts new keys without touching existing DB rows. Content ops edits values in the GUI. A release step runs the export, which writes DB values back to lang/*/*.php, and that diff is reviewed and committed. If you skip the export step you get two diverging truths and nobody notices until someone runs a fresh clone.

Never let the GUI be the only place a key exists. In Lexik, a key created in the admin UI exists only in the database. It won't survive a database reset, it won't appear in code review, and a translations:clean will nuke it. Enforce a rule: keys are created in files, values are edited anywhere. In a Laravel port, add a source column (file vs ui) so you can report on UI-only keys as a drift metric.

Gate the admin surface as a first-class concern. Lexik ships controllers and routes with no authentication — the docs assume you put them behind a firewall and a prefix. This is the single most common production mistake. In Laravel: ->middleware(['auth', 'can:manage-translations']), and put it in a non-public route file. Also add an audit trail (translation_revisions: who, when, old value, new value) — this is the thing flat files are worst at, and it's the strongest argument for the DB-backed model in a regulated or brand-sensitive context.

Make the export a CI gate, not a habit. A nightly job (or pre-release CI step) that imports, exports, and fails if git diff --exit-code lang/ is non-empty turns "the DB and repo drifted" into a red build instead of a support ticket. Lexik gives you the commands; the discipline around them is yours.

Use --force and merge semantics deliberately. Default import is additive — it adds missing keys and leaves existing DB translations alone, which is what you want on every normal deploy. --force overwrites DB values from files, which is what you want exactly once, when bootstrapping, or when you've decided files are authoritative again. Wrap --force in a confirmation prompt or a separate translations:reset command in a Laravel port; you do not want it reachable from muscle memory.

Treat the "coverage overview" as a backlog generator. Lexik's grid highlighting of incomplete domains is its underrated feature. In a port, expose it as a dashboard tile (locale × domain with a % and a delta since last week) and, ideally, a scheduler job that posts the numbers somewhere humans look. Translation debt becomes rankable against feature work only when it's a number.

Cache aggressively, invalidate precisely. Both Lexik and a naive Laravel port resolve translations on every request. Cache per (locale, group, namespace) and flush only the affected key from an Eloquent observer on saved/deleted — not Cache::flush(). Do not cache the loader's array across a deploy either; config:cache/view:cache-style staleness in translations is a classic "why is it still showing the old string" hour.

Gotchas and Tips

Database silently beats files. The loader is registered last, so a DB row overrides an XLIFF/YAML/PHP entry with no warning and no log line. If someone edits lang/fr/messages.php directly and the key already exists in the DB, their change does nothing. This trips up every new team member exactly once — put it in the README in bold.

Stale cache is the number one "it's broken" report. After a GUI edit, the translator cache must be cleared. In Lexik, set debug: true in dev so you're never chasing ghosts; the bundle has a Doctrine listener that handles invalidation on write, but cache clears triggered elsewhere (deploys, cache:clear in a container that isn't the one serving traffic) reintroduce the problem. In a port, make the observer the only write path to translation values.

The bundle's PHP/Symfony compatibility matrix is narrow and moves fast. 8.x targets Symfony 8, 7.x targets Symfony 7.x, and older lines pin older Doctrine. Release notes show churn from dependency bumps and deprecation fixes (ArrayAccess on Doctrine FieldMapping, deprecated fetch(), PHP 8.4 deprecations). Pin the version, and don't assume a future minor is a drop-in.

The GUI's dependencies are not all in composer.json. Historically you had to install symfony/form, symfony/twig-bundle, symfony/asset, symfony/security-csrf, and doctrine/doctrine-bundle yourself, and doctrine/doctrine-bundle was actually moved to require-dev at one point. If the admin page 500s on a fresh install, check for missing Twig/Form/Asset packages before you check the bundle.

managed_locales is a hard allowlist. A locale present in your files but absent from managed_locales is invisible and uneditable in the GUI, and fallback_locale must be in the list. This is a config value, not a discovery mechanism.

Known UI bug in 8.1.0: clicking a locale/domain link on the overview page carries query params in the URL that the filter fields on the grid don't read, so the filter isn't applied. Cosmetic, but it's a good signal of the maintenance surface — expect to read source, not docs.

Assume you'll maintain this. 437 stars and effectively zero dependents means a small, thin ecosystem: docs are terse, issues may sit, and a Symfony major bump can break you for weeks. If that's unacceptable to your org, don't adopt even the pattern wholesale — build the 200-line Laravel version so you own it outright.

Extension points, if you do stay on Symfony: storage.type: custom + a StorageInterface implementation lets you swap the persistence layer (ORM/Mongo were supported; Propel was dropped); base_layout lets you inject the GUI into your own admin chrome; and Twig template overrides via templates/bundles/LexikTranslationBundle/ handle cosmetic changes without forking. In a Laravel port the equivalents are the loader binding, a Filament panel, and published views.

Never run lexik:translations:clean on a DB-as-source-of-truth setup. It removes translations that don't correspond to files — which is precisely the UI-created content you wanted to preserve. Read the command before running it anywhere near production.

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.
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
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource