Exceptions

Which failures raise which exception – a dead provider, a misconfigured strategy, a missing key, a language the site never registered.

TranslationException is the plugin's own; the rest are Kirby's. All extend Kirby\Exception\Exception and serialize cleanly to API responses. Built-in strategies raise TranslationException only when zero units could be translated – partial failures emit a :warning hook instead.

TranslationException

JohannSchopplich\ContentTranslator\Translation\Exception\TranslationException extends Kirby\Exception\Exception. Built-in strategies throw it when zero units survived – per-unit failures keep the source text and only emit a :warning hook. A closure strategy never raises it; its own exception propagates.

DeepL errors – a rejected key, an HTTP error, a target language it cannot name – reach a translateContent() caller as TranslationException, with the original message as its reason. Only a missing key throws AuthException.

public function __construct(
    string $strategy,
    string $reason,
    int $unitsAttempted,
    int $unitsTranslated = 0,
)

Details Payload

The exception carries structured details for logging:

strategy
String
The strategy identifier (deepl, copilot-ai, or your own).
unitsAttempted
Int
How many units the strategy was asked to translate.
unitsTranslated
Int
How many succeeded before the strategy gave up. Always 0 when this exception is thrown.

For per-unit error context, listen to content-translator.translate:warning – it fires for each failed unit. Its previous carries the underlying Throwable when the strategy caught one, and is null for the pipeline's own checks, such as a placeholder mismatch.

Catching

use JohannSchopplich\ContentTranslator\Translation\Exception\TranslationException;

try {
    $translator->translateContent('de', 'de', 'en');
} catch (TranslationException $error) {
    $details = $error->getDetails();
    // ['strategy' => 'deepl', 'unitsAttempted' => 12, 'unitsTranslated' => 0]
}

An unhandled TranslationException reaches the Panel as HTTP 502.

LogicException

Configuration errors and a locked model throw Kirby\Exception\LogicException:

TriggerMessage
'strategy' => 'ai' without kirby-copilot installedStrategy "ai" requires the kirby-copilot plugin
'strategy' => 'banana'Unknown strategy "banana"
Another user is editing the modelLockedContentException (HTTP 423), with Kirby's own lock message

The Panel skips a locked model; a script has to catch Kirby\Content\LockedContentException per model and carry on.

InvalidArgumentException

Kirby\Exception\InvalidArgumentException is thrown for a language code the site does not know:

TriggerMessage
Unregistered code on a multi-language siteUnknown language code "…"; not registered in Kirby languages.

Both the translate() helper and Translator::translateText() resolve their $targetLanguage and $sourceLanguage against Kirby's language registry, so a typo in a script surfaces here rather than as a failed request. A single-language site skips the check – any code is passed on to the strategy.

AuthException

Kirby\Exception\AuthException is thrown for a missing API key:

TriggerMessage
Missing DeepL API keyMissing DeepL API key
Missing Copilot AI provider API keyMissing API key in "johannschopplich.copilot.providers.<name>.apiKey"
For per-unit rejections, listen to content-translator.translate:warning instead of catching exceptions.