---
title: "Overview"
description: "Pluggable translation backends – pick DeepL, Copilot AI, or a callable, or implement your own to route through any service."
canonical_url: "https://kirby.tools/docs/content-translator/php-classes/strategies"
---

# Overview

> Pluggable translation backends – pick DeepL, Copilot AI, or a callable, or implement your own to route through any service.

A `Strategy` receives a list of `TranslationUnit`s, returns translations in the same order, and stays stateless across calls. Built-in strategies handle batching and partial failures – per-unit failures keep source text and emit a warning hook; only zero survivors throw.

<note>

Available since v3.11. The deprecated `translateFn` config option still works and is wrapped automatically in a `CallableStrategy`.

</note>

## The Interface

```php
namespace JohannSchopplich\ContentTranslator\Translation;

interface Strategy
{
    /**
     * @param list<TranslationUnit> $units
     * @return list<string>
     *
     * @throws TranslationException When zero units could be translated.
     */
    public function execute(array $units, ExecutionOptions $options): array;
}
```

The contract is short: return one translation per input in the same order, and stay stateless across calls. Throw `TranslationException` only when *zero* units survived – per-unit failures keep the source text and trigger `content-translator.translate:warning`. The typed payloads passed in are `TranslationUnit` and `ExecutionOptions`.

Two things are handled for you, so a custom strategy never has to repeat them:

- **Untranslatable text never arrives.** Blanks, pure numbers, standalone URLs, and prose that is only KirbyTag placeholders are filtered out before `execute()` is called, and their source text is spliced back into the result. A batch with nothing else left never calls the strategy at all – so `$units` is never empty.
- **The result is validated afterwards.** Any unit that lost or invented a `<cN/>` token – and any slot that comes back missing or as a non-string – is reverted to source and reported through the warning hook, whichever strategy produced it.

## Built-in Implementations

<card-group>
<card icon="i-simple-icons-deepl" title="DeepLStrategy" to="/docs/content-translator/php-classes/strategies/deepl-strategy">

Default. Wraps the `DeepL` HTTP client, batches up to 50 texts per request.

</card>

<card icon="i-ri-sparkling-line" title="CopilotAIStrategy" to="/docs/content-translator/php-classes/strategies/copilot-ai-strategy">

Routes through Kirby Copilot. Chunks by item count and byte budget, drops responses with mismatched lengths.

</card>

<card icon="i-ri-function-line" title="CallableStrategy" to="/docs/content-translator/php-classes/strategies/callable-strategy">

Adapts a `Closure(string $text, string $target, ?string $source): string` to the strategy contract.

</card>
</card-group>

## Strategy Resolution

`Translator` resolves the active strategy in this order, first match wins:

<steps level="3">

### 1. Method Parameter

The `?Strategy $strategy` argument on `translateText`, `translateTexts`, and `translateContent`.

```php
Translator::translateText('Hello', 'de', 'en', new DeepLStrategy());
```

### 2. `strategy` Config Option

```php [config.php]
return [
    'johannschopplich.content-translator' => [
        'strategy' => 'deepl',
    ],
];
```

Four value types are accepted: the string presets `'deepl'` and `'ai'`, a closure with the signature of [`CallableStrategy`](/docs/content-translator/php-classes/strategies/callable-strategy), or an instance of any class implementing [`Strategy`](#implementing-a-custom-strategy).

### 3. Legacy `translateFn` <u-badge className="align-middle,ml-2,rounded-full!" label="deprecated" variant="subtle"></u-badge>

Kept for back-compat. Wrapped in `CallableStrategy` automatically.

### 4. Default

`new DeepLStrategy()`.

</steps>

<warning>

`'strategy' => 'ai'` throws `LogicException` when [Kirby Copilot](/copilot) is not installed. An unknown string throws `LogicException('Unknown strategy "<name>"')`.

</warning>

## Implementing a Custom Strategy

Implement the interface, attempt each unit, and only throw when *zero* units survive – matching the failure pattern of the built-in strategies:

<code-collapse>

```php
use JohannSchopplich\ContentTranslator\Translation\Exception\TranslationException;
use JohannSchopplich\ContentTranslator\Translation\ExecutionOptions;
use JohannSchopplich\ContentTranslator\Translation\Strategy;
use JohannSchopplich\ContentTranslator\Translation\TranslationUnit;
use Kirby\Cms\App;

final class MyApiStrategy implements Strategy
{
    public function execute(array $units, ExecutionOptions $options): array
    {
        // Pre-fill with source so failed units keep the original text
        $results = array_map(fn (TranslationUnit $u) => $u->text, $units);
        $translatedCount = 0;
        $lastError = null;

        foreach ($units as $i => $unit) {
            try {
                $results[$i] = myTranslate(
                    text: $unit->text,
                    target: $options->targetLanguage->code,
                    source: $options->sourceLanguage?->code,
                );
                $translatedCount++;
            } catch (Throwable $error) {
                $lastError = $error;
                App::instance()->trigger('content-translator.translate:warning', [
                    'unit' => $unit,
                    'reason' => $error->getMessage(),
                    'previous' => $error,
                ]);
            }
        }

        if ($translatedCount === 0) {
            throw new TranslationException(
                strategy: 'my-api',
                reason: $lastError?->getMessage() ?? 'unknown error',
                unitsAttempted: count($units),
            );
        }

        return $results;
    }
}
```

</code-collapse>

The three moves to remember:

1. **Pre-fill $results with source text** so a per-unit failure preserves the existing content.
2. **Emit content-translator.translate:warning** for each dropped unit – listeners can log or alert.
3. **Throw TranslationException only when zero units survived** – partial success is success.

Wire it up:

```php [config.php]
return [
    'johannschopplich.content-translator' => [
        'strategy' => new MyApiStrategy(),
    ],
];
```

This works as long as the constructor stays inert. A constructor that reads Kirby options or calls `App::instance()` runs too early here – wrap the instantiation in Kirby's `ready` callback instead, as shown under [DeepLStrategy](/docs/content-translator/php-classes/strategies/deepl-strategy#usage).

<tip>

If your backend supports batching, prefer implementing `Strategy` directly over passing a closure. `CallableStrategy` translates one text at a time; a real `Strategy` receives the full unit array, can batch, can route via `fieldKey`, and can decide per-unit whether to translate or pass through.

</tip>

---

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