forci/lexik-translation-ui-bundle
Provides a web interface for managing translations with LexikTranslationBundle, letting teams browse, edit, and organize translation messages from a single dashboard. Ideal for Symfony apps that need a lightweight translation admin UI.
Reality check before you start: this is a Symfony bundle, not a Laravel package. composer require in a Laravel app will get you nowhere — it expects a Symfony kernel, the lexik/translation-bundle translation storage layer, Twig, and Doctrine. Treat it as (a) something you'd use if you also run Symfony services, or (b) a reference implementation to copy for a Laravel equivalent. Both are covered below.
Prerequisite: lexik/translation-bundle installed and configured with Doctrine storage. This package is a UI shell over Lexik's storage/translation manager — it has nothing to render without it.
# config/packages/lexik_translation.yaml
lexik_translation:
fallback_locale: en
managed_locales: [en, fr, de]
storage: doctrine # verify the key name for your Lexik major version
Install: composer require forci/lexik-translation-ui-bundle. Pin an exact version in composer.json ("forci/lexik-translation-ui-bundle": "x.y.z") — with 0 stars and 0 dependents there is no ecosystem pressure keeping BC breaks honest.
Register the bundle in config/bundles.php. Read vendor/forci/lexik-translation-ui-bundle/ first: the bundle class namespace is in composer.json under autoload.psr-4, and the class name will be the file in the bundle root. Don't guess.
Mount routes — find the routing file in Resources/config/ (routing.xml, routing.yaml, or PHP attributes on controllers) and import it under a prefix you control:
# config/routes/lexik_translation_ui.yaml
lexik_translation_ui:
resource: "@ForciLexikTranslationUiBundle/Resources/config/routing.xml"
prefix: /_translations
Verify with php bin/console debug:router | grep -i transl.
Lock it down immediately. This UI writes to your database. Add ^/_translations to security.yaml with ROLE_ADMIN (or a dedicated ROLE_TRANSLATOR), and in production put it behind an IP allowlist or an APP_ENV !== 'prod' guard. Do this before your first cache:clear, not after.
First use case — production missing-key triage. Log untranslated keys (framework.translator.logging: true or a listener on the Symfony\Component\Translation\Event\* events), then use the UI to add the missing key in the DB and have it live in seconds instead of a deploy cycle. That is the entire value proposition of DB-backed translations; if you aren't doing this, the bundle is dead weight.
You cannot use this package directly. The transferable pattern is: translation catalog in the database + a writer UI on top, which pairs with spatie/laravel-translation-loader. Read this repo's controllers and templates as UX spec — the grid layout, per-locale columns, inline save, and import/export round-trip are the parts worth copying. A Filament resource or a single Livewire page over a language_lines table gets you 80% of it in an afternoon, with middleware-based auth you already understand.
Three viable models; mixing two is where teams get hurt:
| Model | Files | DB | Deploy flow |
|---|---|---|---|
| Files canonical | ✅ authoritative | transient | edit YAML → lexik:translations:import → deploy |
| DB canonical | export artifact | ✅ authoritative | edit in UI → lexik:translations:export → commit → deploy |
| DB canonical, files ignored | never read | ✅ authoritative | no deploy needed; DB is a shared service |
The bundle shines only in model 2 or 3. Model 1 with this UI installed is a trap: translators edit the DB, the next deploy imports files over their work, and nobody notices until a customer does.
dev adds keys in code → bin/console lexik:translations:import
translator edits in UI → (no code, no deploy)
release prep → bin/console lexik:translations:export --dir=translations
review + commit YAML → CI runs a diff check; unexpected deletions block the merge
The CI diff check is the single highest-value integration tip here: a translator deleting a key in the UI should show up as a reviewable YAML deletion, not as a silent null in production.
messages, validators, admin) — catalogs over ~500 keys get unusable in one grid. Filter server-side via the bundle's domain parameter, not client-side.access_control entry rather than trusting the template to hide buttons.Translation edits in the DB are invisible until the Symfony translation cache is rebuilt in prod. Wire it into your deploy or your save path:
php bin/console cache:clear
php bin/console translation:warmup prod # if you pre-warm
If you can't clear cache on every save, use the debug translator in staging only and accept a short lag in prod, or add a small event subscriber that invalidates the catalogue for the affected locale on save.
If your Laravel app needs to read translations written via this Symfony UI, don't duplicate the write path. Point Laravel's translation loader at the same table (or expose a small read-only JSON endpoint from Symfony and cache it in Redis with a TTL keyed on a translations_version row). Shared table + spatie/laravel-translation-loader is less code and fewer moving parts than an endpoint, if the schema matches.
Licensing is the first thing to resolve, not the last. The repo reports NOASSERTION — no recognized LICENSE file. LexikTranslationBundle itself is MIT, but a fork/extraction with no license grant is legally "all rights reserved" by default. Before this goes into anything commercial, open an issue asking the maintainer to add a LICENSE file, or budget for a clean-room reimplementation from the Lexik original (MIT, so you can lift it legally).
Zero stars, zero dependents means zero safety net. No Stack Overflow answers, no CVE feed coverage, no one else hitting the bug before you. Concretely: pin the version, vendor a fork under your own namespace if you need patches, and add a smoke test that boots the kernel and hits the index route so an upgrade can't silently break the UI. Read every file in the bundle before trusting it — it's a small package, and it sits on a write path to production data.
The UI runs on raw translation-table writes with no optimistic locking. Two translators on the same key = last write wins, silently, including on the same locale. If your team is more than one translator, that's a real data-loss path. Mitigate with per-domain assignment (one owner per domain) or add a updated_at-based conflict check in a forked controller.
Template overrides are your main extension point. Symfony's bundle override convention applies: create templates/bundles/ForciLexikTranslationUiBundle/<path> to override views without touching vendor/. Confirm the exact view paths from Resources/views/ first — get the directory name wrong and your override silently does nothing. Same for controllers: override the route to your own controller and call the bundle's as a parent, rather than editing vendor code.
Debugging checklist when the grid is empty or saves don't stick:
debug:router | grep -i translation — are routes actually mounted?debug:container | grep -i lexik — is the storage service wired to Doctrine, or still on the filesystem?managed_locales — a locale not in this list won't render as a column and its values may be silently dropped.translation table has rows: run an import before opening the UI. An empty table looks identical to a broken bundle.Performance cliffs. The grid loads all keys for a domain in one page. At ~2–3k keys it becomes unusable and starts timing out behind a proxy. If you're heading there, filter by domain aggressively and add pagination in a fork; don't discover this with a translator on a deadline.
Never expose it publicly, even briefly. "I'll secure it after I check it works" is how translation tables get edited by strangers. Add the firewall entry in the same commit that mounts the route. Also confirm the bundle's forms carry CSRF tokens — unverified third-party bundles are a common place to find them missing — and check the save action against your CSRF config if it's stateless.
Missing-key logging is the feature that justifies the DB. If you're not capturing untranslated keys from production traffic, the UI is just a worse YAML editor. Turn on translator logging in a low-traffic window, let it accumulate for a week, then batch through the UI. That loop — log, triage, edit, ship without deploy — is the actual day-to-day value.
How can I help you explore Laravel packages today?