DeepL Client
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:
| Status | Exception | Message |
|---|---|---|
| 400 | LogicException | Bad request – check parameters |
| 403 | AuthException | Authorization failed |
| 404 | LogicException | API endpoint not found |
| 413 | LogicException | Request size limit exceeded |
| 429, 529 | LogicException | Too many requests (after retries) |
| 456 | LogicException | Quota exceeded |
| 500/503/504 | LogicException | Server 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 code | LC_ALL | DeepL receives |
|---|---|---|
de-ch | de_DE.UTF-8 | DE-CH |
de | de_CH.UTF-8 | DE-CH |
en | en_GB.UTF-8 | EN-GB |
es | es_MX.UTF-8 | ES-419 |
de | none | DE |
ca | es_ES.UTF-8 | CA |
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.
Request Options
Custom request options merge into every translate call. See DeepL request body parameters.
return [
'johannschopplich.content-translator' => [
'DeepL' => [
'apiKey' => env('DEEPL_API_KEY'),
'requestOptions' => [
'formality' => 'more',
'glossary_id' => 'YOUR_GLOSSARY_ID',
],
],
],
];
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.