goodcat/laravel-l10n
A Laravel localization package for building multilingual apps, with helpers for locale detection, language switching, and localized routes/URLs. Lightweight and easy to integrate into existing Laravel projects.
Updated for v0.5.3 (previous assessment: v0.5.x baseline).
Architecture fit
goodcat/laravel-l10n is positioned as an opinionated localization layer for Laravel — it does not merely supply helpers, it asserts a structure: where translation files live, how the active locale is resolved, how routes are prefixed, and how the app switches languages. The central TPM question remains "what does it own that we would otherwise own ourselves?"
v0.5.3 shifts that boundary in two directions at once, and both matter architecturally:
RedirectToPreferredLocale makes the package responsible for a navigation decision (redirecting a first-time visitor to their best-matching locale), not just a resolution decision. That is a routing-layer behavior with SEO and caching consequences, previously left to the application.SessionLocale in favor of "write your own LocaleResolver" moves session-backed preference storage out of the package and into application code. For a TPM this is the more interesting change: one of the package's opinions is being retired, which reduces lock-in but also reduces the "conventions are the documentation" value proposition. A greenfield team now writes and owns a resolver class — small, but it is now their code to test.The move from a single resolve(): string to resolve(): array plus getPreferredLocales() is a genuine capability improvement for multilingual-expecting users (Accept-Language with a quality-ordered list is now honored rather than truncated to one tag). It is also a change to the package's core contract.
Three fit profiles, revised:
| Profile | Fit | Rationale |
|---|---|---|
| Greenfield Laravel app, 3+ locales planned from day one | Strong | Conventions still cost nothing pre-code; the new multi-locale resolution is closer to correct behavior than the prior single-tag model. Budget a custom resolver for session preference. |
| Existing app with hand-rolled locale middleware | Weak–moderate | Unchanged, and now slightly worse: the package's resolution semantics are mid-flight (list-based), so reconciling with an incumbent SetLocale is a moving target. |
| Inertia / TypeScript frontend via Wayfinder | Moderate, improved | The exported LocalizedRoutes type and fixed TS signature forwarding make this materially more usable in a typed frontend than before. |
| Single-locale app with future i18n ambition | Moderate | Unchanged. |
The architectural bet is still convention over configuration. v0.5.3 is a reminder that the conventions themselves are not yet stable.
Integration feasibility
Greenfield install remains mechanically straightforward: Composer, provider registration, publish config and language stubs, middleware on the web group, localized route wrappers. 1–3 days to a working pilot in one additional locale, plus translation content time.
Two new increments:
RedirectToPreferredLocale: +0.5 day to wire and, more importantly, to test against your proxy/CDN. This middleware is where redirect loops live. It must be sequenced after session start and must not fight an existing Accept-Language redirect at the edge.LocalizedRoutes and re-verify route helper signatures after the argument-forwarding fixes.For existing adopters, v0.5.3 is not a drop-in. Despite the patch-level version number, it contains breaking changes:
| Change | Breakage |
|---|---|
config('app.preferred_locale') → config('app.preferred_locales') |
Any published config, env var, or app code reading the old key silently yields null. No dual-read fallback is described. |
PreferredLocaleUpdated payload: $locale/$previousLocale → $locales/$previousLocales |
Any listener (analytics, cache invalidation, notification preference) breaks or no-ops. |
LocaleResolver::resolve() now may return a list |
Custom resolvers returning string will either break or be coerced; must be audited and retyped. |
SessionLocale deprecated |
Applications relying on it must ship their own resolver before the 1.0 removal. |
Realistic upgrade cost for an app already on this package: 0.5–1.5 engineer-days, dominated by auditing the three consumer surfaces (config, event listeners, custom resolvers) and re-running the locale smoke suite. That is not a blocker — but it is a signal.
The verification burden is unchanged and still the gating factor. Adoption remains negligible; no third party has validated this package across a Laravel major. The spike must still answer route:cache, config:cache, optimize, and Octane request-scoped state — and the new redirect middleware adds a fourth: does the once-per-session check hold under concurrent requests and behind a load balancer?
Technical risk
SessionLocale deprecation shows the maintainer is willing to shed opinions, which lowers the cost of divergence — but it also means the package's surface is a moving target during your integration.Key questions
composer.json constraint, and is the CI matrix exercised across all of them?app.preferred_locale, or is the rename hard? Is the removal of SessionLocale scheduled for 1.0, and is there a published migration guide?RedirectToPreferredLocale relative to StartSession, auth, and any existing locale middleware? Is the once-per-session check session-key based, and what is the key?resolve() returns a list — is the first match wins, or is there fallback chaining across the list? Is it overridable and documented?Vary: Accept-Language, and if so, does it defeat the CDN caching strategy for path-prefixed URLs?route:cache and Octane — is locale state request-scoped, including for the redirect decision?Stack fit
Best fit is a conventional Laravel monolith (Blade or Inertia) on PHP-FPM or Octane, with locale expressed as a URL segment. v0.5.3 strengthens the Inertia + TypeScript case specifically: the exported LocalizedRoutes type and the corrected argument-preserving route helper signatures mean typed route generation with locale prefixes is now a supported path rather than a workaround. If your frontend is React/Vue with Wayfinder, this release is the first one that plausibly fits without hand-written types.
Fit still degrades where localization is content-driven rather than interface-driven. A CMS-backed marketing site needs a TMS, not a routing package. Be explicit: UI string localization versus content localization are different purchases.
Migration path
For existing adopters upgrading to v0.5.3, sequence the breaking changes as one atomic change — do not split config rename from event payload:
config('app.preferred_locale'), PreferredLocaleUpdated listeners, custom LocaleResolver implementations, any SessionLocale usage.SessionLocale now, not later. Write the custom resolver while the context is fresh; the deprecation will become a removal.RedirectToPreferredLocale deliberately, and last. It is additive for greenfield but behavior-changing for anyone with an existing language-detection redirect. Land it behind a flag, verify no loop against proxy/CDN rules, verify crawl behavior.route:cache/config:cache build, Octane mixed-locale load test, hreflang reciprocity check.For greenfield, the prior sequence holds — spike (1 week, timeboxed), contract capture in an ADR, one bounded pilot surface, then broad rollout. Add the redirect middleware to the spike's scope rather than the rollout's.
Compatibility
Verify against five integration surfaces before committing (up from three):
SetLocale — de-duplicate or delete the incumbent.PreferredLocaleUpdated listeners receive lists now.wire:navigate, or a typed SPA consuming LocalizedRoutes — locale must be a first-class, cache-keyed value in each.Vary: Accept-Language unless the redirect middleware genuinely needs it, or hit rates collapse.Sequencing
Localization is a cross-cutting change landing in routing, middleware, events, views, and tests simultaneously. Do not bundle it with a framework upgrade or an auth refactor. This release adds a reason: the v0.5.3 upgrade is itself a small cross-cutting change (config + events + resolvers), so schedule it in its own window with a documented rollback — revert the deploy, restore the prior pin, and the app returns to the prior state.
Maintenance
Four components now, one of which is new:
LocaleResolver in place of SessionLocale, you now maintain a small class with tests. Trivial in size, but it belongs in your codebase review process, not the vendor's.Support
No vendor. Support is the maintainer's responsiveness, your developers' ability to read the source, and the MIT fork escape hatch. v0.5.3 is evidence the maintainer is active — good — and evidence the API is unsettled — less good. Keep the "adopt with a fork budget" posture at 2–3 engineer-days per year, and add to the decision record the specific trigger: if a breaking change lands without a changelog entry during a framework upgrade window, fork.
Scaling
Unchanged for the common case. Two limits to plan for:
Vary: Accept-Language header emitted to support it will fragment your CDN cache across every browser language. Resolve the path-derived-versus-header-derived question explicitly rather than accepting the default.Failure modes
| Failure | Symptom | Detection |
|---|---|---|
| Stale config key after upgrade | Every user gets the default locale; no error raised | Assert resolved locale in a post-deploy smoke test; grep for the old key in code review |
| Event payload mismatch | Analytics/preference listeners silently stop firing | Contract test on PreferredLocaleUpdated; assert listener receives a list |
| Custom resolver type mismatch | TypeError or single-element coercion after resolve() becomes list-returning |
Unit test each resolver; static analysis in CI |
| Redirect loop | Locale middleware and reverse proxy disagree on preferred language | Canary deploy; monitor 3xx rate per route; test first-visit and returning-visit paths |
| Session check under concurrency | Redirect fires more than once per session, or not at all behind a load balancer | Load test with shared session store; assert once-per-session semantics |
| Locale drift in long-lived workers | Octane users see the previous user's language | Load test with mixed locales; assert request-scoped state |
| Silent fallback | Untranslated keys render in the default language in production | CI check for missing keys per locale; per-locale smoke test |
| Route cache staleness | New locale 404s after deploy | Include locale routes in deploy smoke suite |
| SEO regression | Duplicate content across locale URLs, broken hreflang, or unexpected redirects on first crawl |
Crawl diff before/after; validate hreflang reciprocity; monitor crawl budget |
| Upstream abandonment / churn | Package blocks a Laravel major, or a minor breaks the app | Track last commit date, open issue age, CI status, and changelog discipline — review quarterly |
Ramp-up
Developer onboarding stays light — under a day — though the SessionLocale deprecation means a new developer now writes one small resolver class rather than finding a config toggle. That is a marginal increase in required understanding, not a barrier.
The heavier ramp remains organizational: translators need a defined handoff (key extraction, context notes, review step), and product needs a rule for what "translated" means for launch. Neither is provided by the package. Translator workflow is a separate workstream with its own owner, and remains the most common reason localization projects slip.
TPM recommendation posture
Adopt for greenfield or early-stage Laravel work where i18n is a committed roadmap item and the team lacks an i18n specialist — with a stronger case than before for Inertia/TypeScript frontends, and with two new preconditions: pin the exact version and read every changelog, and budget a custom resolver plus the redirect middleware's verification cost in the spike. Do not adopt where locale-in-URL is already publicly established, where a framework upgrade is imminent, or where the organization requires a supported vendor dependency.
Do not upgrade an existing installation to v0.5.3 casually — despite the patch number, it is a breaking change to config, events, and the resolver contract. It is worth doing (multi-locale Accept-Language handling and the TS route types are real improvements), but schedule it as a discrete change window with a rollback, not as a routine composer update.
The composite score remains low, and the release cadence cuts both ways: it demonstrates a maintained package and confirms an unsettled API. You are still the reference implementation — plan the spike accordingly.
How can I help you explore Laravel packages today?