Overview
A strategy is the translation backend Kirby Content Translator routes through. Pick a built-in (DeepL, Copilot AI, callable) or implement Strategy to plug in any service.
translateFn config option still works and is wrapped automatically in a CallableStrategy.The Interface
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, ExecutionOptions, and TranslationMode.
Built-in Implementations
DeepLStrategy
Default. Wraps the DeepL HTTP client, batches up to 50 texts per request, falls back to single requests for TranslationMode::Single.
Strategy Resolution
Translator resolves the active strategy in this order, first match wins:
1. Method Parameter
The ?Strategy $strategy argument on translateText, translateTexts, and translateContent.
Translator::translateText('Hello', 'de', 'en', new DeepLStrategy());
2. strategy Config Option
Accepts a string preset, a closure, or a Strategy instance.
return [
'johannschopplich.content-translator' => [
// String presets
'strategy' => 'deepl', // or 'ai' (requires kirby-copilot)
// …or a closure
'strategy' => fn (string $text, string $target, ?string $source) => /* … */,
// …or a Strategy instance
'strategy' => new MyCustomStrategy(),
],
];
3. Legacy translateFn deprecated
Kept for back-compat. Wrapped in CallableStrategy automatically.
4. Default
new DeepLStrategy().
'strategy' => 'ai' throws LogicException when Kirby Copilot is not installed. An unknown string throws LogicException('Unknown strategy "<name>"').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:
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;
}
}
The three moves to remember:
- Pre-fill
$resultswith source text so a per-unit failure preserves the existing content. - Emit
content-translator.translate:warningfor each dropped unit – listeners can log or alert. - Throw
TranslationExceptiononly when zero units survived – partial success is success.
Wire it up:
return [
'johannschopplich.content-translator' => [
'strategy' => new MyApiStrategy(),
],
];
Strategy directly over passing a closure. CallableStrategy translates one text at a time and ignores TranslationMode; 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.