Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Translation Bundle Laravel Package

shchedrakov/translation-bundle

A PHP translation bundle for managing localized strings in your application. It helps organize translation resources, simplify multilingual support, and integrate message catalogs into your existing framework setup with minimal configuration.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the honest framing: this is not a Laravel package. It is a Symfony 2/3 bundle (a fork of jms/translation-bundle) that wraps the Symfony Translation Component — service container, AppKernel, app/console, YAML config under a bundle key. There is no service provider, no facade, no config/translation.php. With 0 stars, 0 dependents, and a last release in March 2018, treat it as a legacy reference implementation, not something you composer require into a modern app.

You should read it if you are (a) maintaining an inherited Symfony 2.8/3.4 app, or (b) building equivalent tooling for Laravel and want a proven design for static extraction of translatable strings from templates.

What it actually does

Two things, and only these two things matter:

  1. Extraction — a console command walks your PHP and Twig sources, finds every trans() / transchoice() / |trans / {% trans %} call, and merges the keys into translation catalogues (.xlf, .yml, .php) without destroying existing translations.
  2. A translation web UI — a grid of messages × locales served by a bundle controller, so non-developers can fill in translations in a browser instead of editing XLIFF by hand.

Minimal steps (Symfony context)

composer require shchedrakov/translation-bundle
// app/AppKernel.php
$bundles = [ /* ... */ new JMS\TranslationBundle\JMSTranslationBundle() ];
# app/config/config.yml
jms_translation:
    configs:
        app:
            dirs: ["%kernel.root_dir%/../src", "%kernel.root_dir%/../app"]
            output_dir: "%kernel.root_dir%/../app/Resources/translations"
            ignored_domains: [routes]
            excluded_names: ["*Test.php", "*TestCase.php"]
            output_format: xlf
            extractors: [php, twig]
php app/console translation:extract en --config=app --output-format=xlf

Where to look first

  • DependencyInjection/Configuration.php — the real, authoritative list of options; the README is thinner than the config tree.
  • Translation/Extractor/ — the PHP and Twig extractors. This is the part worth stealing conceptually.
  • Translation/Extractor/Visitor/ — the node visitors that walk the AST. Note it is an AST walker, not a regex, which is why it survives formatting but also why it sees only static literals.
  • Controller/ — the web UI endpoints, which you will almost certainly need to firewall.

First Laravel-equivalent use case

If your goal is Laravel, the same job is done by kkomelin/laravel-translatable-string-exporter (Blade/PHP scanning → JSON/PHP lang files) plus barryvdh/laravel-translation-manager (the UI). Start there; come back to this repo only for design ideas.

Implementation Patterns

The extract → review → translate → commit loop

This is the workflow the bundle is built around, and it maps cleanly onto any framework:

# 1. Discover new/changed keys, print them, change nothing
php app/console translation:extract en --config=app --dump-messages

# 2. Merge into catalogues (existing targets are preserved)
php app/console translation:extract en --config=app --output-format=xlf

# 3. Human translates in the UI or the diff
# 4. Commit the catalogue diff alongside the code that introduced the keys

The key design decision to copy: extraction merges, it does not regenerate. Existing <target> values survive; only new <trans-unit> entries appear. That is what makes the output reviewable in a PR instead of a 4,000-line rewrite.

Multiple named configs

Rather than one giant config, define one per application surface. This is the bundle's strongest idea and has no Laravel-native equivalent:

jms_translation:
    configs:
        storefront:
            dirs: ["%kernel.root_dir%/../src/StorefrontBundle"]
            output_dir: "%kernel.root_dir%/../app/Resources/translations"
            ignored_domains: [routes, validators]
        admin:
            dirs: ["%kernel.root_dir%/../src/AdminBundle"]
            output_dir: "%kernel.root_dir%/../src/AdminBundle/Resources/translations"
            output_format: yml

Separate configs mean separate catalogue ownership, separate output formats, and no cross-team merge conflicts on a single messages.en.xlf.

Domains as module boundaries

trans('checkout.title', [], 'checkout') produces checkout.en.xlf. In Laravel terms this is lang/en/checkout.php and __('checkout.title') — file-per-domain, not one monolith. Adopt the same rule: if a key is used by more than one module, promote it to a messages domain rather than duplicating.

CI enforcement

The most valuable pattern here is failing the build on untranslated new strings:

php app/console translation:extract en --config=app --dump-messages --no-backup
git diff --exit-code app/Resources/translations || \
  (echo "Run translation:extract and commit the catalogue"; exit 1)

