vildanbina/laravel-auto-translation
Automatically translate Laravel model attributes and language files with a simple trait. Supports multiple drivers like Google, DeepL, and OpenAI, plus caching and queues for fast, seamless multilingual applications.
What this release is: 2.2.2 is a compatibility patch — the only change is Laravel 13 support (illuminate/* ^13 added to the constraint set). No API changes, no deprecations. Upgrading from 2.2.1 on Laravel 10–12 is a no-op.
Version to require:
composer require vildanbina/laravel-auto-translation:^2.2.2
On Laravel 13 you must be on 2.2.2 or later — earlier releases will fail Composer resolution against illuminate/support ^13. Confirm the PHP floor for your Laravel version (Laravel 13 requires PHP 8.3+) before bumping the framework.
Setup order:
php artisan vendor:publish --tag="auto-translation-config"
config/auto-translation.php first — it is the whole package. Everything (driver, source/target locales, queue behaviour, enable flag) is decided there.AUTO_TRANSLATION_ENABLED=true
AUTO_TRANSLATION_DRIVER=google # google | deepl | openai | ...
AUTO_TRANSLATION_API_KEY=...
src/Drivers/ (one class per provider) and src/Contracts/ (the interface a custom driver implements) before writing model code — the contract tells you the supported options per provider.php artisan list auto-translation — the package registers commands for bulk/backfill translation.First use case — translate a Product on save:
use Illuminate\Database\Eloquent\Model;
use Vildanbina\AutoTranslation\Concerns\HasAutoTranslation;
use Vildanbina\AutoTranslation\Contracts\AutoTranslatable;
class Product extends Model implements AutoTranslatable
{
use HasAutoTranslation;
// Attributes the package should translate (check HasAutoTranslation
// for the exact hook name in your installed version — it is the
// single method to override).
protected array $translatable = ['name', 'description'];
}
Create a Product with name set in your source locale, then inspect the raw attributes ($product->getRawOriginal('name') or your translations table) to confirm the target locales were populated. If nothing happened, the model is not resolving as AutoTranslatable, or AUTO_TRANSLATION_ENABLED is false in that environment.
Sync on save vs. queued. Inline translation blocks the HTTP request for the duration of N API calls (locales × attributes). For anything user-facing, move it to the queue:
// config/auto-translation.php
'queue' => [
'enabled' => true,
'connection' => 'redis',
'queue' => 'translations',
],
Run a dedicated worker so a slow/rate-limited provider can't starve your default queue.
Only translate what changed. The expensive mistake is re-translating the whole model on every save() when only price changed:
public function shouldAutoTranslate(): bool
{
return $this->isDirty(['name', 'description'])
&& app()->getLocale() === config('app.fallback_locale');
}
Gate on dirty source attributes and skip when the target locale already has a value, otherwise editing a typo in name re-bills the translation of description.
Backfill existing rows. After adding the trait, existing records have no translations. Use the package command for the bulk pass, then chunk with a rate-limited job:
Product::query()->whereNull('name->es')->chunkById(200, function ($products) {
foreach ($products as $product) {
TranslateProductJob::dispatch($product)->onQueue('translations');
}
});
Add ->middleware(new RateLimited('translations')) to respect provider quotas rather than getting 429s mid-run.
Custom / self-hosted provider. Implement the driver contract in src/Contracts/, register it in the drivers map in config, then point AUTO_TRANSLATION_DRIVER at your key. This is the cleanest way to use an internal translation service, a LibreTranslate instance, or a mocked driver for local dev.
Hook into completion. Use the package's events (grep src/Events/) to purge cache, reindex in Scout, or notify an editor — don't pollute the model with that logic:
Event::listen(TranslationCompleted::class, fn ($e) => Cache::forget("product:{$e->model->id}"));
Testing. Never let tests hit a real API:
<!-- phpunit.xml -->
<env name="AUTO_TRANSLATION_ENABLED" value="false"/>
Then add targeted tests that re-enable it and fake the transport with Http::fake() per driver endpoint, asserting the outgoing payload contains the source text and the expected target locale. Keep one integration test per driver — provider request shapes drift.
CI. If you support Laravel 13, add it to the matrix now; the patch only permits the install, it doesn't prove your app's other dependencies are L13-ready.
Save-loop recursion. Translating inside saving and then persisting causes re-entry. The package guards against this internally in most paths, but any custom listener that calls $model->save() after translation will loop. Use saveQuietly() in your own hooks.
Source locale detection is the #1 cause of garbage output. The package sends "whatever is in the attribute" with a declared source. If app.locale and app.fallback_locale disagree, or a user saves while browsing es, you'll translate Spanish → Spanish. Explicitly pin the source locale in config and consider blocking writes from non-source locales.
Payload limits. Google caps around 5k characters per request; DeepL caps the number of texts per call. A long description HTML field silently truncates or 413s. Chunk long attributes and strip/restore markup deliberately — HTML tags often come back mangled or reordered.
Don't machine-translate everything. Slugs, SKUs, unique columns, enum values, JSON blobs, and email templates should be excluded. Machine-translated slugs break URLs and analytics; translated unique columns will collide.
Cost control. Every call is billable and non-deterministic. Cache aggressively, keyed on (attribute, source_hash, target_locale) so a retry or a reseed doesn't re-bill. Log token/character counts per job and alert on daily spend.
Rate limiting and retries. Configure backoff and maxExceptions on the translation job; providers return 429 with Retry-After. Retrying immediately burns your quota and leaves half-translated models — make the job idempotent (re-check for existing target values on retry).
Secrets in logs. Failed HTTP requests often log the full payload + Authorization header. Ensure your translation queue's exception handler redacts the API key; use a dedicated log channel at info level at most.
Localisation of the translation itself. OpenAI-family drivers accept extra instructions (tone, formality, glossary). DeepL has formality and glossaries. These are per-driver config keys — read the driver class rather than guessing, and prefer glossaries over prompt tweaks for brand terminology.
Extension points, in order of usefulness: custom driver → events → config knobs. Avoid subclassing the service provider; it's not a documented seam and will break on minor bumps.
Debugging checklist when a model won't translate: (1) implements AutoTranslatable and use HasAutoTranslation; (2) attribute listed in the translatable set; (3) AUTO_TRANSLATION_ENABLED true in that environment (Horizon workers inherit a different env than your web dyno surprisingly often); (4) queue worker running on the translations queue; (5) API key valid and provider not out of credits — a 401/402 usually surfaces as a failed job, not an exception in the UI.
How can I help you explore Laravel packages today?