simonet85/laravel-deepl
Laravel integration for the DeepL translation API. Add your API key via config, then translate text between supported languages from your application using a simple, framework-friendly wrapper around DeepL's translation endpoints, including source and target language selection.
composer require simonet85/laravel-deepl — requires PHP 8.2+ and Laravel 11, 12 or 13.php artisan vendor:publish --tag=deepl-config. Otherwise the package works out of the box once DEEPL_API_KEY is set..env. The package auto-detects the API tier: a key ending in :fx is treated as Free, anything else as Pro. No separate config switch to get wrong.php artisan deepl:translate fr --dry-run # inspect what would be translated and the character cost
php artisan deepl:translate fr # write the missing keys into lang/fr.json
php artisan deepl:usage # confirm what you just spent
Where to look first, in order: the published config/deepl.php (cache store/TTL, formality, context), the DeepL facade, then the two Artisan commands. The facade is the whole public API surface: DeepL::translate() and DeepL::usage(). If you only ever call the command, you may never touch the facade — and vice versa.
String vs. array. translate() accepts either. With an array, keys are preserved on the way out, so you can pass an associative map straight from your domain and get the same shape back:
$labels = DeepL::translate([
'greeting' => 'Hello :name',
'items' => 'You have :count items',
], 'fr');
// ['greeting' => 'Bonjour :name', 'items' => 'Vous avez :count articles']
Prefer the array form over looping. It is what enables batching (up to 50 texts per request) and what keeps cache lookups cheap.
Respect the batch ceiling yourself when the input is unbounded. The package batches at 50 texts per request; when you are translating user-generated lists or a large import, array_chunk($texts, 50, true) before calling and merge afterwards. Preserving keys matters — array_chunk with true keeps them.
Queue the expensive paths. Translation is a network call with real per-character cost. Anything triggered by a request or a webhook should dispatch a job, not call the facade inline. Inside a job, set the translation cache store to something shared (Redis/database) so workers on other hosts benefit from the cache; a file cache on ephemeral containers silently defeats the "never billed twice" guarantee.
Cache is a cost control, not a correctness feature. A previously translated string is not re-billed. That means wording changes in your source strings produce new cache entries (fine), but it also means a stale translation may persist after you change formality/context config. Treat config changes to formality/context as requiring a cache clear plus a --force re-run.
Placeholder protection changes your string conventions. :count-style Laravel placeholders plus & < > are protected. Write source strings using Laravel placeholders (:name), not {{ name }} or %s — the latter are not covered and DeepL may translate or reorder them. Never concatenate translated fragments; translate whole sentences so placeholders and grammar land correctly.
Deploy-time workflow for lang files. Run deepl:translate {locale} in CI or as a deploy step, guarded by --dry-run so you can diff and fail the build on unexpected spend. Commit the resulting lang/*.json. Use --scan= to point at directories containing translation keys so strings only referenced dynamically (e.g. __("status.$code")) are discovered — without it, those keys are invisible and the command will under-translate. The command also reads keys from every lang/*.json, which is how it covers keys that exist in another locale but not the target one.
--force is the budget-destroying flag. Default behaviour is additive: missing keys only. --force retranslates existing keys — correct after a source-language rewrite, expensive if run habitually. Guard it behind a manual step, never a nightly cron.
Usage monitoring. DeepL::usage() (and php artisan deepl:usage) returns characters consumed this month. Surface it on an internal dashboard or alert at a threshold — quota exhaustion shows up as failed translations in production otherwise, and the failure mode is usually a thrown SDK exception rather than a graceful fallback.
Wrap the facade. Bind your own Translator interface with a DeepL implementation and depend on the interface in app code. The facade is static and hits the network in tests; one thin adapter lets you swap in a fake and keeps test suites fast and free.
DeepL::translate() run in a test suite — bind a fake behind your own interface, or hit the cache/mock layer. Assume real characters get billed if you forget.lang/{locale}.json only. The command completes JSON translation files. If your app uses PHP array files (lang/fr/messages.php) or a database translation driver, the command will not touch them — you'll need to loop the facade yourself.fr and de could return the wrong result from cache. Smoke-test this on day one.& < > protection is not full HTML handling. The package escapes/restores these characters, but that is not the same as DeepL's tag_handling=html. Content with real markup (links, <strong>) should be translated per-fragment, not as a whole HTML blob.:fx suffix. If auto-detection misbehaves (e.g. the key is trimmed or wrapped in quotes in .env), you'll get auth or wrong-endpoint errors that look like credential problems. Check for stray whitespace and quotes first.config/deepl.php (API key, cache store/TTL, formality, context, batch behaviour) is the primary knob, plus the cache store itself for cache warm-up and invalidation. There is no documented middleware, event or glossary support in v1.0.0 — if you need DeepL glossaries for brand terminology, you'll be working below the package surface with the underlying deeplcom/deepl-php SDK.^1.0) and re-read the config file after every update — config keys added in later versions won't appear in an already-published file.How can I help you explore Laravel packages today?