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
- pestphp/pest: ^4.0
This package is auto-updated.
Last update: 2026-04-06 07:49:33 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' 'overrides' => [ 'de' => ['formality' => 'prefer_more'], ], ],
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
