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

Deepl Php Laravel Package

deeplcom/deepl-php

Official PHP client for the DeepL translation API. Send text and documents for high-quality translation from PHP 7.3+ applications, with full API support and simple Composer installation.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and key up

composer require deeplcom/deepl-php

Get a key from your DeepL account (the Free tier gives 500k characters/month and the key ends in :fx — the library routes Free vs Pro hosts automatically). Put it in .env, never in a committed file:

DEEPL_AUTH_KEY=f63c02c5-f056-...

2. Register a singleton — don't new DeepLClient at call sites. config/services.php:

'deepl' => [
    'key' => env('DEEPL_AUTH_KEY'),
    'options' => [
        'timeout' => 30,   // see DeepLClientOptions for exact setters
    ],
],

app/Providers/AppServiceProvider.php:

use DeepL\DeepLClient;
use DeepL\DeepLClientOptions;

$this->app->singleton(DeepLClient::class, fn () => new DeepLClient(
    config('services.deepl.key'),
    new DeepLClientOptions(config('services.deepl.options')),
));

3. Where to look first

  • src/DeepLClient.php — the whole public surface. Read the method signatures once; almost everything is translateText, rephraseText, translateDocument, uploadDocument/getDocumentStatus/downloadDocument, glossary CRUD, getUsage, listLanguages.
  • TranslateTextOptions / RephraseTextOptions / TranslateDocumentOptions / TextTranslationOptions — constant classes so you don't hand-type 'formality', 'tag_handling', etc.
  • src/Exceptions/ — this is what you actually code against.

4. First use case — a one-liner to confirm credentials:

$client = app(\DeepL\DeepLClient::class);
$result = $client->translateText('Hello, world!', null, 'fr');
echo $result->text;              // Bonjour, le monde !
echo $result->detectedSourceLang; // en
echo $result->billedCharacters;   // 13

Note the second argument is the source language and may be null for auto-detect. That positional order (text, source, target, options) trips up nearly everyone on day one.

Implementation Patterns

Wrap it in your own contract — this is the single most important pattern

DeepLClient is a concrete, final-ish class with no interface, so you cannot $this->mock(DeepLClient::class) cleanly and you must never let tests hit the real API. Define a thin adapter:

interface Translator
{
    public function translate(string $text, string $target, ?string $source = null, array $options = []): string;
}

class DeepLTranslator implements Translator
{
    public function __construct(private \DeepL\DeepLClient $client) {}

    public function translate(string $text, string $target, ?string $source = null, array $options = []): string
    {
        return $this->client->translateText($text, $source, $target, $options)->text;
    }
}

Bind Translator → DeepLTranslator in the container and bind a fake in tests. Every downstream concern (caching, retries, quota guards) then has a seam.

Batch instead of looping

Every call is an HTTP round trip plus rate-limit exposure. Pass an array:

$results = $client->translateText(
    $paragraphs,           // string[]
    null,
    'en-GB',
    [TranslateTextOptions::PRESERVE_FORMATTING => true],
);
// $results is TextResult[] in the same order as input

Sum billedCharacters across results to record cost per request.

Cache aggressively — translations are deterministic-ish and billed per character

$key = 'deepl:'.hash('xxh128', $text.'|'.$source.'|'.$target.'|'.json_encode($options));

return Cache::remember($key, now()->addDays(30), fn () => $translator->translate($text, $target, $source, $options));

Cache at the segment level (one row per source string), not the whole page. CMS content that changes rarely can be translated once at save-time and stored in a translations table keyed by (model, field, locale) — this is the pattern that keeps you inside the Free tier indefinitely.

HTML from a WYSIWYG / Blade content

Raw markup will get mangled unless you tell the API:

$client->translateText($html, null, 'de', [
    TranslateTextOptions::TAG_HANDLING => 'html',
    TranslateTextOptions::IGNORE_TAGS => 'code,pre',   // if supported in your version
    TranslateTextOptions::PRESERVE_FORMATTING => true,
]);

For user-generated HTML prefer tag_handling => 'xml' with explicit splitting_tags / non_splitting_tags when you control the markup — more predictable than html.

Use context — it's free

Characters in context are not billed. Feeding a few sentences of product/domain context markedly improves tone:

['context' => 'This is a B2B SaaS invoicing app. Use formal business language.']

For whole-domain terminology, pair it with a glossary rather than stuffing the prompt.

Glossaries

Create one per domain/product line, then pass the ID:

$glossary = $client->createGlossary('invoicing-de', 'de', 'en', \DeepL\GlossaryEntries::fromTsv($tsv));
$client->translateText($text, 'en', 'de', ['glossary' => $glossary->glossaryId]);

Cache glossaryId in config/DB — glossary IDs are account-scoped and change if you delete and recreate. Wrap lookups so a stale ID (404) falls back to a no-glossary translation instead of a 500.

Queue anything non-interactive

class TranslateDocumentJob implements ShouldQueue
{
    use Queueable;

    public int $tries = 1;       // do NOT auto-retry; each attempt re-uploads
    public int $timeout = 600;

    public function __construct(public string $documentId) {}

