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 Laravel Package

symfony/translation

Symfony's Translation component gives you the tools to internationalize your application: translate messages using locale-aware catalogs, handle pluralization and ICU message formats, and load translations from PHP, XLIFF, YAML, JSON, or PO files. Use it standalone or with the full Symfony framework...

View on GitHub
Deep Wiki
Context7

Getting Started

Install (standalone / non-Laravel code):

composer require symfony/translation

In a Laravel app you already have this transitively — illuminate/translation extends Symfony\Component\Translation\Loader\FileLoader and uses Symfony's MessageSelector for trans_choice(). So the component is present, but the Symfony Translator class itself is not wired into the container. Don't assume app(Symfony\Contracts\Translation\TranslatorInterface::class) resolves; bind your own instance explicitly.

Smallest working setup:

use Symfony\Component\Translation\Translator;
use Symfony\Component\Translation\Loader\ArrayLoader;

$translator = new Translator('fr_FR');
$translator->addLoader('array', new ArrayLoader());
$translator->addResource('array', [
    'Hello World!' => 'Bonjour !',
], 'fr_FR');

echo $translator->trans('Hello World!'); // Bonjour !

Where to look first (in this order):

  1. Translator::trans() and Translator::transChoice()/%count% handling — the whole API contract lives there.
  2. MessageCatalogue — the in-memory map of domain => id => message, plus metadata and fallback merging. Everything you debug ends up here.
  3. Loader/* — ArrayLoader, PhpFileLoader, XliffFileLoader, CsvFileLoader, JsonFileLoader, YamlFileLoader. Pick one and standardize.
  4. Formatter/* — MessageFormatter (default, %param%) vs IntlFormatter (ICU {param} + {count, plural, ...}).
  5. Provider/* — CrowdinProvider, LokaliseProvider, NullProvider for push/pull sync (commands live in FrameworkBundle).
  6. Extractor/* — PhpAstExtractor/PhpExtractor + ChainExtractor if you want to mine untranslated strings out of your own codebase.

First real use case: you have a CLI/queue worker or an API endpoint that renders user-facing strings and you can't rely on the framework's __() helper (no request locale, no container, or you want XLIFF catalogs shared with a translation vendor). Instantiate the Translator, register one loader, addResource() your files, call trans().

Implementation Patterns

Bind one translator as a singleton, and let everything else be injection.

// AppServiceProvider::register()
$this->app->singleton(Translator::class, function ($app) {
    $t = new Translator($app->getLocale());
    $t->addLoader('php', new PhpFileLoader());
    $t->addLoader('xliff', new XliffFileLoader());
    $t->setFallbackLocales(['en']);
    foreach (glob(lang_path('symfony').'/*.{php,xliff}', GLOB_BRACE) as $file) {
        // messages.en.php / messages+intl-icu.en.xliff
        [$domain, $locale] = parse_catalog_filename($file);
        $t->addResource(pathinfo($file, PATHINFO_EXTENSION), $file, $locale, $domain);
    }
    $t->setConfigCacheFactory(new ConfigCacheFactory(app()->environment('production')));
    return $t;
});

Caveat: addResource() at boot is fine for tens of files; for hundreds, register a custom loader instead (below) so lookups stay lazy.

One loader per source of truth. This is the pattern that actually pays off day-to-day:

Source Loader Why
Unit tests ArrayLoader no filesystem, no cache, deterministic
App strings PhpFileLoader opcache-friendly, no parse cost
Vendor / shared catalogs XliffFileLoader round-trips with translators' tools
Hand-off to translators CsvFileLoader / JsonFileLoader non-devs can edit in a spreadsheet
DB-backed / dynamic content custom LoaderInterface per-tenant, per-user-generated translations

Custom loader for DB or remote catalogs. This is the extension point people miss. Implement LoaderInterface::load($resource, $locale, $domain) returning a MessageCatalogue, then addLoader('db', $loader) and addResource('db', 'tenant_42', $locale, 'messages'). The $resource argument is yours to define — a tenant id, an S3 key, a locale row id. Wrap the loader with an in-memory memo + PSR-6 cache to avoid one query per trans() call.

Domains, not dot-notation keys. This trips up every Laravel dev:

// Laravel: first segment is the file
__('messages.welcome');

// Symfony component: domain is an explicit argument, key is flat
$translator->trans('welcome', [], 'messages');
$translator->trans('invoice.paid', ['%id%' => 42], 'billing', 'de_DE');

Use domains to separate validation, emails, legal — it lets a vendor translate one domain without touching the rest, and it lets you swap loaders per domain (e.g. legal from a signed-off XLIFF, notifications from the DB).

Pluralization and formatting. Symfony's MessageSelector handles interval syntax:

$translator->trans('apples', ['%count%' => 5]);
// 'apples' => '{0} No apples|{1} One apple|]1,Inf[ %count% apples'

You must pass %count% for the selector to pick a branch — passing ['count' => 5] silently returns the raw string. In Laravel's trans_choice() the placeholder is :count, not %count%, because Laravel does its own replacement after the selector runs. For currency/number/date formatting inside messages, switch the formatter to IntlFormatter and use ICU syntax ({count, plural, one{# apple} other{# apples}}); in Symfony this is selected by the +intl-icu filename suffix (messages+intl-icu.en.yaml). Laravel has no equivalent suffix convention — if you need ICU, register the IntlFormatter on your own translator instance.

Translatable objects instead of raw keys. Implement TranslatableInterface (or use TranslatableMessage) on DTOs/enums so domain code carries its own message id + params:

public function trans(TranslatorInterface $t, ?string $locale = null): string
{
    return $t->trans('order.status.'.$this->value, [], 'orders', $locale);
}

Then $translator->trans($order->status) works, and templates don't hardcode strings. Symfony 7.4 added StaticMessage for the same idea with a literal string; Symfony 8 removed TranslatableMessage::__toString() — call trans() or getMessage() explicitly or your upgrade will fatal.

Extraction as a CI job, not a manual chore. Wire PhpAstExtractor + ChainExtractor into an Artisan command that scans app/ and resources/views/ for trans()/__() calls, diffs against your source catalogue, and fails the build if new keys appear without translations. This is the single highest-value automation — it's what turns your translation turnaround from weeks into days.

Push/pull to Crowdin/Lokalise via Symfony\Component\Translation\Provider\* when you're past ~50 locales. Note the changelog history here: pushing only the default locale without --force used to replace remote files with partial data (#63231), and very large Lokalise projects hit "Project too big for sync export" (#62348). Pin a version that includes those fixes before going live.

Testing pattern: in unit tests, build a Translator with ArrayLoader and assert on the id, not the rendered string. For integration tests, assert the catalogue has the key in every locale ($catalogue->has($id, $domain)) so missing translations fail CI rather than shipping the raw key.

Gotchas and Tips

  • Missing key = key returned, silently. trans('foo.bar') returns "foo.bar" rather than throwing. Don't rely on exceptions. Add a decorator around TranslatorInterface that logs $translator->getCatalogue($locale)->defines($id, $domain) === false, or run a CI assertion.
  • has() vs fallback. $translator->getCatalogue('de')->has($id) can be false while trans($id, [], null, 'de') still returns a German-ish string — because the catalogue merges fallbacks when loaded. To check native coverage, use $catalogue->defines($id, $domain) on a catalogue loaded without fallback merge, or getCatalogue()->all($domain).
  • No escaping. trans() does no HTML escaping. Use {{ }} in Blade, htmlspecialchars() elsewhere. If a translator writes <script> into a message, {!! !!} will ship it. Treat catalogs as trusted-but-reviewable input.
  • % in message parameters. A literal % in a message is interpreted as a placeholder delimiter; double it (%%) or your message will silently lose text. Same trap with %count% typos.
  • Filename conventions are load-bearing. domain.locale.format (e.g. messages.en.yaml, validators.de.xlf). A file named en.messages.php won't be found and you'll debug the loader for an hour. XliffFileLoader keys by the resname attribute, falling back to <source> — if your vendor exports <trans-unit id="..."> without resname, your keys will be the source strings and every lookup misses.
  • Catalogs are cached; your edits are not. Symfony's ConfigCache/Translator caches compiled catalogues; Laravel's php artisan cache:clear does not touch them, and config:cache doesn't either. Stale translations after a deploy are almost always a cache-dir permissions or warmup issue, not a loader bug.
  • Memory at scale. Loading every domain × locale into one catalogue is fine for 10k strings and painful at 100k × 40 locales. Use a lazy custom loader (DB/Redis) plus a per-locale catalogue cache, and avoid getCatalogue() over the whole tree in loops.
  • CSV quirks. CsvFileLoader choked on empty lines historically (fixed in 6.4.32 / 7.4.4 / 8.0.4) and is sensitive to BOM and quoting. If you accept translator-edited CSVs, pin a patched version and validate on import.
  • Locale string format is not normalized for you. fr_FR, fr-FR, and fr are three distinct keys to the Translator. Normalize at the boundary (Locale::canonicalize()) or you'll get silent fallbacks.
  • Version skew across the 6.4 / 7.x / 8.x lines. Symfony 8 requires PHP ≥ 8.4 and enforces return types component-wide; 6.4 is the LTS and still gets the same bug fixes (see the mirrored 6.4.38 / 7.4.10 / 8.0.10 releases). Pick the line that matches your PHP floor, and if you're on 7.4, note that TranslatableMessage::__toString() is deprecated there and removed in 8.0 — migrate to trans()/getMessage() before jumping.
  • Don't rebuild what exists. Before writing a translator abstraction, check Provider/, Extractor/, Dumper/, Formatter/, and Catalogue/ — the push/pull, extraction, and merge operations you're about to hand-roll are already in the package.
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.
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
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core