el-schneider / statamic-magic-translator
Translate Statamic content
Package info
github.com/el-schneider/statamic-magic-translator
Type:statamic-addon
pkg:composer/el-schneider/statamic-magic-translator
Fund package maintenance!
Requires
- deeplcom/deepl-php: ^1.0
- prism-php/prism: ^0.100
- statamic/cms: ^5.0 || ^6.0
Requires (Dev)
- laravel/pint: ^1.14
- orchestra/testbench: ^10.0 || ^11.1
- pestphp/pest: ^4.0
This package is auto-updated.
Last update: 2026-08-09 23:33:42 UTC
README
Statamic Magic Translator
Translate Statamic entry content across multi-site localizations using LLMs or DeepL — with full support for Bard, Replicator, Grid, and deeply nested content structures.
Features
- 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
Installation
composer require el-schneider/statamic-magic-translator
Publish the configuration:
php artisan vendor:publish --tag=statamic-magic-translator-config
CP Usage
Translating a single entry
- 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.
Bulk translation
- Select entries in a collection listing
- Choose Translate from the actions menu
- Pick target locales and options in the dialog
Translation dialog options
| 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. |
Staleness badges
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
CLI Usage
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.
Common examples
Preview what would be translated for a collection (safe, no changes):
php please statamic:magic-translator:translate --collection=pages --to=de --dry-run
Translate all missing pages entries into German and French asynchronously:
php please statamic:magic-translator:translate --collection=pages --to=de --to=fr --dispatch-jobs -n
Re-translate stale entries for CI/cron:
php please statamic:magic-translator:translate --collection=pages --include-stale --dispatch-jobs -n
Translate one specific entry to every site its collection supports:
php please statamic:magic-translator:translate --entry=abc-123
Options
| 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). |
Exit codes
| 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). |
Configuration
1. Exclude blueprints (optional)
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 ],
2. Choose a translation service
Prism (LLMs)
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.
DeepL
MAGIC_TRANSLATOR_SERVICE=deepl DEEPL_API_KEY=your-deepl-key
DeepL-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.
3. Set up a queue worker
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:work
Optionally configure a dedicated queue:
MAGIC_TRANSLATOR_QUEUE_CONNECTION=redis MAGIC_TRANSLATOR_QUEUE_NAME=translations
Content Structure Support
The 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.
Deeply nested structures (Bard → set → Replicator → set → Bard → …) work to arbitrary depth.
Customizing Prompts
Translation prompts are Blade views. Publish them to customize:
php artisan vendor:publish --tag=statamic-magic-translator-views
This copies prompt templates to resources/views/vendor/magic-translator/prompts/.
Per-language prompt overrides
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.
Available prompt variables
| Variable | Example |
|---|---|
$sourceLocale |
en |
$targetLocale |
de |
$sourceLocaleName |
English |
$targetLocaleName |
German |
$hasHtmlUnits |
true (Bard content present) |
$hasMarkdownUnits |
true (Markdown fields present) |
Events
Hook into the translation lifecycle:
BeforeEntryTranslation
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 });
AfterEntryTranslation
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 });
Custom Translation Service
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);
Requirements
- 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)
License
MIT
