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 Lib Laravel Package

babymarkt/deepl-php-lib

A PHP client for the DeepL translation API by babymarkt. Translate text across languages, manage glossaries, and integrate DeepL into Laravel or any PHP application through a clean, framework-agnostic interface.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up an API key

composer require babymarkt/deepl-php-lib

The library is a thin, synchronous wrapper around the DeepL REST API (v2). Everything hangs off a single Babymarkt\Deepl\Client instance constructed with an options array:

use Babymarkt\Deepl\Client;

$client = new Client([
    'auth_key' => config('services.deepl.key'),
    // 'server' => Client::SERVER_FREE, // required when the key ends in ":fx"
]);

Where to look first (in this order)

  1. README.md — the only real documentation; the examples cover translate, glossary, and document flows.
  2. src/Client.php — the full public surface. This file is the contract. Because the project is archived, treat the source as the authoritative docs.
  3. tests/ — the test suite doubles as executable examples for option arrays and response shapes.
  4. Your DeepL account dashboard — confirm free vs. pro, and the monthly character allowance.

First use case: translating a batch of short strings

$result = $client->translate('Hallo Welt', 'DE', 'EN');
// ['translations' => [['detected_source_language' => 'DE', 'text' => 'Hello world']]]

echo $result['translations'][0]['text'];

That is the 90% path: take user-facing copy (product names, CMS labels, transactional emails), send it through translate(), persist the output. Once that works, add a glossary for brand and domain terms, then move to document translation for PDFs/DOCX if you have marketing collateral.

A minimal Laravel binding:

// app/Providers/AppServiceProvider.php
$this->app->singleton(Client::class, fn () => new Client([
    'auth_key' => config('services.deepl.key'),
    'server'   => config('services.deepl.server'),
]));
// config/services.php
'deepl' => [
    'key'    => env('DEEPL_AUTH_KEY'),
    'server' => env('DEEPL_SERVER', 'pro'), // 'free' for :fx keys
],

Implementation Patterns

Wrap the client behind your own interface. The package is archived; you will outgrow it or be forced onto the official deeplcom/deepl-php client. A one-method interface (Translator::translate(string $text, string $from, string $to, array $opts = []): string) means the swap is a single binding change, not a grep-and-replace across the app.

final class DeepLTranslator implements Translator
{
    public function __construct(private Client $client) {}

    public function translate(string $text, string $from, string $to, array $opts = []): string
    {
        $r = $this->client->translate($text, $from, $to, $opts);
        return $r['translations'][0]['text'];
    }
}

Cache aggressively, bill once. DeepL charges per source character per request. The single biggest cost lever is a translation-memory table keyed by sha1($text . $from . $to . serialize($opts)). Cache in Redis or a DB table; strings repeat constantly across CMS pages and product catalogs.

$key = 'deepl:'.sha1("$from|$to|$text|".json_encode($opts));
return Cache::rememberForever($key, fn () => $this->inner->translate($text, $from, $to, $opts));

Batch by locale, not by row. The translate endpoint accepts multiple text values per request (capped at ~50 items and ~128 KiB total). Loop over the collection in chunks of 50 with a consistent source/target pair, then zip results back by index. This cuts HTTP round-trips by an order of magnitude.

collect($rows)->chunk(50)->each(function ($chunk) use ($client) {
    $res = $client->translate($chunk->pluck('body')->all(), 'EN', 'DE', ['tag_handling' => 'html']);
    $chunk->values()->each(fn ($row, $i) => $row->update([
        'body_de' => $res['translations'][$i]['text'],
    ]));
});

Use glossaries for brand and domain vocabulary. Create one glossary per language pair per domain (e.g. en-de-products), store the returned glossary_id in config or DB, and pass it as an option on every translate call. Glossary entries are exact-match and case-sensitive, so seed them from your terminology list rather than inventing them — and remember entries can only be replaced by deleting and recreating the glossary.

$g = $client->createGlossary('en-de-products', 'EN', 'DE', [
    'powerbank' => 'Powerbank',
    'bundle'    => 'Bundle',
]);
// persist $g['glossary_id']

Document translation is a three-step async flow. Upload → poll status → download. Model it as a queued job with retries, and persist both document_id and document_key (the key is returned once at upload time and is required for download — losing it means re-uploading and re-billing).

$doc = $client->uploadDocument(storage_path("app/export/{$id}.docx"), 'EN', 'DE');
$job = TranslateDocument::dispatch($doc['document_id'], $doc['document_key']);

// inside the job
$status = $client->checkDocumentStatus($this->documentId, $this->documentKey);
if ($status['status'] === 'done') {
    Storage::put("translations/{$id}.docx",
        $client->downloadDocument($this->documentId, $this->documentKey));
}

