ongr/translations-bundle
ONGR TranslationsBundle adds database-backed translation management to Symfony apps, letting you store, edit, and organize translation messages as Doctrine entities instead of flat files, with import/export support for existing translations.
ongr/translations-bundle stores Symfony translation catalogues in Elasticsearch and feeds them back into the standard translator service, so {{ 'checkout.title'|trans }} keeps working unchanged while the source of truth moves out of YAML/XLIFF files and into an index. It sits on top of ongr/elasticsearch-bundle, which means the real prerequisite is an ONGR/Elasticsearch stack, not just Symfony.
Minimum steps:
Pin the environment first. This package's last release is from July 2016. Expect Symfony 2.8/3.x, PHP 5.5–7.0-era code, and Elasticsearch 1.x/2.x semantics (mapping types, string fields). Do not attempt this on Symfony 4+ or ES 6+ without a fork.
Install the stack, not just the bundle:
composer require ongr/elasticsearch-bundle ongr/translations-bundle
Register the bundles in AppKernel, ONGR first:
new ONGR\ElasticsearchBundle\ONGRElasticsearchBundle(),
new ONGR\TranslationsBundle\ONGRTranslationsBundle(),
Configure a manager and the managed locales:
# app/config/config.yml
ongr_elasticsearch:
managers:
default:
index:
index: app_translations
hosts: ['%elasticsearch_host%']
mappings:
- ONGRTranslationsBundle
ongr_translations:
managed_locales: ['en', 'de', 'fr']
Create the index, then import your existing files:
app/console ongr:es:index:create --manager=default
app/console ongr:translations:import --help # confirm flags in your version
app/console ongr:translations:import --path=app/Resources/translations
Verify the wiring before writing any code: app/console debug:translation en AppBundle should now show values coming from the index, and app/console debug:container | grep ongr_translations tells you which loader/services the bundle actually registered in your version. Service IDs drifted between 2015–2016 releases; trust the container over the README.
First real use case: a site where translators edit copy in a browser (the ONGR sandbox-style admin, or your own CRUD over the Translation document), changes land in ES immediately, and the next request renders the new copy without a deploy. That is the entire value proposition — if you don't need runtime-editable translations, you don't need this package.
Files in Git as source of truth, ES as runtime store. The most maintainable pattern with a package this old is to keep YAML/XLIFF in the repo, import them as a deploy step, and never hand-edit the index in production:
# deploy.sh
app/console ongr:es:index:create --manager=default --if-not-exists
app/console ongr:translations:import --path=app/Resources/translations --force
app/console cache:clear --env=prod
Go the other direction (ongr:translations:export) when translators have been working directly in the admin UI, so their changes get committed and reviewed like any other diff.
Rebuild the index rather than mutate it. Because imports generally upsert by translation key, keys deleted from your files tend to linger as orphans. For a clean state, drop and recreate the index on deploy (or on a versioned alias) and re-import. This also solves mapping changes, which ES 1.x/2.x cannot do in place.
Use domains deliberately. The loader is queried per locale (and per domain, depending on version), so keep messages, validators, and any custom domains separate. Don't dump everything into messages — it makes both the loader payload and the admin UI unusable.
Programmatic editing. For a custom admin controller, inject the document manager and work with the Translation document (key as _id, an array of locale/value pairs):
$dm = $this->get('ongr_elasticsearch.manager.default');
$repo = $dm->getRepository('ONGRTranslationsBundle:Translation');
$translation = $repo->find('checkout.title');
$translation->addTranslation('de', 'Zur Kasse');
$dm->persist($translation);
$dm->commit();
Refresh the index ($dm->refresh(), or wait for the 1s refresh interval) before asserting in tests, otherwise reads immediately after writes return stale data.
Locale fallback awareness. Symfony's fallback chain means a request for de_AT may load de and then en. Every locale in that chain must exist in managed_locales and must have been imported, or you'll get half-translated pages with no error.
Decorating the loader for caching. The biggest day-to-day integration tip: wrap the ES-backed loader in a caching decorator keyed by locale + domain, backed by your app cache pool. This turns a per-request ES round-trip into a cache hit and is the single change that makes the package viable under load.
services:
app.translation.loader.cached:
class: AppBundle\Translation\CachedLoader
decorates: <the loader service id from debug:container>
arguments: ['@app.translation.loader.cached.inner', '@cache.app']
debug:container and --help. Budget time for reading the bundle source; it's small.string fields, mapping types, and transport assumptions from ES 1.x/2.x will fail on ES 6+. This is a hard blocker, not a config quirk.index: not_analyzed on the key field, or checkout.title and checkout title collide.translation panel — measure with the Symfony profiler before blaming ES.cache:clear is not optional. Symfony caches the container and, in some configurations, compiled translation resources. A deploy that imports but doesn't clear can serve yesterday's copy from the app cache.debug:translation is your best debugging tool. It reads through the real translator, so it reflects exactly what ES is serving — including missing keys (shown as falls back to the key itself). Use it instead of curl-ing the index when a string "isn't translating".curl 'localhost:9200/app_translations/_search?pretty&size=5'. If the document isn't there, the problem is the import command, not the translator.MessageCatalogue from stored strings — but transChoice plural forms are resolved at import time from your XLIFF, so editing a pluralized message in the admin UI typically edits one variant only. Keep pluralized strings in files.TranslatorInterface + getCatalogue()->has()), otherwise untranslated keys silently ship to production.How can I help you explore Laravel packages today?