Architecture
This page is a technical overview for the agencies that maintain, extend or integrate with the extension.
Target platform: Shopware ~6.6.9 || ~6.7.0, PHP >= 8.2, Vue 3 Administration.
INFO
This page describes the extension that operates inside your Shopware installation. This documentation describes the managed translation service only as merchants see it: the plans, the credits, the service tiers and the quota. See modernice All-in-One.
Constants
| Item | Value |
|---|---|
| Plugin class | Nice\Translate\NiceTranslate |
| Technical name | NiceTranslate |
| Composer package | modernice/sw-translate (type shopware-platform-plugin) |
| Namespace root | Nice\Translate\ → src/ (PSR-4) |
| Version constant | NiceTranslate::VERSION |
| PHP constraint | >=8.2 |
| Shopware constraint | ~6.6.9 || ~6.7.0 |
| Database table prefix | nice_translate_ |
| System config domain | NiceTranslate.config. |
| API route prefix | /api/_action/nice-translate/ |
| API route name prefix | api.action.nice_translate. |
| ACL key | nice_translate (roles viewer, editor) |
| Provider service tag | nice_translate.provider |
| Administration module | nice-translate |
There is no config.xml. Each setting is under the NiceTranslate.config. domain. The settings page of the extension writes the settings, and the PHP code reads them with in-code defaults. The Settings reference contains the full key list.
Five files contain the service wiring, and src/Resources/config/services.xml imports all of them: entity.xml, provider.xml, engine.xml, job.xml and api.xml. src/Resources/config/routes.xml loads the controllers by attribute:
<import resource="../../Api/**/*Controller.php" type="attribute"/>Component overview
Administration (Vue module `nice-translate`)
│ REST /api/_action/nice-translate/*
▼
API controllers (Nice\Translate\Api)
│
▼
JobService ──▶ Message queue (low priority) ──▶ JobStartHandler ──▶ TranslateBatchHandler ×N
│
▼
EntityTranslator (Engine)
│ PlaceholderGuard
│ GlossaryApplier
│ Slot/CustomFields/Snippet walkers
│ RecordTracker (hashes)
│
▼
ProviderRegistry ──▶ TranslationProviderInterface
(DeepL, Google, OpenAI,
Anthropic, Gemini, Mistral,
Managed)The extension writes the translated content through the repository of the target entity, into the native *_translation tables of Shopware. It keeps no private copy of a translation.
Request path
A manual translation run goes through these steps. Each step commits before the next step starts, therefore a process that dies during the run leaves a state that the recovery can use.
Administration to API. The module calls the Admin API through
NiceTranslateApiService(apiEndpoint = '_action/nice-translate'). An ACL guard protects each route. See Admin API.JobController::create()toJobService::create().JobConfig::fromArray()normalises theconfigobject of the body. The validation then examines whether the provider is known and configured, whether the target languages resolve, and whether the extension supports the entities. The extension inserts anice_translate_jobrow with the statusqueued, and dispatchesJobStartMessage.If the Messenger dispatch throws, the call still returns
200with the job id. The committedqueuedrow is the durable start marker, and the scheduled recovery relays it.JobStartHandler. One transaction does four things: it enumerates the affected entity ids for each entity and target language, divides them into batches of the configuredbatchSize, writes the completenice_translate_job_batchmanifest, records the enumeration errors, and sets the job torunningwith its exacttotal_items.JobBatchDispatcherthen keeps a bounded window of in-flightTranslateBatchMessages full, therefore a large catalogue does not flood the transport.TranslateBatchHandler. It reads the job status again for each message, therefore it honours a cancellation between the batches. It takes a connection-scoped advisory lock on the stable request id of the batch. If a stored receipt exists, it replays that receipt. Otherwise it callsEntityTranslator::translateBatch(). The content writes and the receipt commit together. The progress consumption advances the counters and insertsnice_translate_batch_completion. After the consumption of the batch, the dispatcher refills the window by one.Finalisation. When
processed + failed + skipped >= total,JobProgressUpdater::finish()sets the terminal status and inserts an immutable snapshot intonice_translate_finished_event, in the same transaction.JobFinishedEventRelaydispatchesTranslationJobFinishedEventafter the commit. See Events & Flow Builder.
The finalizer sets completed or completed_with_errors and nothing else (IF(failed_items > 0, …)). A job reaches failed only through JobProgressUpdater::failQueued(), which processes a queued job whose manifest preparation threw. JobService::cancel() sets cancelled, and it emits no finished event.
Job statuses: queued, running, completed, completed_with_errors, failed, cancelled.
Messages and handlers
All three messages implement LowPriorityMessageInterface of Shopware, and they carry scalar ids and arrays only. Workers can therefore prioritise the core queue traffic. A real isolation of the queue still depends on your Messenger configuration and worker configuration.
| Message | Handler | Purpose |
|---|---|---|
JobStartMessage | JobStartHandler | Build the batch manifest and start the job |
TranslateBatchMessage | TranslateBatchHandler | Process or replay exactly one batch |
RevertJobMessage | RevertJobHandler | Keyset-paginated guarded revert of the applied history of a job |
TranslateBatchMessage carries a stable requestId that is the idempotency key. The handler uses a valid UUID from the caller without changes. Otherwise it derives the id deterministically from the immutable payload, therefore each Messenger redelivery of the same work uses the same key. A message with jobId === null is a jobless batch from on-save automation, and its configOverride then carries the serialised JobConfig.
The extension registers two scheduled tasks:
| Task | Task name | Default interval | Handler runs |
|---|---|---|---|
AutoTranslateTask | nice_translate.auto_translate | 3600 s | History pruning on each tick, then the creation of a scheduled job when one is due |
BatchDispatchTask | nice_translate.dispatch_batches | 60 s | JobBatchDispatcher::recover() and JobFinishedEventRelay::recover() |
AutoTranslateTask ticks hourly and translates a maximum of once per scheduledIntervalHours (default 24). The history pruning happens on each tick. The job creation happens only while scheduledEnabled is on and the interval passed. Events & Flow Builder contains its full gate chain.
Both tasks need active workers (messenger:consume for the transports, and scheduled-task:run).
Engine pipeline
EntityTranslator::translateBatch(TranslationTask $task): BatchResult is the provider-agnostic core. One call processes one batch: one entity, a list of ids, one target language. The stages operate in this order:
| # | Stage | What happens |
|---|---|---|
| 0 | Early exits | An empty id list returns an empty BatchResult. The snippet pseudo-entity branches into SnippetTranslator |
| 1 | Resolve the provider and the locales | ProviderRegistry::get(), then the Shopware locale code of the source language and the target language |
| 2 | Normalise the options, resolve the fields | The engine merges the job options with the system config, and TranslatableFieldResolver derives the FieldSpec list |
| 3 | Dual read | Once in the source-language context with the inheritance considered, and once in the raw target-language context. For cms_page the criteria adds sections.blocks.slots |
| 4 | Load the fingerprints | RecordTracker::getStates() for the entity, and the cms_slot config states for CMS pages |
| 5 | Phase 1, collect the units | The per-field skip gates decide which values the engine sends |
| 6 | Phase 2, provider calls | Glossary preparation, grouping by format (html / text), chunking, bisection after a failure |
| 7 | Phase 3, post-processing | Glossary replace-terms, placeholder restore, maximum-length enforcement |
| 8 | Phase 4, write | The engine assembles the custom-field and slot structures again, and writes for each entity inside JobBatchStore::transactionalResult(), therefore the content and the receipt commit together |
The engine evaluates the field-level skip gates of stage 5 in this order:
mode === 'missing'and a direct target value already exists → skip.- No tracked state, or no target value at all → do not skip.
skipUnchangedand the source hash did not change → skip.protectManualEditsand the current target value no longer agrees with the last value that the extension wrote → skip.
The writes go through the repository of the entity, as a native translations payload, ['id' => …, 'translations' => [$targetLanguageId => [...fields]]], inside the context state nice_translate.writing, therefore the on-save subscriber never loops.
PlaceholderGuard replaces the Twig expressions, the %name%-style placeholders, {0}, %s, %d and the URLs with ⟦n⟧ tokens before the translation. A token that does not come back throws PlaceholderLostException and fails that field. The extension never writes corrupted content. Safety mechanisms describes the behaviour in detail.
The getMaxBatchSize() and getMaxChunkBytes() values of the provider bound the chunking. EntityTranslator::CHUNK_BYTE_LIMIT (80 000 bytes) caps it further. See Custom providers for the participation of a provider in this.
Persistence
The extension owns eleven tables. Five of them have DAL definitions, registered with shopware.entity.definition. The other six are DBAL-only records, without a repository and without an Admin API resource.
| Table | DAL entity | Purpose |
|---|---|---|
nice_translate_job | TranslationJobDefinition | The job header: status, title, config JSON, provider, languages, counters, cost or credits, managed usage and quota, timestamps |
nice_translate_job_error | JobErrorDefinition | The per-record errors of a job, with a foreign key to the job and a cascade delete |
nice_translate_glossary | GlossaryDefinition | The glossary terms: keep or replace mode, per-language forced translations, case sensitivity, active flag |
nice_translate_usage | UsageDefinition | The monthly usage per provider, unique on (provider_id, period) |
nice_translate_history | TranslationHistoryDefinition | The previous and new field values, and the applied or reverted state for the guarded rollback |
nice_translate_record | — (DBAL only) | The sha1 fingerprints of the source value and the written target value, per entity, field and language |
nice_translate_job_batch | — (DBAL only) | The durable batch manifest, and the transient counter and accounting receipt until the progress commits |
nice_translate_jobless_batch | — (DBAL only) | The durable on-write batch message and its completion tombstone |
nice_translate_batch_completion | — (DBAL only) | The stable batch request ids that the job counters already contain |
nice_translate_managed_usage_request | — (DBAL only) | The managed upstream request ids that the monthly usage already contains |
nice_translate_finished_event | — (DBAL only) | The immutable terminal job snapshots for the Flow event relay |
Each DAL field carries ApiAware(AdminApiSource::class). The entities are therefore reachable through the Admin API only, and never through the Store API.
A job row stores a narrow slice of the managed usage. ManagedProvider validates the full upstream accounting response, then persists six fields: requestedTier, deliveredTier, multiplier, sourceCharacters, debitedCredits and providerAttempts. It never writes the route identifiers and upstream model identifiers that it validated, therefore Administration API clients cannot observe the topology of the managed service.
Seven migrations create and extend this schema: the base schema, the translation history, the managed accounting, the durable batches, the durable start relay, the finished-event outbox, and the batch safety and cleanup. None of them implements updateDestructive().
NiceTranslate::uninstall() returns early when the merchant keeps the user data. Otherwise it drops all eleven tables and deletes the NiceTranslate.% rows from system_config. It never deletes the translations in the native *_translation tables.
Durability
Translation work is expensive, and for BYOK providers it is not idempotent upstream. Troubleshooting describes the operation of the mechanisms below.
| Mechanism | Guarantee | How |
|---|---|---|
| Durable manifests | The work set is knowable from the database alone, without a question to the transport about what it still holds | nice_translate_job_batch (and nice_translate_jobless_batch for on-save work) records the complete list of batches, in the same transaction that sets the job to running. The recovery waits before it claims work that nobody dispatched, relays incomplete dispatches again after a safety window, and rotates one batch per job per round, therefore a large old job cannot starve newer work |
| Execution receipts | A crash cannot leave translated content without its counters and managed usage, and cannot leave counters without content | The translated content of a batch and its receipt commit in one transaction. The receipt holds the counters, the error rows and the accounting metadata, and never translated content. The progress consumption atomically clears the receipt payload, advances the job counters and inserts a nice_translate_batch_completion row |
| At-most-once provider claim (BYOK) | A replay without a receipt after the claim boundary becomes a durable terminal failure and does not spend money again | Before BYOK work can reach its non-idempotent provider, the manifest commits an attempt claim. A worker that dies after the claim but before the provider call gives up automatic availability, because the alternative is a second payment. Managed calls do not use this claim. Their stable sequence of idempotency keys keeps the automatic replay safe after an ambiguous failure |
| Terminal event outbox | The delivery is at-least-once, and the snapshots outlive the job | The extension inserts the nice_translate_finished_event row in the same transaction that finalises the job, and the relay dispatches only after that commit. A failed dispatch keeps its lease timestamp, and the recovery retries it after the stale window. The snapshots are immutable and have no foreign key to the job |
| Duplicate-delivery serialisation | Two deliveries of the same batch cannot both enter the provider path, and a redelivery upserts the error rows instead of a duplication | JobBatchExecutionLock takes a connection-scoped advisory lock (GET_LOCK/RELEASE_LOCK), keyed by the stable request id, and turns a completed request id into a no-op. The error rows use deterministic ids from the request id and the item index |
WARNING
The advisory lock needs a sticky, direct MySQL or MariaDB session for the duration of one handler call. Database proxies that pool transactions or multiplex sessions are unsupported, unless they explicitly preserve the session affinity for GET_LOCK and RELEASE_LOCK.
Selected bounds for your capacity planning:
| Bound | Value |
|---|---|
| In-flight batch messages per job | 100 |
Batch size (batchSize) | default 25, clamped to a minimum of 5 and a maximum of 200 |
| Error rows persisted per job | 500 |
| Recovery delay before a claim on undispatched work | 60 s |
| Safety window before a second relay of incomplete dispatches | 900 s |
| Stale window of a finished-event claim | 300 s |
| Retention of receipts and completion markers | 45 days |
| History ids per revert or re-apply request | 200 |
Directory layout
| Path | Contents |
|---|---|
Api/ | The Admin API controllers: job, provider, catalog, usage, glossary, history |
Command/ | The CLI commands nice-translate:run and nice-translate:providers |
Core/Content/ | The DAL definitions: TranslationJob (and the JobError aggregate), Glossary, Usage, TranslationHistory |
Engine/ | The translation pipeline, its DTOs and the content walkers |
Job/ | The job orchestration: config, service, batch store, dispatcher and lock, queue messages and handlers, scheduled tasks, subscribers, flow event |
Migration/ | The schema migrations: base schema, translation history, managed accounting, durable batches, durable start relay, finished-event outbox, batch safety and cleanup |
Provider/ | The provider contracts, the registry, the locale mapping, the abstract HTTP and LLM bases, the concrete providers |
Subscription/ | The managed-subscription integration on top of Shopware In-App Purchase |
Usage/ | The usage records (UsageRecorder) |
Resources/config/ | services.xml, the per-layer service files, and routes.xml |
Resources/app/administration/ | The Vue Administration module, its components, extensions and snippets |
Extension points
| Extension point | Where to start |
|---|---|
| Add a translation provider | Implement TranslationProviderInterface and tag the service nice_translate.provider. The optional SupportsLiveModelsInterface adds the live model listing. See Custom providers |
| React to finished jobs | The Flow Builder trigger nice_translate.job.finished, or a PHP subscriber. See Events & Flow Builder |
| Call the extension from your own code | The routes under /api/_action/nice-translate/. See Admin API |
| Depend on the privileges of the extension | The roles nice_translate.viewer and nice_translate.editor. See Permissions (ACL) |