Poll with exponential backoff ($this->release(15 * $attempts)); documents routinely take longer than a single HTTP timeout.

Track quota before you spend it. getUsage() returns character_count and character_limit. Cache it for a few minutes and expose it in your admin dashboard, or gate bulk jobs on "remaining > estimated cost". This is far cheaper than discovering an exhausted quota mid-job.

Cache the language list forever. getLanguages() (and getGlossaryLanguages()) are static reference data — put them in a config cache or a long-TTL cache entry and never call them on a request path.

Use it inside queues, not controllers. Every call is network I/O with 100–500 ms latency and occasional 429s. A queued job with retry(3) and a rate limiter (Redis::throttle('deepl')->allow(10)->every(1)) gives you backpressure without blocking user requests.

Gotchas and Tips

Server must match key type. Free-tier keys end in :fx and must hit api-free.deepl.com; pro keys hit api.deepl.com. Mixing them yields a terse 403. Make server an env var so a key rotation doesn't require a deploy — and validate at boot.

The package is archived (last release June 2022). DeepL's API has moved on: newer glossary APIs, expanded formality values, additional tag_handling modes, and revised document limits. Some options may be passed through untouched and some may be silently rejected or dropped. Before relying on any parameter, confirm it actually appears in the outgoing request (log the body) or in src/. If you need post-2022 features, plan a migration to the official client behind your own interface.

No built-in retry, backoff, or rate limiting. You get raw exceptions. Wrap calls yourself and distinguish cases:

  • 429 → retry with jittered backoff; also respect any Retry-After.
  • 456 → quota exceeded; do not retry, alert.
  • 403 → bad key or wrong server; do not retry.
  • 5xx/timeouts → retry.
  • 400 → malformed request (invalid UTF-8, unknown option, oversized payload); do not retry.
retry(3, fn () => $this->inner->translate(...), 500, fn ($e) => $this->isRetryable($e));

Set explicit timeouts. Default HTTP timeouts can be long enough to stall a PHP-FPM worker or eat a queue job's runtime. If the client lets you inject an HTTP client/options array, set connect ~5 s and total ~30 s; otherwise wrap in a queued job with a hard timeout.

UTF-8 or bust. Invalid byte sequences produce opaque 400s. Run source text through mb_check_encoding($text, 'UTF-8') and normalise/strip before sending — especially for text scraped from legacy databases or third-party feeds.

HTML needs tag_handling => 'html'. Without it, tags get mangled or escaped. Even with it, inline tags without whitespace can break; use ignore_tags for things like {{ twig }} placeholders or <code> blocks. For Laravel Blade/vue interpolation, add ignore_tags entries for your placeholder patterns or pre-substitute sentinels.

Auto-detect (source_lang = null) fails on short strings. Product SKUs, single words, and proper nouns get misclassified — often into the wrong language entirely. When you know the source locale (you usually do, since it's a DB column), pass it explicitly. Batch mixed-language content into separate calls per locale.

formality is not universal. It's only accepted for certain target languages (roughly DE, FR, IT, ES, PT-PT/BR, NL, PL, RU, and a few others). Sending it for an unsupported target returns a 400, which a naive batch job will treat as fatal for the whole chunk. Filter the option per target locale.

Size limits are per request, not per string. ~128 KiB total and ~50 text entries. The library does not split for you. Chunk by both count and cumulative strlen(), and remember a single oversized paragraph will still fail — split on sentence boundaries if needed.

Billing is per source character, and whitespace counts. Cache before you translate, not after. Also beware "translate on save" hooks in Eloquent models: an editor hitting save ten times translates the same paragraph ten times.

preserve_formatting and split_sentences interact. Disabling sentence splitting (split_sentences => '0') improves quality for single-line UI strings but degrades paragraph translation. Use different option sets for different content types rather than one global config.

Laravel's Http::fake() will not intercept this client. It brings its own transport, so feature tests that fake Laravel HTTP calls will still hit the network. Fake at your wrapper boundary instead — bind a stub Translator in the container — or inject a mock HTTP client if the options array supports it. For integration tests, record real responses as fixtures and replay them.

Never log the auth key. The library doesn't integrate with PSR-3, so any logging is on you. Log the endpoint, target languages, character count, and elapsed time — never the options array wholesale, since it contains the key.

PHP 7.3 floor means no typed properties or enums in the library's internals. The code predates PHP 8 idioms; on PHP 8.3+ you may see deprecation notices from transitive dependencies. Pin the lockfile, suppress the noise, and keep this client isolated behind your interface so a future PHP upgrade doesn't couple to it.

Glossary IDs are persistent but language-pair bound. You need one per source/target combination; there is no cross-pair fallback. Store them in a small config map and treat "missing glossary" as a soft failure — translate without it rather than throwing in a user-facing path.

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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin