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.
What this actually is. Despite the "Laravel package" framing, orchestra/translation is a component of Orchestra Platform, not a general-purpose localization library. Its job is narrow and real: it rebinds Laravel's translation loader so that __() and friends resolve from a database table instead of resources/lang/*.php, and it ships the CRUD/admin surface for editing those rows. The user-visible contract — existing __('messages.welcome') calls keep working — is the entire value proposition. Everything else is plumbing.
Architecture fit. Two viable fits, and only two:
orchestra/foundation, orchestra/support, orchestra/memory in the lockfile. Integration is config plus migrations plus the extension/ACL wiring, and the admin UI comes largely for free. This is the intended path.TranslationServiceProvider registration order. Outside Orchestra this is a liability.Integration feasibility. The happy-path commands are trivial (composer require, vendor:publish, migrate), but the real gate is constraint resolution, not code. v6.0.0's entire changelog is "Update support for Laravel Framework v8." That makes the package's version number a function of Laravel's major, not of its own feature set. If you're on Laravel 6 or 7, there is no backport path — adopting this version moves your framework floor. If you're on Laravel 8, you're now coupled to raising both majors together, indefinitely.
Technical risk — the material ones:
TranslationServiceProvider, you get either no override (files win, admin edits appear to do nothing) or an override that shadows a loader you still needed. The symptom is "edits save fine but __() returns the old value" — a bug that looks like a data problem and is actually a boot-order problem.messages.checkout_title to a customer, that's a visible bug. There is no built-in observability for this — logging raw-key returns in non-production is your responsibility.symfony/translation's formatter or a TMS; bolting it on here is out of scope by design.orchestra/* peer takes your build down with no fallback.Key questions to resolve before committing:
orchestra/* already in our lockfile, at a version pairing with Laravel 8? (If no — stop; the answer is "port the pattern," not "adopt the package.")lang/ files satisfy the actual roadmap requirement?Stack fit. Orchestra Platform on Laravel 8, full stop. The integration surface to audit is small and enumerable, which is the good news: the service provider (does it rebind translator / translation.loader, and in what order), the migrations (schema is the fastest read of the data model), the Loader implementation (where __() actually resolves), and the config (whether locale enablement is config- or DB-driven). An hour of reading the source beats a day of debugging it, and there is no documentation corpus to substitute for that reading.
Validation spike — timeboxed, before any roadmap commitment. Roughly 30 minutes on a throwaway branch: seed one locale and one key, then prove the override actually intercepts app('translator')->get('auth.failed', [], 'fr'). Then write to the DB and read again in a fresh process. If the second fresh read returns the old value, you've found the cache-invalidation behavior on day one rather than in production. This single test is the entire go/no-go.
Migration path from existing language files. Do not hand-enter current strings. Write a one-shot, committed, re-runnable artisan importer that walks resources/lang/{locale}/*.php, flattens with Arr::dot(), and upserts into the translation table. It will be run more than once — someone will add keys via PR instead of the admin — so treat re-runnability as a requirement, not a nicety.
Compatibility discipline.
composer.json. Do not use a caret range against a single-maintainer, zero-dependent package.Sequencing. Two orderings, choose one deliberately:
LoaderInterface implementation plus migrations and an admin form). Same day-to-day ergonomics (__() just works), no Orchestra coupling on the data layer, and you own the upgrade path.Either outcome is valuable: it turns an unknown into a budget line.
If you integrate, hold these lines:
__() as the single contract. Do not introduce a parallel helper or repository call in views — two translation systems and no source of truth is the failure state.__() call referencing it, with no static analysis to catch it.saved/deleted events rather than controllers — admin edits and importer runs both need it.Maintenance. This is the dominant cost and it is structural, not incidental. The package's version is the framework's version, so maintenance load arrives in discrete major-version events rather than continuous small patches: constraint resolution, a lockfile exercise, and re-validation of the loader override and fallback chain. Estimate it as recurring budget on the roadmap. There is no upgrade guide to lean on; the source is the spec.
Support and bus factor. One maintainer, zero dependents, no issue-triage surface to lean on. Before this goes anywhere customer-facing, name the internal owner, pin the version, and vendor or fork it. If you cannot name that owner, the correct answer is to implement the loader pattern yourself — it is short, and you own the upgrade path. This is a defensible trade for an internal tool or MVP with a named owner; it is not defensible for a customer-critical path without one.
Scaling. The resolution path is now a database read, on every __() miss through the loader. That is fine at modest scale and requires deliberate caching above it, with invalidation on write (see above). Two scaling levers matter and both are cheap if done early: a cache keyed by locale, and a nullable tenant/group scope on the existing table rather than a schema fork.
Failure modes to instrument for:
__() returns stale file values. Diagnose with app('translation.loader') and inspect the loaders array order.Ramp-up. The package's own surface is small — a provider, a loader, a handful of migrations, a config file. The expensive part is the surrounding context: Orchestra's extension model and ACL conventions for the admin surface, and Laravel's translation loader registration order. Plan for an engineer who knows Laravel to spend their time on Orchestra, not on this package.
Product-side ramp-up is the real operational question. The entire product argument is "marketing edits copy without a ticket." That only holds if the editing surface is genuinely usable by non-engineers and if key hygiene is enforced by the tool (read-only keys, validation on values). If translators still need an engineer to add a key or fix a fallback, the velocity gain evaporates and the maintenance cost stands alone.
Regression coverage. Because resolution sits in the request path, test at the __() level, not the model level. A small helper asserting [locale, key, expected] triples against a seeded in-memory database — plus a deliberate test for the middle of the fallback chain (key exists in en, missing in fr, locale is fr) — catches the large majority of breakage from a framework bump. Twenty keys per locale is a reasonable starting corpus.
How can I help you explore Laravel packages today?