Hooks
The plugin exposes three Kirby hooks. They fire for every individual text the pipeline processes – including text inside blocks, structures, layouts, and table cells.
| Hook | When | Return |
|---|---|---|
content-translator.translate:before | Before a unit is sent to the strategy | Modified text (string) |
content-translator.translate:after | After the strategy returns a translation | Modified text (string) |
content-translator.translate:warning | When a unit is dropped (per-unit failure) | n/a (event-style) |
:before and :after fire for every unit holding content, including values no provider ever sees – a field that is just a price or a URL still runs both hooks, with the text unchanged. Branch on $unit->fieldKey or inspect $text if a hook should only act on prose.content-translator.translate:before
Modify text before translation, or short-circuit translation entirely.
de, fr, en-gb, …).null if not specified.text for now. Reserved for future expansion.TranslationUnit (text, fieldKey). Lets you branch on the originating field or table cell.ExecutionOptions carrying both targetLanguage and sourceLanguage as TranslationLanguage value objects.apply() matches by parameter name, so a closure only has to declare the payload keys it uses.return [
'hooks' => [
'content-translator.translate:before' => function ($text, $targetLanguage, $sourceLanguage, $type) {
// Skip translation for marked text
if (str_contains($text, '[no-translate]')) {
return str_replace('[no-translate]', '', $text);
}
return $text;
}
]
];
content-translator.translate:after
Postprocess translated text – language-specific formatting, terminology enforcement, logging.
:before rewrites).text for now.return [
'hooks' => [
'content-translator.translate:after' => function ($text, $originalText, $targetLanguage) {
// Restore untranslatable terms
$protected = ['API', 'CSS', 'HTML', 'JavaScript'];
foreach ($protected as $term) {
$text = preg_replace('/\b' . $term . '\b/i', $term, $text);
}
return $text;
}
]
];
content-translator.translate:warning v3.11+
Fires once per unit that was dropped. The unit keeps its source text. Silent by default – wire it to logging, Sentry, or Slack to surface drops in production.
How much survives depends on where the drop happened. A placeholder count mismatch or non-string translation is caught after the strategy returns, so it applies to every strategy – including a custom one – and only the affected unit is reverted. The remaining reasons come from the strategy itself: CopilotAIStrategy drops per unit and per chunk, so the rest of the batch still gets written, while DeepLStrategy sends the batch in one request and, when it fails, fires a warning for every unit and then throws TranslationException, so nothing from that batch is stored. Treat a DeepL warning as a batch-level failure that happens to be reported per unit.
Drop Reasons
| Reason | Strategy | Cause |
|---|---|---|
<upstream error message> | DeepLStrategy | Upstream batch request threw |
<upstream error message> | CopilotAIStrategy | Provider call threw (rate limit, network, auth) |
response length mismatch | CopilotAIStrategy | AI returned the wrong number of translations |
empty or non-string translation | CopilotAIStrategy | An empty string or non-string entry appeared in the response array |
placeholder count mismatch | any | Translation lost or invented a <cN/> placeholder |
non-string translation | any | Strategy returned a non-string entry for the unit |
Example: Send Drops to Sentry
use Sentry\State\Scope;
use function Sentry\captureMessage;
use function Sentry\withScope;
return [
'hooks' => [
'content-translator.translate:warning' => function ($unit, $reason, $previous) {
withScope(function (Scope $scope) use ($unit, $reason, $previous) {
$scope->setExtra('field', $unit->fieldKey);
$scope->setExtra('text_excerpt', mb_substr($unit->text, 0, 200));
if ($previous !== null) {
$scope->setExtra('upstream_error', $previous->getMessage());
}
captureMessage('Translation dropped: ' . $reason);
});
},
],
];
Field-Aware Preprocessing
The unit payload key lets you branch on the originating field:
return [
'hooks' => [
'content-translator.translate:before' => function ($text, $targetLanguage, $sourceLanguage, $type, $unit) {
// Tighter prompts for headlines
if (in_array($unit->fieldKey, ['headline', 'title'], true)) {
return trim($text);
}
return $text;
}
]
];
Notes
Hooks fire for every individual text – they may run hundreds of times during a batch translation. Keep them fast.
:after hook applies to the final stored content. Bugs in your hook propagate to disk – review carefully before deploying.