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

orchestra/translation

Orchestra Translation gives Laravel developers a flexible foundation for managing multilingual applications, with translation storage, locale handling, and localization utilities that integrate smoothly into Orchestra Platform workflows.

View on GitHub
Deep Wiki
Context7

Getting Started

Verify the environment before writing a line of code. orchestra/translation is a component of Orchestra Platform, not a general-purpose Laravel library. Check your composer.json lockfile for orchestra/foundation, orchestra/support, and orchestra/memory. If they aren't present, do not install this — read the "port the pattern" note under Gotchas instead. Installing it on plain Laravel gives you a translator override plus migrations that assume an admin surface (Orchestra extensions, ACL) you don't have.

Minimal steps (Orchestra app, Laravel 8):

composer require orchestra/translation:^6.0
php artisan vendor:publish --provider="Orchestra\Translation\TranslationServiceProvider"
php artisan migrate

Where to look first, in this order:

  1. composer.json — v6.0.0 pins to Laravel 8. Confirm the orchestra/* peer constraints resolve against your lockfile before anything else. Constraint resolution, not code, is the usual blocker.
  2. The service provider — this is the whole story. Determine whether it rebinds translator / translation.loader in the container, and at what boot order relative to Laravel's own TranslationServiceProvider.
  3. migrations/ — the schema communicates the data model faster than any (absent) documentation will.
  4. The loader class implementing Illuminate\Contracts\Translation\Loader — this is where __() actually resolves from the database.
  5. Config file — fallback locale, and whether locale enablement is config-driven or DB-driven.

First use case to validate the integration (30 minutes, throwaway branch): seed a locale and one key, then prove the override intercepts:

// tinker
app('translator')->get('auth.failed', [], 'fr');
// compare against config('app.locale') fallback behaviour

Then write to the DB, read again in the same process, and read again in a fresh process. If the fresh read returns the old value, you've discovered the cache-invalidation behaviour on day one instead of in production.

Implementation Patterns

Treat __() as the contract, everything else as plumbing. The entire value proposition is that existing __('messages.welcome') calls in Blade and controllers keep working but now resolve through the database. Do not introduce a parallel helper or a repository call in views — that's how you end up with two translation systems and no single source of truth.

Give non-engineers an edit surface. The product argument is "marketing edits copy without a ticket." In Orchestra that means wiring the translation model into an extension resource: list, inline edit, save. Keep the form dumb — key, locale, value, save. Validate the value (length, no accidental HTML) and make the key read-only after creation; renaming a key silently breaks every __() call referencing it, and there is no static analysis to catch you.

Make the fallback chain explicit and test it. Use one chain and document it: requested locale → app fallback locale → the key itself. Then deliberately test the middle case:

// fr exists, key missing in fr, present in en
app()->setLocale('fr');
__('messages.checkout_title'); // must return the en value, not "messages.checkout_title"

Anywhere a missing translation renders the raw key to a customer, you have a visible defect. Add a test per locale for your top 20 keys.

Seed from existing lang/ files — don't hand-enter strings. Write a one-shot importer that walks resources/lang/{locale}/*.php, flattens with Arr::dot(), and upserts into the translation table. Run it from a migration or an artisan command, commit the command, and keep it re-runnable — you'll want it again when someone adds keys via PR instead of the admin.

Cache discipline on write. Wrap writes so invalidation is not optional:

DB::transaction(function () use ($payload) {
    $translation = Translation::updateOrCreate([...], [...]);
    Cache::forget("translation.{$translation->locale}");
    // or: app('translator')->setLoaded([]) if the loader memoizes
    return $translation;
});

Put this in the model's saved/deleted events rather than in controllers, so admin edits and importer runs both get it.

Per-tenant or per-environment overrides. If one key needs to say different things for different customers, do not fork the package's schema. Add a nullable tenant_id (or group) scope and extend the loader's lookup — a handful of lines — and the fallback chain stays intact. Forking the schema makes every future Laravel bump a merge conflict.

Testing. Because resolution sits in the request path, regress-test at the __() level, not the model level. A small helper asserting [locale, key, expected] triples against a seeded SQLite database catches 90% of the breakage from a major bump.

Gotchas and Tips

The version number is a function of Laravel's version, not this package's feature set. v6.0.0's entire changelog is "support for Laravel Framework v8." There's no 5.x branch with Laravel 8 backported. So: on Laravel 7 you can't take it; on Laravel 8 you take it and are now coupled to raising both majors together forever. Budget that as recurring work.

Loader registration order is where the integration silently fails. Laravel's default loaders (FileLoader, DatabaseLoader) are registered in the framework's TranslationServiceProvider. If your provider boots at the wrong point, you either get no override (files win, your admin edits appear to do nothing) or an override that shadows a loader you still wanted. Symptom: edits save fine to the database but __() returns the old lang/ value. Diagnose with app('translation.loader') in tinker and inspect the loader array order.

Framework-level translation cache is not your cache. Laravel memoizes loaded groups per request and, in some configurations, caches compiled translations. Clearing your own cache key is insufficient if the framework has already loaded the group in that process. If you're editing and testing in the same request lifecycle, you will chase a phantom bug — verify in a fresh process before concluding the write path is broken.

There is no documentation corpus; the source is the spec. No upgrade guide exists for a major with breaking changes implied by a framework bump, and the changelog is one line. Before upgrading, diff the tag and read the provider and loader line by line. It's a small surface area; an hour of reading beats a day of debugging.

Pluralization and ICU are out of scope. This resolves keys; it does not do trans_choice beyond whatever the loaded value contains, and it doesn't do ICU message format. If your product needs plural rules for Slavic or Arabic locales, that's symfony/translation's message formatter or a TMS — don't try to bolt it on here.

Missing-key observability is your responsibility. Add a listener or wrapper that logs __() calls returning the raw key in non-production. Without it, gaps appear as odd-looking English in the UI rather than as alerts.

Extension point: implement LoaderInterface yourself if the shape doesn't fit. The clean seam is the loader contract. If the package's model doesn't match your tenancy model, keep the package's migrations out entirely and write a ~100-line loader against your own table, registered on the container. You get the same day-to-day ergonomics (__() just works) without inheriting the schema or the Orchestra coupling on the data layer.

Bus factor, practically. One maintainer, zero dependents, no issue triage to lean on. Before this goes anywhere customer-facing, name whoever owns it internally, pin the exact version, and vendor or fork it so a Packagist yank or an abandoned orchestra/* peer doesn't take your build down. If you can't name that owner, implement the loader pattern yourself — it's short, and you own the upgrade path.

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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource