Translate Statamic entry content across multi-site localizations using LLMs or DeepL — with full support for Bard, Replicator, Grid, and deeply nested content structures.
- LLM & DeepL support: Translate via any Prism-supported provider (OpenAI, Anthropic, Gemini, Mistral, Ollama, …) or DeepL's dedicated translation API
- Deep content awareness: Recursively walks Bard fields, Replicators, Grids, and Tables — translates text while preserving structure, marks, and custom extensions
- Async processing: Control-panel translations run as queued jobs with retry and backoff; the CLI can run either synchronously or via dispatched jobs
- Sidebar UI: Auto-injected fieldtype with a translation dialog — pick target locales, track progress per locale, retry failures inline
- Bulk actions: Translate multiple entries at once from collection listings
- Artisan command:
statamic:magic-translator:translatefor scripted/automated bulk translation with dry-run, filtering, and sync/async modes - Staleness detection: Badges in the Sites panel show which localizations are up-to-date, outdated, or missing
- Customizable prompts: Blade views for system/user prompts, with per-language overrides
- Statamic v5 + v6
composer require el-schneider/statamic-magic-translatorPublish the configuration:
php artisan vendor:publish --tag=statamic-magic-translator-config- Open an entry in the control panel
- Click Translate in the sidebar
- Select target locales and options
- Click Translate selected
Each locale shows its own progress indicator. Failed translations display the error inline with a retry button.
- Select entries in a collection listing
- Choose Translate from the actions menu
- Pick target locales and options in the dialog
| Option | Description |
|---|---|
| Source locale | Defaults to origin entry. Can be changed to translate from any existing localization. |
| Generate slugs | Auto-generate slugs from the translated title. |
| Overwrite existing | When disabled (default), locales with existing translations are unchecked to prevent accidental overwrites. |
The Sites panel in the sidebar shows translation status per locale:
- 2h ago — translated, up-to-date
⚠️ outdated — origin has been updated since last translation- — — localization exists but was never machine-translated
The addon also ships with an artisan command for bulk and automated translation:
php please statamic:magic-translator:translate [options]Requires at least one filter:
--to,--collection,--entry, or--blueprint.
Preview what would be translated for a collection (safe, no changes):
php please statamic:magic-translator:translate --collection=pages --to=de --dry-runTranslate all missing pages entries into German and French asynchronously:
php please statamic:magic-translator:translate --collection=pages --to=de --to=fr --dispatch-jobs -nRe-translate stale entries for CI/cron:
php please statamic:magic-translator:translate --collection=pages --include-stale --dispatch-jobs -nTranslate one specific entry to every site its collection supports:
php please statamic:magic-translator:translate --entry=abc-123| Option | Description |
|---|---|
--to=* |
Target site handle (repeatable). Default: all sites each entry supports minus source site. |
--from= |
Source site handle. Default: entry origin site. |
--collection=* |
Filter by collection handle (repeatable). |
--entry=* |
Filter by entry ID (repeatable). |
--blueprint=* |
Filter by blueprint handle (repeatable). |
--include-stale |
Also re-translate entries where source was updated after target last_translated_at. |
--overwrite |
Re-translate everything regardless of existing state. |
--generate-slug |
Slugify translated title. |
--dispatch-jobs |
Dispatch queued jobs instead of running synchronously. |
--dry-run |
Print the plan without executing. |
-n, --no-interaction |
Skip confirmation prompt (required in CI/non-interactive environments). |
| Code | Meaning |
|---|---|
0 |
Success, dry run, empty plan, or user-declined confirmation. |
1 |
Partial failure (some translations failed). |
2 |
Command-level error (invalid options/handles, unsafe non-interactive run without -n). |
By default, the addon auto-injects its fieldtype into entry blueprints. Use
exclude_blueprints to opt out specific blueprints or whole collections:
// config/statamic/magic-translator.php
'exclude_blueprints' => [
'pages.redirect', // exact blueprint
'blog.*', // all blueprints in a collection
],Set your provider and model:
MAGIC_TRANSLATOR_SERVICE=prism
MAGIC_TRANSLATOR_PROVIDER=openai
MAGIC_TRANSLATOR_MODEL=gpt-5-mini
OPENAI_API_KEY=sk-...Any Prism-supported provider works — Anthropic, OpenAI, Gemini, Mistral, Ollama, etc. Just add the provider's API key to your .env and reference it in Prism's config.
MAGIC_TRANSLATOR_SERVICE=deepl
DEEPL_API_KEY=your-deepl-keyDeepL-specific options:
'deepl' => [
'api_key' => env('DEEPL_API_KEY'),
'formality' => 'default', // 'more', 'less', 'prefer_more', 'prefer_less'
'glossary' => env('DEEPL_GLOSSARY_ID'),
'overrides' => [
'de' => ['formality' => 'prefer_more', 'glossary' => 'glossary-id-for-german'],
],
],A DeepL glossary enforces your own terminology. Because a glossary is bound to a single language pair, set glossary for your main pair and add a per-language glossary under overrides for every additional target language. An override set to an empty string opts that language out of the global glossary.
Glossaries are matched on the target language only, so they assume a single source language. If you translate the same target from several source sites, configure the glossary for the pair you translate most and expect DeepL to reject the others.
If you already published config/statamic/magic-translator.php before upgrading, add 'glossary' => env('DEEPL_GLOSSARY_ID'), to its deepl array — Laravel merges published config shallowly, so DEEPL_GLOSSARY_ID alone has no effect.
Control-panel translations and CLI runs using --dispatch-jobs execute asynchronously. You need a queue driver other than sync and a running worker:
php artisan queue:workOptionally configure a dedicated queue:
MAGIC_TRANSLATOR_QUEUE_CONNECTION=redis
MAGIC_TRANSLATOR_QUEUE_NAME=translationsThe addon handles all idiomatic Statamic content patterns:
| Fieldtype | Handling |
|---|---|
| Text, Textarea | Translated as plain text |
| Markdown | Translated as markdown (formatting preserved) |
| Bard | Body text serialized with inline HTML tags, sets extracted recursively. Custom marks and extensions (e.g., Bard Texstyle) are preserved — the ProseMirror structure is never round-tripped through HTML. |
| Bard (raw markdown) | Starter kit entries storing markdown instead of ProseMirror JSON are detected and translated as markdown. |
| Replicator | Each set's fields are recursively extracted and translated. |
| Grid | Each row's columns are recursively extracted and translated. |
| Table | Each cell is translated as plain text. |
| Link | text property is translated, url is preserved. |
| Assets, Toggle, Integer, Select, … | Skipped (non-text fields are never translated). |
Fields marked localizable: false in the blueprint are always skipped. Individual fields can be excluded with translatable: false in the field config.
Custom fieldtypes from an addon or your own project are skipped, because a plain string can just as easily be a colour swatch or an ID as a meta title. Opt one in by declaring what it holds:
// config/statamic/magic-translator.php
'custom_fieldtypes' => [
'aardvark_seo_meta_title' => 'plain',
'my_addon_body' => 'markdown',
],Only plain and markdown can be declared. A fieldtype holding structured data cannot be opted in.
Deeply nested structures (Bard → set → Replicator → set → Bard → …) work to arbitrary depth.
Translation prompts are Blade views. Publish them to customize:
php artisan vendor:publish --tag=statamic-magic-translator-viewsThis copies prompt templates to resources/views/vendor/magic-translator/prompts/.
Different languages may need different instructions (e.g., formal "Sie" in German, polite form in Japanese):
// config/statamic/magic-translator.php
'prism' => [
'prompts' => [
'system' => 'magic-translator::prompts.system',
'user' => 'magic-translator::prompts.user',
'overrides' => [
'de' => ['system' => 'magic-translator::prompts.system-de'],
'ja' => ['system' => 'magic-translator::prompts.system-ja'],
],
],
],Create the override views (e.g., resources/views/vendor/magic-translator/prompts/system-de.blade.php) with language-specific instructions.
| Variable | Example |
|---|---|
$sourceLocale |
en |
$targetLocale |
de |
$sourceLocaleName |
English |
$targetLocaleName |
German |
$hasHtmlUnits |
true (Bard content present) |
$hasMarkdownUnits |
true (Markdown fields present) |
Hook into the translation lifecycle:
Fired before extraction. Modify the $units array to exclude or alter translation units.
use ElSchneider\MagicTranslator\Events\BeforeEntryTranslation;
Event::listen(BeforeEntryTranslation::class, function ($event) {
// $event->entry — the source entry
// $event->targetSite — target locale handle
// $event->units — mutable array of TranslationUnit objects
});Fired after translation, before save. Modify $translatedData to post-process the result.
use ElSchneider\MagicTranslator\Events\AfterEntryTranslation;
Event::listen(AfterEntryTranslation::class, function ($event) {
// $event->entry — the source entry
// $event->targetSite — target locale handle
// $event->translatedData — mutable array of translated entry data
});Implement the TranslationService contract to add your own backend (Google Translate, etc.):
use ElSchneider\MagicTranslator\Contracts\TranslationService;
use ElSchneider\MagicTranslator\Data\TranslationUnit;
class GoogleTranslateService implements TranslationService
{
public function translate(array $units, string $sourceLocale, string $targetLocale): array
{
// $units is an array of TranslationUnit objects
// Return the same array with translatedText set on each unit
return array_map(fn (TranslationUnit $unit) => $unit->withTranslation(
$this->callGoogleApi($unit->text, $sourceLocale, $targetLocale)
), $units);
}
}Bind it in a service provider:
$this->app->bind(TranslationService::class, GoogleTranslateService::class);- PHP 8.2+
- Statamic 5.0+ or 6.0+
- An async queue driver (
database,redis,sqs, …) with a running worker - At least one translation provider configured (LLM API key or DeepL API key)
MIT
