caouecs/laravel-lang
Laravel Lang provides ready-made translation files for Laravel's validation, authentication, password reset, and pagination messages in dozens of languages. Drop in the locales you need and your app speaks your users' language out of the box.
caouecs/laravel-lang bundles ready-made translation files (validation, auth, passwords, pagination, and JSON attribute names) for ~80 locales. It is not a runtime library you call — it is a source of files you publish into your app's lang/ directory.
1. Install with a version matching your Laravel release. Version alignment is strict; a mismatch silently gives you keys Laravel no longer uses:
# Laravel 9/10/11 — use the maintained successor
composer require laravel-lang/common --dev
# Legacy Laravel 5.x/6.x/7.x
composer require caouecs/laravel-lang:^3.0 --dev
2. Look first at what you actually have. Before writing any code, inspect an existing locale file:
ls vendor/caouecs/laravel-lang/src/fr # or vendor/laravel-lang/lang/locales/fr
cat vendor/caouecs/laravel-lang/src/fr/validation.php | head -40
You will see the exact key shape Laravel's validator expects: required, email, between.numeric, custom.*, and the attributes array.
3. Publish one locale, not all of them. Shipping 80 locales bloats your repo and makes diffs unreadable:
# modern laravel-lang/common
php artisan lang:add fr
# legacy packages
cp -R vendor/caouecs/laravel-lang/src/fr resources/lang/fr
4. Wire it up and verify. config/app.php → 'locale' => 'en', 'fallback_locale' => 'en'. Then prove the wiring works end to end:
Route::get('/probe', function () {
app()->setLocale('fr');
return trans('validation.required', ['attribute' => 'email']);
});
Once that returns "Le champ email est obligatoire.", you have the whole pipeline: file on disk, locale resolved, trans() hitting your app copy rather than vendor.
Your first real use case is almost always translating login and registration error messages — the default en strings look unfinished to non-English users, and validation.php + auth.php cover 90% of what a fresh Laravel app emits.
Per-user locale via middleware. The canonical pattern: persist the locale on the user (or session), then set it before the request hits your controllers.
class SetLocale
{
public function handle($request, Closure $next)
{
$locale = $request->user()?->locale
?? $request->session()->get('locale')
?? config('app.locale');
if (in_array($locale, config('app.available_locales'))) {
app()->setLocale($locale);
}
return $next($request);
}
}
Register it in the web group before ShareErrorsFromSession, otherwise flashed validation errors render in the previous locale.
Locale-prefixed routes. For public-facing multilingual sites, put the locale in the URL and derive app()->setLocale() from the route parameter. This is worth doing over session-based switching because it makes pages cacheable and shareable.
Route::group(['prefix' => '{locale}', 'middleware' => 'setlocale'], function () {
Route::get('/', HomeController::class);
});
Keep published files in version control, and edit them there. The single most important habit: your lang/ directory is your code, not vendor's. Add project-specific strings (auth.failed, passwords.sent, your domain validation messages) directly in the published copy. Never edit vendor/.
Extend validation.attributes for your domain. Laravel's generic attribute names produce awkward output like "The first name field is required." Map them:
// lang/fr/validation.php
'attributes' => [
'email' => 'adresse e-mail',
'password' => 'mot de passe',
'billing.postal_code'=> 'code postal de facturation',
],
Dot notation works and is the cleanest way to cover nested form fields without renaming inputs.
Layer custom messages over the package defaults. Use custom in validation.php for per-field rule overrides, and a lang/{locale}/app.php for your own UI strings. Keep the package file's structure intact so lang:update can merge cleanly.
CI: add locales declaratively, not by hand.
// composer.json
"scripts": {
"post-install-cmd": ["@php artisan lang:add en fr de --force"]
}
Or, if you commit the files, add a CI check that runs lang:update and fails on a dirty diff — this catches missing keys after a Laravel upgrade.
Testing. Use $this->app->setLocale('fr') in a test case or withSession(['locale' => 'fr']), then assert on translated strings. Do not assert on hardcoded English — it silently passes under fallback_locale even when a translation is missing.
Vendor namespaced translations. If you publish into a package namespace, use __('mypkg::validation.required'). For app code, plain __('validation.required') is correct and hits lang/{locale}/validation.php.
The package is effectively superseded. caouecs/laravel-lang moved to laravel-lang/lang and then to laravel-lang/common. The original repo is archived. For anything Laravel 9+, use laravel-lang/common; pinning the old package is a maintenance liability, and installing both causes duplicate-file conflicts.
Version pinning is not optional. caouecs/laravel-lang 3.x targets Laravel 5.5–5.8. Using it on Laravel 10 gives you validation.php without keys for prohibited_if, password.letters, decimal, and the newer accepted_if family — those render as raw keys in your UI. Always run lang:update (or re-publish) after any Laravel minor upgrade.
lang:add is add-only; lang:update is merge. lang:add skips locales that already exist. If you already have lang/fr/, adding de works but re-running lang:add fr does nothing — use lang:update to pull new keys into an existing locale. Both need --force in non-interactive CI.
Locale code casing bites on Linux. pt_BR, zh_CN, pt-br, and pt_BR are four different directory names. macOS is case-insensitive and hides the bug; production Linux is not. Standardize on Laravel's xx_YY convention and validate against an allowlist rather than trusting user input.
fallback_locale masks missing translations. A missing French key returns the English string with no error. During development, temporarily set 'fallback_locale' => null and run your test suite — it surfaces every gap as a raw key. Turn it back on before shipping.
Pluralization rules are the deepest trap. Slavic and Arabic locales need 3–4 plural forms, and Laravel 8+ supports range syntax in the string: {0} aucun article|[1,4] :count articles|[5,*] :count articles. The package ships these correctly — but only if you use trans_choice(), not trans(), and only if your custom strings follow the same pipe-separated format. Mixing them produces off-by-one plurals that nobody notices until a native speaker signs off.
JSON vs PHP files serve different jobs. lang/{locale}/validation.php is keyed (required → message). lang/{locale}.json is source-string keyed ("Email" → "E-mail"). The package provides both. Attribute names for @error-style UI text belong in the JSON file; validator messages belong in the PHP file. Putting them in the wrong one fails silently.
Deployment caching. Laravel has no separate translation cache, but config:cache captures app.locale and fallback_locale — after changing them you must re-run php artisan config:cache. On OPcache-enabled hosts, changed lang/ files need opcache_reset() or a graceful PHP-FPM reload. Don't generate lang/ at deploy time on immutable filesystems without a writable build step; commit the files instead.
Extension points to prefer over forking: a small Translator decorator bound to translator if you need to log missing keys, and a Lang facade fake in tests. Avoid subclassing FileLoader — the surface area is larger than it looks and lang:update assumes the default loader layout.
Debugging missing keys: php artisan tinker → app('translator')->get('validation.required') tells you which loader resolved it. Lang::has('validation.required') returning false while the file exists almost always means a locale-code mismatch or a stale config:cache.
How can I help you explore Laravel packages today?