runroom/translation-bundle
Runroom Translation Bundle helps PHP applications manage multilingual translation resources, offering simple tools to organize, store, and retrieve translated messages so content stays consistent across locales and workflows.
Install and register
composer require runroom/translation-bundle
Register Runroom\TranslationBundle\RunroomTranslationBundle in config/bundles.php (usually only in dev/all with Sonata, but you typically want it in every env that has Sonata admin).
Configure the bundle
Create config/packages/runroom_translation.yaml. At minimum you'll declare:
framework.translator.enabled_locales),messages, admin, validators, etc.).
Only configured domains/locales show up in Sonata — see Gotchas.Create the schema
The bundle ships a Doctrine entity for stored translations but not a ready-made migration. Run bin/console doctrine:migrations:diff (or doctrine:schema:update in dev) and review the generated table before applying. It is usually a unique row per (domain, locale, key) with a text value.
Register the Sonata admin
Add the bundle's TranslationAdmin to sonata_admin.yaml (or via sonata.admin service attributes):
sonata_admin:
dashboard:
groups:
translations:
label: Translations
items:
- runroom.translation.admin.translation
Adjust the service id to whatever the bundle exposes in config/services.xml.
First use case
A content editor needs to change "Sign in" to "Log in" without waiting for a deploy. Developer keeps the canonical string in translations/messages.en.yaml, the editor opens Sonata → Translations → Translation, filters by domain/locale, finds the key, edits the value. The bundle's DB-backed loader/translator takes precedence over the file loader, so the override is served immediately (after cache warmup in prod).
Look here first: src/Admin/ (how the admin is built, what fields exist), src/Entity/Translation.php (schema and uniqueness), src/Repository/ (query helpers), src/DependencyInjection/Configuration.php (the real config keys), and src/Resources/config/ (service ids for admin/loader). Also read the bundle's README and CHANGELOG — the public config surface is small and the changelog tells you what broke between versions.
File defaults + DB overrides. Treat YAML files as the source of truth in code review and use the DB strictly for overrides. A custom translator/loader (provided by the bundle) merges these, DB winning. This keeps git blame meaningful while letting editors work.
Seeding existing translations. Write a one-off Symfony command that parses translations/*.{locale}.yaml and upserts rows into the translation table. This gets editors a populated admin on day one instead of an empty grid. Idempotent upserts (by domain+locale+key) make it safe to re-run after each deploy.
Import/export workflow. For larger teams, add commands to export DB overrides back to YAML (reviewable diff) or import a spreadsheet into the DB. Pair with debug:translation <locale> <bundle> to spot keys present in files but missing in DB and vice versa.
Admin wiring. Register the admin in a translations dashboard group and restrict it with a dedicated role (ROLE_TRANSLATOR) in security.yaml. Filter the admin list by domain/locale so editors don't page through every domain.
Cache strategy. In prod the translator caches results; after editor changes, flush the translation cache (cache:pool:clear cache.app or cache:clear if you must) or hook a Sonata post-update event on the translation admin to invalidate. For high-traffic apps, wrap DB lookups in a cache pool keyed by locale/domain.
Testing. Boot a kernel test with a translation row inserted, then assert $translator->trans('key', [], 'messages', 'en') returns the DB value, and that removing the row falls back to the file value. This catches loader priority regressions on upgrade.
Integration tips. If you already use Sonata for CMS content, put translations in the same group so editors have one place to look. Keep the bundle in a "content" bundle set rather than coupling it to domain code. If you replace Symfony's translator with a custom service, decorate the existing translator service instead of replacing it outright.
Only configured domains/locales exist. If a new domain or locale doesn't appear in Sonata, it's almost always the bundle config, not a cache issue. Also confirm framework.translator.enabled_locales matches.
Migrations aren't shipped. Upgrading the bundle can change the entity mapping; always diff the schema after composer update and check the CHANGELOG for BC breaks in the Translation entity.
Translation cache is sticky. A common "my edit didn't apply" report is a stale translation cache or opcache. In prod run cache:clear/cache:warmup or wire an event listener that clears the translation pool on admin updates. Verify with bin/console debug:translation en messages before assuming the loader is broken.
Empty string vs missing. Decide whether an empty DB value means "override with empty" or "fall back to file." The bundle typically treats a row as an explicit override; make sure editors know that clearing a cell may not delete the row and restore fallback — provide an explicit delete/restore action.
translation:extract conflicts. Running translation:extract regenerates YAML files but doesn't touch the DB. Your editors' DB overrides survive, which can confuse developers diffing files. Document that files are the schema of keys, the DB is the runtime value.
Performance. Loading every translation from DB per request is fine on small sites and painful on large ones. Prefer the bundle's cached loader, or add a request-scoped cache, and ensure the repository query is by (locale, domain) not a full table scan.
Extension points. If you need extra columns (e.g., updated_by, reviewed), extend the Translation entity and re-map, or decorate the repository. To change loader priority, decorate the bundle's translator/loader service rather than overriding its class — this survives minor upgrades.
Debugging checklist: (1) is the row actually in the DB, (2) does debug:translation show the DB value, (3) is the cache warm, (4) is the domain/locale configured, (5) is the admin route ACL-gated by a role the editor has. Ninety percent of "not working" reports stop at step 2 or 3.
Roles and dashboards. Sonata hides dashboard groups based on roles; if translators see nothing, it's often the ROLE_SONATA_ADMIN plus your translator role not being combined correctly in sonata_admin.security.
Compatibility. This bundle tracks Sonata Admin and Symfony LTS versions; pin it to a tag in composer.json and check the supported matrix before upgrading Sonata, since admin service definitions move between Sonata majors.
How can I help you explore Laravel packages today?