Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Deepl Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. composer require simonet85/laravel-deepl — requires PHP 8.2+ and Laravel 11, 12 or 13.
  2. Publish the config only if you need to override defaults: php artisan vendor:publish --tag=deepl-config. Otherwise the package works out of the box once DEEPL_API_KEY is set.
  3. Put your key in .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.
  4. First use case — fill a missing locale file. This is the lowest-friction way to see the package work end to end:
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.

Implementation Patterns

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.

Gotchas and Tips

  • Facade calls hit the network in tests. There is no documented fake in v1.0.0. Don't let 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.
  • Cache key composition is undocumented. Verify whether target locale, formality and context are part of the cache key before relying on the cache across multiple target languages in one process. If only the source text is keyed, translating the same string to 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.
  • Formality is language-dependent. It is not supported for every target language, and requesting it where unsupported can error or be ignored. Keep formality in config per environment, and check the API's supported-language matrix before enabling it globally.
  • Context steers tone/terminology for the whole request. A single global context is a blunt instrument — if you translate marketing copy and UI strings in the same app, consider separate calls with different context rather than one global setting.
  • Free-tier keys are identifiable by the :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.
  • Rate limits and 429s are the most common runtime failure with large batches. Retry with backoff around the batch call; the official SDK throws typed exceptions, so catch narrowly rather than swallowing everything into an empty translation.
  • Silent-empty failure mode. Decide explicitly what happens when translation fails: throw, or fall back to the source string. Falling back is usually right for UI labels but hides broken locales — log it.
  • Extension points: the published 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.
  • Version pinning: this is the first public release, so treat the internal API as unstable between minor versions. Pin the constraint (^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.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource