DeepL Client

Talk to the DeepL API directly from PHP – override language mapping, tune request options, or mock the client in tests.

Used internally by DeepLStrategy. Instantiate it directly when building a custom Strategy, overriding language mapping, or stubbing translation in tests.

Construction

use JohannSchopplich\ContentTranslator\DeepL;

$client = DeepL::instance();

The cached singleton reads johannschopplich.content-translator.DeepL.apiKey once. The constructor throws AuthException('Missing DeepL API key') when the key is empty.

The constructor takes two optional closures, $remote and $delay, that stand in for the HTTP request and the backoff sleep. DeepL::reset() drops the singleton so a later instance() builds a fresh one.

Methods

translate

public function translate(string $text, string|TranslationLanguage $targetLanguage, string|TranslationLanguage|null $sourceLanguage = null): string

Convenience wrapper around translateMany([$text], …).

translateMany

public function translateMany(array $texts, string|TranslationLanguage $targetLanguage, string|TranslationLanguage|null $sourceLanguage = null): array

Send up to N texts. The client auto-chunks into requests of 50 texts each and merges the results.

$translations = DeepL::instance()->translateMany(
    ['Hello', 'Goodbye', 'Thank you'],
    'de',
    'en'
);
// ['Hallo', 'Auf Wiedersehen', 'Danke']

Error Handling

The client maps DeepL HTTP responses to typed exceptions:

StatusExceptionMessage
400LogicExceptionBad request – check parameters
403AuthExceptionAuthorization failed
404LogicExceptionAPI endpoint not found
413LogicExceptionRequest size limit exceeded
429, 529LogicExceptionToo many requests (after retries)
456LogicExceptionQuota exceeded
500/503/504LogicExceptionServer error (after retries)

429, 500, 503, 504, and 529 responses are retried automatically with exponential backoff – up to 5 retries after the initial request, with the delay capped at 8 seconds. Every other error status throws on the first response.

A 200 whose body does not answer every text in the batch throws LogicException too. DeepL answers one to one, so a mismatch means the response was truncated or rewritten in transit, and no positional mapping back onto the source texts can be trusted.

Language Code Resolution

The Kirby language code names the language; its LC_ALL locale may only sharpen that code into a regional variant, and a locale naming another language is discarded. Both are matched against DeepL's supported target languages, and a code that already carries a region ignores the locale entirely.

Kirby codeLC_ALLDeepL receives
de-chde_DE.UTF-8DE-CH
dede_CH.UTF-8DE-CH
enen_GB.UTF-8EN-GB
eses_MX.UTF-8ES-419
denoneDE
caes_ES.UTF-8CA

When the code names no supported target, translateMany() throws LogicException – a locale from another language cannot stand in for it, so map the code via targetLanguageOverrides.

Source languages take the same route, then drop the regional part – DeepL rejects a variant in source_lang. A de-ch language is sent as DE. When no supported code can be named, the source is omitted and DeepL detects it.

Passing a TranslationLanguage instead of a code skips the language registry lookup, which is what DeepLStrategy does. A bare code is looked up to find its locale.

To map a Kirby language code to a DeepL target code yourself, see Language Code Overrides.

Request Options

Custom request options merge into every translate call. See DeepL request body parameters.

config.php
return [
    'johannschopplich.content-translator' => [
        'DeepL' => [
            'apiKey' => env('DEEPL_API_KEY'),
            'requestOptions' => [
                'formality' => 'more',
                'glossary_id' => 'YOUR_GLOSSARY_ID',
            ],
        ],
    ],
];
The client sends tag_handling=html only for texts that carry markup – an HTML tag or a KirbyTag placeholder – so writer-field markup survives while plain text keeps its <, > and ' unescaped. A batch mixing both is split into one request per group. split_sentences=1 is always sent, to keep markdown line breaks intact.Options DeepL only reads under tag handling – tag_handling_version, outline_detection, splitting_tags, non_splitting_tags and ignore_tags – are dropped from the plain-text request along with it. DeepL rejects the last three outright when they arrive without tag_handling.Setting tag_handling in requestOptions pins it for every text and turns the per-text detection off. It still snaps back to html for texts carrying the plugin's <span translate="no"> wrapper, which DeepL only honours under HTML tag handling.