Hooks
Preprocess, postprocess, or skip content with before/after hooks. Surface dropped translations with the warning hook.
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) |
content-translator.translate:before
Modify text before translation, or short-circuit translation entirely.
text
String
The text about to be translated.
targetLanguage
String
Target language code (
de, fr, en-gb, …).sourceLanguage
String | null
Source language code, or
null if not specified.type
String
Always
text for now. Reserved for future expansion.unit
TranslationUnit
The full
TranslationUnit (text, mode, fieldKey). Lets you branch on the originating field or table cell.options
ExecutionOptions
Typed
ExecutionOptions carrying both targetLanguage and sourceLanguage as TranslationLanguage value objects.unit and options were added alongside the strategy refactor. Existing closures that consume only the legacy keys keep working unchanged – Kirby's apply() matches by parameter name.config.php
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.
text
String
The translated text returned by the strategy.
originalText
String
The text as it entered the strategy (after
:before rewrites).targetLanguage
String
Target language code.
sourceLanguage
String | null
Source language code.
type
String
Always
text for now.unit
TranslationUnit
The unit that was sent.
options
ExecutionOptions
The execution options.
config.php
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 by a built-in strategy. The unit's source text is kept as-is; the rest of the batch continues. Silent by default – wire it to logging, Sentry, or Slack to surface drops in production.
unit
TranslationUnit
The unit that was dropped.
reason
String
Short tag explaining the drop (see table below).
previous
Throwable | null
The upstream exception, when applicable.
Drop Reasons
| Reason | Strategy | Cause |
|---|---|---|
<upstream error message> | DeepLStrategy | Upstream batch or single-unit request threw |
<upstream error message> | CopilotAIStrategy | Provider call threw (rate limit, network, auth) |
response length mismatch | CopilotAIStrategy | AI returned the wrong number of translations |
non-string translation | CopilotAIStrategy | A non-string entry appeared in the response array |
placeholder count mismatch | CopilotAIStrategy | Translation lost or invented a <cN/> placeholder |
Example: Send Drops to Sentry
config.php
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('mode', $unit->mode->value);
$scope->setExtra('text_excerpt', mb_substr($unit->text, 0, 200));
if ($previous !== null) {
$scope->setExtra('upstream_error', $previous->getMessage());
}
captureMessage('Translation dropped: ' . $reason);
});
},
],
];
A drop means the field still contains its source-language text. End users see English where they expected German. Treat warning volume as an SLO signal.
Use Cases
Protecting Specific Content
config.php
return [
'hooks' => [
'content-translator.translate:before' => function ($text) {
if (preg_match('/\[preserve\](.*?)\[\/preserve\]/s', $text, $matches)) {
return $matches[1];
}
return $text;
}
]
];
Custom Terminology Management
config.php
return [
'hooks' => [
'content-translator.translate:after' => function ($text, $originalText, $targetLanguage) {
$terminology = [
'de' => [
'dashboard' => 'Dashboard',
'workflow' => 'Arbeitsablauf',
],
'fr' => [
'dashboard' => 'tableau de bord',
'workflow' => 'flux de travail',
],
];
foreach ($terminology[$targetLanguage] ?? [] as $english => $translation) {
$text = str_ireplace($english, $translation, $text);
}
return $text;
}
]
];
Field-Aware Preprocessing
The new unit payload key lets you branch on the originating field:
config.php
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.
The
:after hook applies to the final stored content. Bugs in your hook propagate to disk – review carefully before deploying.