laravel-lang/translator
laravel-lang/translator is a Laravel package for streamlining translation of language files. It provides tooling to generate and manage locale strings, helping maintain multilingual applications and keep translations consistent across supported languages.
This is an alpha package (1.0-ALPHA6), so "getting started" means reading the source before reading the docs — the README is thin, and the release notes for this version only advertise Laravel 12 / dependency hygiene.
1. Install (pinned, alpha stability)
composer require laravel-lang/translator:"1.0.0-alpha6" --with-all-dependencies
If Composer refuses the constraint, add "minimum-stability": "alpha" plus "prefer-stable": true in composer.json rather than globally loosening stability. Pin the exact version — alpha releases in this repo have already churned illuminate/*, pestphp/pest, and symfony/var-dumper constraints between tags, and there is no documented upgrade path.
2. Find the config and the contract first
Before writing any code, look at these three things in the vendor directory:
config/translator.php — the driver list, default driver, and (likely) source/target locale defaults.src/Contracts/ — the translator interface; this tells you what every driver returns and how errors surface.src/Drivers/ — which providers actually exist (Google / DeepL / Yandex / Bing-style HTTP drivers) and what credentials each expects.Publish the config if the package registers a publish tag:
php artisan vendor:publish --tag=translator-config
php artisan list | grep -i translat # confirm the exact artisan signature, if one ships
3. Credentials in .env, not in config
TRANSLATOR_DRIVER=google
TRANSLATOR_GOOGLE_KEY=xxxxxxxx
TRANSLATOR_SOURCE_LOCALE=en
TRANSLATOR_TIMEOUT=15
Keep the API key out of config/translator.php and reference it via env() in the config file so config:cache behaves correctly.
First use case: pick one low-risk locale (e.g. de) and one file (lang/en/auth.php), translate it, and diff the result against the English source by hand. Do this before wiring anything into a pipeline — it is the fastest way to learn whether placeholders and plural pipes survive the round trip.
Direct service use for one-off strings
Resolve from the container so the configured driver is respected; don't new a driver class directly or you bypass config and HTTP client binding.
use LaravelLang\Translator\Contracts\Translator;
$translator = app(Translator::class);
$de = $translator->translate('The :attribute field is required.', 'de', 'en');
Exact method names vary by alpha — confirm against src/Contracts/Translator.php before committing code.
File-level workflow (the day-to-day pattern)
Treat translation as a diff-generating operation, not an in-place mutation:
$source = require lang_path('en/validation.php'); // nested arrays
$target = file_exists(lang_path("{$locale}/validation.php"))
? require lang_path("{$locale}/validation.php")
: [];
foreach (Arr::dot($source) as $key => $value) {
if (blank($target[$key] ?? null)) { // only fill gaps
$target[$key] = $translate($value);
}
}
file_put_contents(
lang_path("{$locale}/validation.php"),
'<?php return '.var_export($target, true).';'
);
Key properties of this pattern:
Protect placeholders before the call
Machine translators will happily localise :attribute into something unusable. Wrap substitution tokens in an opaque sentinel before sending and restore afterwards:
$guarded = preg_replace_callback('/:[a-z_]+/i', function ($m) use (&$map) {
$token = '__PH'.count($map).'__';
$map[$token] = $m[0];
return $token;
}, $value);
$translated = strtr($translate($guarded), $map);
abort_if(
preg_match_all('/:[a-z_]+/i', $value) !== preg_match_all('/:[a-z_]+/i', $translated),
500, 'Placeholder count mismatch'
);
Same idea for {name}, %s, @, and HTML tags (<a href=":url">).
Split plural variants before translating
Laravel's trans_choice uses | separators, and locales need different segment counts (2 for en/de, 3 for ru/pl, 6 for ar). Never send "one apple|many apples" as a single sentence.
$variants = explode('|', $value);
$translated = implode('|', array_map($translate, $variants));
Then assert substr_count($result, '|') === substr_count($value, '|').
Queue it for anything bigger than a few hundred keys
final class TranslateLocaleFile implements ShouldQueue
{
use Queueable;
public int $tries = 3;
public int $backoff = 30;
public int $timeout = 120;
public function __construct(
public string $locale,
public string $file,
) {}
public function handle(): void { /* per-file loop above */ }
}
Provider APIs are rate-limited and billed per character. One job per file (not per locale, not per string) gives you natural back-pressure and a resumable unit of work.
Cache aggressively
$key = 'tr:'.md5($driver.$from.$to.$source);
$result = Cache::remember($key, now()->addMonth(), fn () => $translator->translate(...));
This is the single biggest cost lever: validation.php and auth.php change rarely, and re-running a whole-locale backfill should be nearly free after the first pass.
Fake the translator in tests
Bind a stub implementation of the contract in the test container that prefixes input (e.g. "de:$text") and leaves placeholders intact. Your tests then assert file structure and placeholder integrity, not translation quality — which is the only thing you can actually assert.
Alpha means the API moves. There is no stable 1.x, no documented BC policy between alpha tags, and the last release notes are entirely dependency updates. Pin the exact version, commit the vendor diff review to your PR checklist, and keep the package behind your own thin adapter class so a rename in src/ costs you one file, not fifty.
Laravel 12 is the only signal in this release. illuminate/* constraints were bumped for Laravel 12 and Pest/symfony/var-dumper were refreshed. If you are on Laravel 10/11, prove resolution with composer why-not laravel-lang/translator before assuming backwards compatibility — the release notes do not claim it.
config:cache bites you on credentials. Changing TRANSLATOR_DRIVER or a key in .env does nothing until you php artisan config:cache (or config:clear) in cached environments. This is the #1 "why is it still using Google?" cause.
Locale code mismatch. Providers want pt-BR, zh-CN, pt_BR, or plain pt depending on the vendor, and Laravel locales are often pt_BR. Normalise explicitly in your adapter (str_replace('_', '-', $locale)) rather than letting the driver guess. A wrong code often returns the source text unchanged instead of throwing — silent failure.
Untranslated-but-successful responses. Several free/cheap endpoints return HTTP 200 with the input echoed back, or with the text truncated at a character limit. Assert $result !== $input for non-cognate locales and log a warning when they match, otherwise you commit English into lang/de/ and nobody notices.
JSON vs PHP lang files need different writers. For lang/de.json, encode with JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT; for PHP files, prefer var_export over hand-rolled string building so apostrophes and backslashes in translated text don't produce syntax errors. Always php -l each generated file in CI.
Ordering churn. Laravel's own lang files are alphabetically sorted; a naive Arr::dot round-trip can reshuffle keys and produce a 900-line diff for a 12-string change. Sort keys on write (ksort per level) to keep diffs reviewable.
Don't translate vendor/ lang files. Those are published by laravel-lang/publisher and overwritten on update. Target lang/ (or whatever lang_path() resolves to) only.
Timeouts and retries belong in the HTTP client. Set Http::timeout(15)->retry(3, 200) in your driver wrapper rather than around the whole job; a single hung request inside a 40-file loop will otherwise kill the run with no partial progress recorded.
Extension point: custom driver. If your provider (or an internal MT service) isn't in src/Drivers/, implement the package's translator contract and register the class in config/translator.php under the drivers map, then select it by name via TRANSLATOR_DRIVER. That is the intended extension seam and is far safer than forking.
Debug by logging the raw payload. Add a TRANSLATOR_DEBUG=true branch that logs {driver, from, to, source_hash, response_hash, http_status} — not the full text, to avoid leaking customer strings into logs. When a locale suddenly regresses, the driver + status tuple is usually enough to identify quota exhaustion versus a real API change.
Human review is not optional. Machine output should land on a branch, get diffed per locale, and be reviewed by someone who reads the target language — at minimum for auth, passwords, and validation, where a mistranslation changes security-facing UX. Treat the package as a first-draft generator, not an authoritative localiser.
How can I help you explore Laravel packages today?