whitecube/lingua
Lingua adds a translation management UI to Laravel: browse and edit PHP and JSON language files in one place, spot missing keys, and keep every locale in sync without hand-editing files. It installs quickly and slots into existing localization workflows.
Install via Composer and rely on Laravel's package auto-discovery (the service provider and Lingua facade are registered automatically in Laravel 5.5+):
composer require whitecube/lingua
The entry point is the WhiteCube\Lingua\Lingua facade. The mental model is a fluent converter: you declare what you have with a fromX() call, then ask for what you want with a toX() call.
use WhiteCube\Lingua\Lingua;
Lingua::from('en')->toIso639_1(); // "en"
Lingua::from('german')->toIso639_1(); // "de"
Lingua::from('de')->toName(); // "German"
Lingua::from('de')->toNative(); // "Deutsch"
Lingua::from('eng')->toIso639_1(); // "en"
First useful use case: normalizing language codes coming from user input, CSV imports, or third-party APIs before persisting them. In a migration, import job, or form request, funnel every value through the package and store a single canonical form (usually ISO 639‑1), so the rest of the app never has to juggle en / eng / English / english.
Publish/consult the config only if you need to override the bundled language dataset:
php artisan vendor:publish --provider="WhiteCube\Lingua\LinguaServiceProvider"
Canonical storage + conversion at the edges. Pick one code format for your database (ISO 639‑1 is the most readable) and convert only at ingest and render time. A small value object or accessor encapsulates this:
protected function languageCode(?string $input): ?string
{
try {
return Lingua::from($input)->toIso639_1();
} catch (\Throwable $e) {
return null; // or log + fallback
}
}
Form select options. Build a language picker by iterating the repository's language list and mapping each entry to [code => native name]. Cache the generated array (see below) — it is static data.
$options = collect($repository->all())
->mapWithKeys(fn ($lang) => [
$lang->toIso639_1() => $lang->toNative(),
]);
Validation rule. Wrap the converter in a custom rule so "German", "de", and "deu" all pass, but "klingon" fails:
public function passes($attribute, $value)
{
try {
Lingua::from($value)->toIso639_1();
return true;
} catch (\Throwable $e) {
return false;
}
}
Accept-Language negotiation. Parse the header, split on ,, strip ;q= weights and any -REGION suffix (the package deals in languages, not full BCP‑47 locales), then use it to match against your supported set. This is where the package earns its keep in a multi-language app.
Locale → language mapping. If you store Laravel locales like de_DE or pt-BR, take the leading segment (de, pt) and feed that to Lingua::from(...). Do not pass the full locale string — the package has no concept of regions.
Caching and DI. Conversion is cheap (in-memory arrays), but the repository is built from static data — bind it as a singleton or cache the derived option arrays with Cache::remember. In tests, resolve the facade directly rather than rebuilding per call.
from(). It guesses the source format from the string. "en" is unambiguous, but some 3-letter strings collide across ISO 639‑2/639‑3, and some names match multiple entries. Where it matters, use the explicit fromIso639_1() / fromIso639_2() / fromIso639_3() / fromName() helpers instead of the auto-detecting from().ger/deu, fre/fra, dut/nld are the same language with two valid ISO 639‑2 forms. Decide which your app emits and be consistent; libraries and legacy systems will send the other one.toIso639_1() on those will fail or return empty — guard it and fall back to the 3-letter code.pt-BR vs pt-PT, zh-Hans vs zh-Hant, and sr-Latn are outside its scope. Keep region handling in your own layer (or a dedicated locale package) and only use Lingua for the language subtag.trim, and be mindful the dataset is mostly lowercase) before calling. Non-ASCII native names (Deutsch, 日本語, Français) are returned as-is — ensure your DB column and JSON responses are UTF‑8 (utf8mb4).tryFrom()-style nullable API. Wrap conversions, especially anything driven by request data, and log failures rather than letting a 500 escape from a form field.How can I help you explore Laravel packages today?