laravel-lang/lang
Laravel Localizations provides comprehensive language files for Laravel, giving your app ready-made translations for validation, auth, and framework messages across dozens of locales. Install via Composer and ship multilingual apps faster.
laravel-lang/lang is not a service-provider package — it ships no PHP classes, no config, no auto-discovery. It is a data repository: ~126 locale folders of auth.php, passwords.php, pagination.php, validation.php, validation-inline.php, plus a {locale}.json, and (since v10) split directories for the wider ecosystem (jetstream, fortify, breeze, cashier, nova, spark-paddle, spark-stripe).
Minimal setup
composer require --dev laravel-lang/lang
It is a dev dependency by design: you consume its files at development/build time and commit the result into your own lang/ directory. Nothing reads from vendor/laravel-lang/lang at runtime.
Where to look first
vendor/laravel-lang/lang/locales/ — the actual translated files. Confirm the exact folder name before wiring it to app()->setLocale(); codes are pt_BR, zh_CN, sr_Latn, not BCP-47 pt-BR.vendor/laravel-lang/lang/source/ — the English master keys. This is your reference for "does a key exist for this new validation rule yet?"First use case: add French to a Breeze app.
cp -r vendor/laravel-lang/lang/locales/fr lang/fr
cp vendor/laravel-lang/lang/locales/fr.json lang/fr.json
// .env
APP_LOCALE=fr
APP_FALLBACK_LOCALE=en
Then flip a request at runtime with App::setLocale('fr') and watch the login/validation/pagination strings change. If you'd rather not hand-copy, the companion laravel-lang/publisher package wraps this in artisan commands (e.g. lang:add, lang:update) — check the current docs for exact names, since the commands have evolved.
Publish once, own it forever. The idiomatic workflow is: copy the locales you need into lang/, commit them, and treat them as project files. That gives you diffs, code review, and the ability to patch a bad machine translation.
Runtime locale switching. A middleware reading a route segment or a user column is the common shape:
// app/Http/Middleware/SetLocale.php
public function handle($request, Closure $next)
{
$locale = $request->route('locale')
?? $request->user()?->locale
?? config('app.locale');
if (in_array($locale, config('app.available_locales'), true)) {
App::setLocale($locale);
}
return $next($request);
}
Keep the allow-list in config/app.php (or a dedicated config/localization.php) and validate against it — never pass a raw request value into setLocale().
Two different translation namespaces. PHP files under lang/fr/ are for framework messages; lang/fr.json is the flat English-string→translation map used by __('Welcome back') in your Blade templates. You will almost always need both, and forgetting the JSON file is the most common "why is only half my UI translated?" bug.
Override per-form attribute names. The attributes array at the bottom of validation.php turns "The email field is required." into "The work email address field is required." This is the highest-value customization in the whole package and the one developers skip.
Package-namespaced strings. Fortify, Jetstream, Nova and Cashier resolve translations through lang/vendor/{package}/{locale}/…. Publish those with the package's own vendor:publish after copying the laravel-lang subdirectory into place, otherwise auth emails stay English.
Key-parity tests. Because the package is a moving target, add a cheap test that walks lang/en (or source/) and asserts every key exists in each shipped locale:
public function test_locales_have_all_keys(): void
{
$reference = Arr::dot(require lang_path('en/validation.php'));
foreach (config('app.available_locales') as $locale) {
$candidate = Arr::dot(require lang_path("$locale/validation.php"));
$missing = array_diff_key($reference, $candidate);
$this->assertEmpty($missing, "Missing in $locale: ".implode(', ', array_keys($missing)));
}
}
Run it in CI, not on every request.
Incremental updates. After a Laravel minor upgrade introduces new validation rules, bump the dev dependency and re-run the publisher (or re-copy) — but only for files you actually use. Publishing everything on every upgrade creates noisy PRs where 40 locales change because one contributor fixed a typo in Afrikaans.
Trim what you ship. If you support three locales, don't vendor 126. Copy only what's in your allow-list and let the publisher handle the rest.
pt_BR and zh_CN use underscores and script suffixes. Map them to BCP-47 (pt-BR) at the boundaries — <html lang>, Open Graph tags, Carbon::setLocale() — and keep the underscore form for lang/ paths and app()->setLocale().en locale. English is Laravel's built-in default (and the source/ directory). Don't try to publish en; you'll get an empty folder.vendor/. Any publisher re-run silently overwrites your edits. Put custom strings in lang/{locale}/custom.php and merge, or keep an explicit patch list under version control.validation.declined. If you see dotted keys in the UI, your language files predate the validator — bump and republish.--no-dev breaks deploy-time publishing. Since it's a dev dependency, a production container built with composer install --no-dev has no vendor/laravel-lang/lang. Publish and commit during development; never rely on publishing at deploy time.validation.php vs validation-inline.php. The inline variant omits attribute names; some Laravel versions no longer use it. Publishing one but not the other leaves gaps depending on which translator your validation path hits.trans_choice where English needs two. Translators get this wrong; trans_choice on a two-segment Russian string produces grammatically wrong output.ar, he, fa, ur are translated but nothing sets dir="rtl" or flips Tailwind logical properties. The package gives you text, not layout.source/ + locales/{locale}; v10 split keys per Laravel package. Pin the major version and read the upgrade notes before composer update, or your publish script will look in a directory that no longer exists.php artisan lang:publish. Laravel's built-in command only scaffolds the English lang/ directory. The locale content comes from this package (and its publisher).__() in a local-only helper that logs when the returned value equals the input key. It surfaces untranslated strings across an entire click-through far faster than eyeballing each screen.validation.php attributes and custom, and your own lang/{locale}/*.php for domain vocabulary. There are no service-container bindings, events, or config keys to hook into; if you need programmatic behavior, the publisher package is the layer that provides it.How can I help you explore Laravel packages today?