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.

HookWhenReturn
content-translator.translate:beforeBefore a unit is sent to the strategyModified text (string)
content-translator.translate:afterAfter the strategy returns a translationModified text (string)
content-translator.translate:warningWhen 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

ReasonStrategyCause
<upstream error message>DeepLStrategyUpstream batch or single-unit request threw
<upstream error message>CopilotAIStrategyProvider call threw (rate limit, network, auth)
response length mismatchCopilotAIStrategyAI returned the wrong number of translations
non-string translationCopilotAIStrategyA non-string entry appeared in the response array
placeholder count mismatchCopilotAIStrategyTranslation 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.