For Laravel, the analogue is running your exporter in --check/dry-run mode in CI and diffing lang/. Skipping this is why translation files rot.

Custom extractors (the extension point you'll actually use)

Anything the PHP/Twig visitors miss — say, keys built in a config-driven form builder — is handled by a custom extractor registered as a tagged service:

services:
    app.translation.extractor.form_labels:
        class: AppBundle\Translation\Extractor\FormLabelExtractor
        arguments: ["@form.registry"]
        tags:
            - { name: jms_translation.extractor, alias: form_labels }

Then extractors: [php, twig, form_labels] in the config. Do not extend the regex/AST visitors unless you must; a wholesale custom extractor that emits Message objects is far simpler to reason about and to test.

Restricting the web UI

The UI is a controller pair, imported via routing and protected by the firewall — never expose it publicly:

# app/config/routing.yml
jmstranslation:
    resource: "@JMSTranslationBundle/Resources/config/routing.xml"
    prefix: /_translations
# app/config/security.yml
access_control:
    - { path: ^/_translations, roles: ROLE_TRANSLATOR }

Treat the UI as an internal back office: it writes directly to the filesystem under output_dir, so its process needs write permission on your catalogue directories.

Translating the concepts to Laravel

This bundle (Symfony) Laravel equivalent
translation:extract kkomelin/laravel-translatable-string-exporter or a custom Blade scanner
messages.en.xlf / .yml catalogue lang/en/messages.php, lang/en.json
Domain argument 'checkout' File group lang/en/checkout.php → __('checkout.title')
transchoice / ` transchoice`
Translation web UI barryvdh/laravel-translation-manager
Profiler translation panel Debugbar + logging on missing keys
ignored_domains Per-file exclusion list in your exporter config

Gotchas and Tips

Abandonment is the headline risk. Last release 2018-03-15, zero stars, zero dependents. It targets PHP 5.6–7.x and Symfony 2.8/3.4. If you install it at all, pin an exact version, expect to vendor-patch any incompatibility, and have a removal plan from day one. There will be no upstream fixes for a security report.

Extraction is static — dynamic keys are invisible. trans($key), trans('foo.'.$id), and __("Hello {$name}") are all silently skipped. This is the single most common "why is my string missing" support question, and it applies identically to every Laravel exporter. Keep dynamic keys in a hand-maintained file and exclude that file from extraction.

Placeholders inside extracted literals become fragile. The extractor registers Hello %name% as the message id verbatim. Renaming the placeholder orphans the translation. Standardise on one placeholder syntax early and add it to your review checklist.

Pluralisation differs from ICU. The Symfony transchoice interval syntax ({0} none|{1} one|]1,Inf] many) is not ICU MessageFormat. If you also serve ICU-style catalogues, do not let the two share a file — the extractor will happily merge keys whose plural bodies mean different things.

--no-backup and .~ files. By default the command leaves backup copies in output_dir. Either add *~ to .gitignore or always pass --no-backup in CI. Mixing the two behaviours across developers produces confusing diffs.

--force means "regenerate", not "update". It rewrites the catalogue and can destroy human translations. Never put it in a script that a human might run casually; it is a one-off migration flag.

XLIFF version and target-language. Older versions emit XLIFF 1.2 while some translation platforms expect 2.0. Check target-language attributes on the <file> element — a mismatched or empty value causes silent drops in many TMS importers.

Caches and ordering. Catalogue changes require a cache clear on the Symfony side, and the extractor does not guarantee stable key ordering across runs. If your diffs look noisy, sort entries post-extraction with a small formatter rather than fighting the extractor.

The UI needs the route and the firewall and write permissions. All three fail independently and with unhelpful errors — a 404 means routing, a 403 means the firewall, and a save that silently does nothing means filesystem permissions on output_dir.

Extension points, ranked by usefulness: (1) custom extractors via jms_translation.extractor tags, (2) excluded_names / excluded_dirs for keeping fixtures and generated code out, (3) ignored_domains for framework-generated catalogues like routes. The AST visitors are technically extensible and practically not worth touching.

Best use of this repository today: read Translation/Extractor/Visitor/ once, understand how it distinguishes a literal argument from a concatenation, and then write the Laravel equivalent with nikic/php-parser for PHP and a Blade compiler pass for templates. That is a two-day job and gives you something maintained — versus a 2018 fork you cannot upgrade.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource