trekels/translation-bundle
A Symfony bundle for managing application translations, letting you store and edit translation strings from a web interface instead of flat files. Useful for multi-language projects that need runtime editing.
Install the bundle with Composer and wire it into the kernel. Because this is a Symfony 2/3/4-era bundle (last tagged release November 2019), the exact registration style depends on your Symfony version:
composer require trekels/translation-bundle
// config/bundles.php
return [
// ...
Lexik\Bundle\TranslationBundle\LexikTranslationBundle::class => ['all' => true],
];
Point it at your translation directories and locales:
# config/packages/lexik_translation.yaml
lexik_translation:
fallback_locale: [en]
managed_locales: [en, fr, de]
default_domain: messages
resources_dir: "%kernel.project_dir%/translations" # legacy: app/Resources/translations
translation_key_structure: "%domain%.%locale%.%key%"
storage:
type: orm
object_manager: doctrine.orm.default_entity_manager
auto_cache_clean: true
enable_ui: true
enable_management: true
base_layout: "admin/layout.html.twig"
Mount the routes behind your admin prefix and lock them down — the GUI ships enabled by default and will let anyone with access edit translations:
# config/routes/lexik_translation.yaml
lexik_translation:
resource: "@LexikTranslationBundle/Resources/config/routing.yml"
prefix: /admin/translations
# config/packages/security.yaml
security:
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
Create the three tables the bundle expects (lexik_translation_unit, lexik_translation_translation, lexik_translation_file) from its Doctrine mappings, then import your existing YAML/XLIFF files:
php bin/console doctrine:schema:update --force # or generate a migration
php bin/console lexik:translation:import --force
First use case: the canonical workflow is "files exist, humans need to edit them." Import pulls file content into the DB, the grid at /admin/translations/grid lets a non-developer edit copy in every locale side by side, and lexik:translation:export writes the result back out to resources_dir. Look first at config/packages/lexik_translation.yaml, then at the three DB tables — they are the real state.
Decide who owns the truth. There are two viable operating modes and mixing them silently is how people lose translations:
import then editors work, and export is run and committed before release.For a team, mode 2 with a CI check is the safest.
CI drift guard. Export to a temp dir and diff against what is committed; fail the build if the GUI has changes nobody exported:
php bin/console lexik:translation:export --path=/tmp/export
diff -ru translations/ /tmp/export/ || exit 1
Deployment sequence. import --force on deploy only if you intend file content to win; otherwise import before editors touch the grid and never run it again in that environment, or you will overwrite their work.
php bin/console lexik:translation:import --force --locale=fr
php bin/console lexik:translation:export --locale=fr
php bin/console lexik:translation:clean --dry-run
Programmatic writes. For seeding from an external CMS or a migration script, use the manager/storage services rather than raw SQL:
use Lexik\Bundle\TranslationBundle\Manager\TranslationManager;
public function __construct(private TranslationManager $translationManager) {}
public function seed(string $domain, string $key, string $locale, string $content): void
{
$unit = $this->translationManager->getUnit($domain, $key, false)
?? $this->translationManager->newUnit($domain, $key);
$this->translationManager->addTranslation($unit, $locale, $content);
$this->translationManager->flush();
}
This keeps the lexik_translation_file rows and the hash bookkeeping consistent, which hand-written INSERTs do not.
Locale scoping. managed_locales governs what the grid shows and what --locale accepts. Keep it a subset of framework.translator.enabled_locales so the DB never serves a locale the app will not negotiate. The locale_provider extension point lets you restrict the GUI to locales your app actually ships.
Embedding the UI. base_layout lets the grid render inside your existing admin theme instead of the bundle's bare layout. Check the bundle's Resources/views/Translations/*.twig for the blocks it expects your layout to define; getting this wrong produces a blank page rather than an error.
Cache behaviour. With auto_cache_clean: true, saves trigger a translation cache invalidation (auto_cache_clean_batch_size chunks it for large catalogs). If you disable it for performance, you own the invalidation — cache:pool:clear cache.app or a full cache:clear after bulk edits, or editors will report "my change didn't save."
This package is effectively unmaintained. Zero stars, zero dependents, last release November 2019. It predates Symfony 5/6/7 and PHP 8. Concrete friction you should expect:
Symfony\Component\Translation\TranslatorInterface was removed in Symfony 5 — any code path referencing it is dead.Doctrine\Common\Persistence\* namespaces moved to Doctrine\Persistence\*; the ORM storage layer needs patching.ContainerAwareCommand, untyped ArrayAccess/Iterator implementations, and old service definition syntax all trip PHP 8.x and Symfony 5+.If you adopt it, pin the exact version and budget for a fork. If you're starting greenfield, Symfony's translation:extract plus a lightweight CRUD admin over your own translations table is usually less work than reviving this.
It replaces the translator service. The bundle's translator decorates/replaces the default so DB content can be served. If you or another bundle also decorates translator (or translator.default), decoration order decides who wins and failures are silent — one side just stops being consulted. Verify with:
php bin/console debug:container translator --show-arguments
import --force is destructive. Without --force the importer leaves existing DB rows alone; with it, file content overwrites GUI edits for matching keys. Never put --force in a cron job that runs after editors have been working. Run --help on lexik:translation:import to see the exact flags your pinned version supports — they have drifted between releases.
translation_key_structure drives export paths. If it does not match your file naming (messages.en.yaml vs en/messages.yaml vs one-file-per-domain), exports land in surprising places or create new files next to your real ones. Set it explicitly on day one and verify with a single export into a temp directory before trusting it.
Stored file paths go stale. lexik_translation_file records path and a content hash. Rename or move translation files and export will target the old location or skip the file. Re-import after any structural file change, or truncate the file table and re-import cleanly.
clean cannot see dynamic keys. lexik:translation:clean removes units with no translations, but keys built at runtime ('errors.' . $code) never appear in static scanning and can look unused. Always start with --dry-run and treat its output as a suggestion, not a policy.
Large catalogs are slow in the grid. The UI loads and paginates across units and every managed locale; with 20k+ keys and several locales, page loads become painful and the "toggle similar" feature multiplies the row count. Keep managed_locales narrow in the admin environment and consider a read-only replica for browsing.
Debugging translations served from the DB is different from debugging files. debug:translation reports what the translator resolved, which for this bundle may come from lexik_translation_translation rather than your YAML. When a string looks wrong, check the DB row first, then the file, then the cache — in that order.
php bin/console debug:translation fr --domain=messages
php bin/console doctrine:query:sql "SELECT u.\`key\`, t.content FROM lexik_translation_unit u JOIN lexik_translation_translation t ON t.unit_id = u.id WHERE t.locale = 'fr' LIMIT 20"
Extending it is easiest at the file-formatter and storage layers (storage.type, custom formatter services) and at the event dispatcher — inspect EventDispatcher/Event/ in the vendor directory for the actual event class names before subscribing, since they are not documented and differ between forks. Avoid subclassing the controllers; the grid's action signatures are the least stable part of the bundle.
How can I help you explore Laravel packages today?