---
title: "DeepL Client"
description: "Talk to the DeepL API directly from PHP – override language mapping, tune request options, or mock the client in tests."
canonical_url: "https://kirby.tools/docs/content-translator/php-classes/deepl-client"
---

# 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

```php
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`

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

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

### `translateMany`

```php
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.

```php
$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:

<table>
<thead>
  <tr>
    <th>
      Status
    </th>
    
    <th>
      Exception
    </th>
    
    <th>
      Message
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      400
    </td>
    
    <td>
      <code>
        LogicException
      </code>
    </td>
    
    <td>
      Bad request – check parameters
    </td>
  </tr>
  
  <tr>
    <td>
      403
    </td>
    
    <td>
      <code>
        AuthException
      </code>
    </td>
    
    <td>
      Authorization failed
    </td>
  </tr>
  
  <tr>
    <td>
      404
    </td>
    
    <td>
      <code>
        LogicException
      </code>
    </td>
    
    <td>
      API endpoint not found
    </td>
  </tr>
  
  <tr>
    <td>
      413
    </td>
    
    <td>
      <code>
        LogicException
      </code>
    </td>
    
    <td>
      Request size limit exceeded
    </td>
  </tr>
  
  <tr>
    <td>
      429, 529
    </td>
    
    <td>
      <code>
        LogicException
      </code>
    </td>
    
    <td>
      Too many requests (after retries)
    </td>
  </tr>
  
  <tr>
    <td>
      456
    </td>
    
    <td>
      <code>
        LogicException
      </code>
    </td>
    
    <td>
      Quota exceeded
    </td>
  </tr>
  
  <tr>
    <td>
      500/503/504
    </td>
    
    <td>
      <code>
        LogicException
      </code>
    </td>
    
    <td>
      Server error (after retries)
    </td>
  </tr>
</tbody>
</table>

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](https://developers.deepl.com/docs/getting-started/supported-languages), and a code that already carries a region ignores the locale entirely.

<table>
<thead>
  <tr>
    <th>
      Kirby code
    </th>
    
    <th>
      <code>
        LC_ALL
      </code>
    </th>
    
    <th>
      DeepL receives
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        de-ch
      </code>
    </td>
    
    <td>
      <code>
        de_DE.UTF-8
      </code>
    </td>
    
    <td>
      <code>
        DE-CH
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        de
      </code>
    </td>
    
    <td>
      <code>
        de_CH.UTF-8
      </code>
    </td>
    
    <td>
      <code>
        DE-CH
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        en
      </code>
    </td>
    
    <td>
      <code>
        en_GB.UTF-8
      </code>
    </td>
    
    <td>
      <code>
        EN-GB
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        es
      </code>
    </td>
    
    <td>
      <code>
        es_MX.UTF-8
      </code>
    </td>
    
    <td>
      <code>
        ES-419
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        de
      </code>
    </td>
    
    <td>
      none
    </td>
    
    <td>
      <code>
        DE
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ca
      </code>
    </td>
    
    <td>
      <code>
        es_ES.UTF-8
      </code>
    </td>
    
    <td>
      <code>
        CA
      </code>
    </td>
  </tr>
</tbody>
</table>

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.

<callout color="info" icon="i-ri-arrow-right-line" to="/docs/content-translator/providers/deepl#language-code-overrides">

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

</callout>

## Request Options

Custom request options merge into every translate call. See [DeepL request body parameters](https://developers.deepl.com/docs/api-reference/translate#request-body-descriptions).

```php [config.php]
return [
    'johannschopplich.content-translator' => [
        'DeepL' => [
            'apiKey' => env('DEEPL_API_KEY'),
            'requestOptions' => [
                'formality' => 'more',
                'glossary_id' => 'YOUR_GLOSSARY_ID',
            ],
        ],
    ],
];
```

<note>

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.

</note>

---

Every page of this site as Markdown: <https://kirby.tools/sitemap.md>