    public function handle(\DeepL\DeepLClient $client): void
    {
        $in  = Storage::disk('local')->path("docs/{$this->documentId}.docx");
        $out = Storage::disk('local')->path("docs/{$this->documentId}.de.docx");

        try {
            $client->translateDocument($in, $out, 'en', 'de', ['formality' => 'more']);
        } catch (\DeepL\DocumentTranslationException $e) {
            if ($e->documentHandle) {
                // persist handle so you can resume with getDocumentStatus() +
                // downloadDocument() instead of re-uploading (and re-billing)
                DocumentHandle::updateOrCreate(
                    ['document_id' => $this->documentId],
                    ['handle_id' => $e->documentHandle->documentId,
                     'handle_key' => $e->documentHandle->documentKey]
                );
            }
            throw $e;
        }

        Storage::disk('s3')->put("translations/{$this->documentId}.de.docx", file_get_contents($out));
    }
}

translateDocument() is a blocking convenience wrapper around upload → poll → download. For anything user-facing, use the three primitives directly so you can show progress and survive worker restarts.

Validate languages against the API, then cache

$targets = Cache::remember('deepl:targets', now()->addDay(), fn () =>
    collect($client->getTargetLanguages())->pluck('code')->all()
);

Validator::extend('deepl_target', fn ($attr, $value) => in_array($value, $targets, true));

Language codes are case-insensitive but normalize to lowercase (en-us → en-US) before storing so your cache keys and DB rows stay consistent.

Guard the quota in a middleware or job preamble

try {
    $usage = $client->getUsage();
    // log / alert; inspect the Usage object properties (character counts + limits)
} catch (\DeepL\AuthorizationException $e) {
    report($e);           // bad or expired key — fail fast, don't retry
}

Catch QuotaExceededException at the boundaries and degrade gracefully: render the source text with a "translation unavailable" banner rather than a 500.

Gotchas and Tips

Return type flips with the input type. translateText('a', …) returns a single TextResult; translateText(['a'], …) returns TextResult[]. If a caller sometimes passes a string and sometimes an array, you get ->text on an array — always normalize to an array and index [0], or keep the string/array paths in separate methods.

The source argument is positional and often null. translateText($text, 'de', 'fr') means German→French, not auto→fr. Use named options arrays and a wrapper with a sensible signature.

formality is not universally supported. Passing 'less'/'more' to an unsupported target throws. Use 'prefer_less' / 'prefer_more' in any code path where the target language is user-selected — it degrades to default instead of erroring.

custom_instructions and model_type collide. Any request with custom_instructions defaults to quality_optimized; combining it with latency_optimized is rejected. Also: max 10 instructions, 300 chars each, and only certain target languages (de, en, es, fr, it, ja, ko, zh + variants).

rephraseText takes style or tone, never both. Passing both writing_style and tone is an error. Use prefer_* variants if the choice is user-driven.

Character billing is on the source, and retries can double-bill. If a request succeeds server-side but your client times out, the retry is billed again. Mitigate with segment-level caching (above) and by not blind-retrying on timeout for large batches — record what you sent.

translateDocument needs real local filesystem paths. Storage::path() works on the local disk; S3 needs a download to tmp/storage/app/temp first, then an upload of the output. Clean up temp files in a finally block.

30 MB document limit, and minification is pptx-only. ['minification' => true] only applies to the high-level translateDocument() (not uploadDocument()), and only helps pptx. For Word docs bloated with tracked changes/images, shrink them yourself before upload. You can emulate the high-level behaviour by instantiating DocumentMinifier and calling its methods between uploadDocument() and downloadDocument().

Don't $tries = 5 on document jobs. Each retry re-uploads. Persist documentHandle (from DocumentTranslationException) and resume with getDocumentStatus()/downloadDocument() — that's exactly why the exception carries it.

429 handling is yours. TooManyRequestsException is thrown on rate limits. Rely on queue concurrency limits (Redis::throttle, WithoutOverlapping) rather than a retry() loop inside a request — the API is a shared, metered resource and backoff-in-process will hold PHP-FPM workers hostage.

Never let the key reach the browser. No Livewire/Alpine/Inertia props, no VITE_ prefix, no client-side translate-on-keystroke. All translation goes through your own endpoint → queue → adapter.

DeepLClient has no interface. Mocking it with PHPUnit requires createMock on a concrete class with a constructor — brittle. The adapter contract from the patterns section is the workaround, and it's also where you'd swap in a stub provider for offline dev.

PHP version drift. The README states 7.3+, but support for EOL versions is being dropped and 8.1+ is the practical floor. If you're on an older Laravel/PHP combination, pin the package and check the CHANGELOG before composer update — the option constants have grown over time (styles, translation memories, rephrasing are recent additions), so a version bump can introduce behaviour changes in option handling.

Extension points are thin, by design. No events, no middleware, no PSR-18 injection in the public API. Your extension surface is: (a) the adapter contract, (b) a decorator around it for caching/logging/metrics, (c) the queue job layer, (d) DeepLClientOptions passed to the constructor for host/timeout/retry. Plan the architecture around those four seams rather than expecting to hook into the library itself.

Log billedCharacters per call. It's the only reliable way to attribute cost to a feature, tenant, or user, and it makes quota regressions visible before you hit the monthly wall.

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.
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log