incenteev/translation-checker-bundle
Symfony bundle that keeps translations in sync across locales. It scans templates and code to flag missing, unused, and inconsistent messages, works with XLIFF and YAML catalogs, and adds a command you can run in CI to catch translation regressions early.
This is a Symfony bundle (not Laravel), so the first step is confirming you're in a Symfony app — the commands hook into the FrameworkBundle console and the translator service.
Check the platform requirements before installing. As of v1.10.0 the bundle requires PHP ≥ 8.2 and Symfony ≥ 6.4, and it now declares support for Symfony 8. If you're still on Symfony 5.4/6.0–6.3 or PHP 8.0/8.1, Composer will resolve you to v1.9.1 — that's the last release with the wider constraints. Pin explicitly if you need to stay there:
composer require --dev incenteev/translation-checker-bundle:^1.9 # legacy stack
composer require --dev incenteev/translation-checker-bundle:^1.10 # PHP 8.2+, Symfony 6.4+, incl. Symfony 8
Install as a dev dependency — it's a static-analysis/lint tool, not runtime code:
composer require --dev incenteev/translation-checker-bundle
Register the bundle in dev/test only. In modern Symfony (Symfony Flex), add it to config/bundles.php:
Incenteev\TranslationCheckerBundle\IncenteevTranslationCheckerBundle::class => ['dev' => true, 'test' => true],
On older apps, register it in AppKernel::registerBundles() inside the if (in_array($this->getEnvironment(), ['dev', 'test'])) block.
Discover the command surface first. There is no required configuration to get value:
php bin/console list translation
php bin/console translation:check --help
The bundle's whole job is one command: it statically scans your Twig templates and PHP source for translation keys actually used, compares them against the keys defined in your translations/ files, and reports missing keys (used but not defined) and unused keys (defined but never referenced).
First real use case: run it on an existing project right after install. The "missing" report is immediately actionable (you shipped a key that doesn't exist → users see the raw key). The "unused" report is noisier and usually needs an ignore list before you make it blocking.
Where to look first: the README for the exact option set of your installed version, then DependencyInjection/Configuration.php for the config tree, and the TranslationChecker service class if you need to extend extraction.
Local pre-commit / pre-push loop. Run translation:check after adding new templates or controller messages. It's fast enough to run on every change and catches the classic bug of adding {{ 'checkout.title'|trans }} without adding the key to messages.en.yaml.
CI gate — split the two checks. Missing translations should fail the build; unused translations should warn. Typical wiring:
# composer.json scripts
"scripts": {
"check-translations": "bin/console translation:check --env=test"
}
Then in CI run composer check-translations alongside lint:twig and lint:yaml. If the command supports separate exit behaviour or --ignore-unused, use that so a leftover key doesn't block a deploy while a missing key still does.
Pin your CI matrix to the release line you support. With v1.10.0 the support window splits cleanly: PHP 8.2+/Symfony 6.4–8 on ^1.10, older stacks on ^1.9. Run the checker on both the lowest and highest Symfony versions in your matrix so a Symfony 8 upgrade doesn't quietly change which keys the extractor finds.
Scope the run per domain/bundle. On large apps the default scan can be slow and noisy. Narrow it to the bundle or directory you're touching (options such as domain/prefix/bundle filtering — confirm names with --help for your version). A common pattern is one fast scoped check in the inner loop and a full check in CI.
Maintain an explicit ignore list. Dynamic keys — {{ ('status.' ~ code)|trans }}, $translator->trans($key), keys assembled from enums — are invisible to static analysis, so they show up as "unused." Keep a curated ignore list in the bundle config rather than disabling the unused check entirely; it documents why each key exists.
Use the checker service directly. The bundle exposes a TranslationChecker-style service that returns the used/defined key sets. Inject it into a custom console command or a PHPUnit test to assert your own invariants, e.g. "every key in the admin domain has a translation in en and fr."
Decorate for custom sources. If you have translation keys in JavaScript, Vue components, or a custom DSL, decorate the checker (or the extractor it delegates to) with an extra pass that appends keys to the "used" set. That keeps one source of truth instead of maintaining a parallel script.
Pair with Symfony's built-in tools. debug:translation shows per-locale coverage; translation:check shows static usage. Run both — debug:translation catches empty/missing per-locale values, the checker catches keys that are entirely absent or orphaned.
trans() calls, string concatenation, and keys computed at runtime will be reported as unused and their real keys as missing. Budget for an ignore list; don't treat "unused" as gospel.$translator->trans('literal.key')). If you wrap the translator in your own service, the checker won't see through it unless you decorate the extractor.|trans({}, 'admin') is picked up, but keys used without an explicit domain default to messages; a key that exists only under a custom domain can be falsely flagged as missing.templates/bundles/ are scanned if they live in your app directories.composer update of a transitive dev tool can be what drags the floor up. Pin the bundle version in composer.lock, and read the diff of any major Symfony bump.nikic/php-parser constraint still resolves alongside PHPStan/Rector — that parser version is the most common conflict, not the bundle itself.-v, then narrow by domain/prefix to isolate the offending directory. When a report seems wrong, grep the template/PHP for the literal key first — most surprises are a dynamic call you forgot about.laravel-lang/lang linters) scanning __()/@lang calls — the same static-analysis caveats apply.How can I help you explore Laravel packages today?