Skip to content

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

ItemValue
Plugin classNice\Translate\NiceTranslate
Technical nameNiceTranslate
Composer packagemodernice/sw-translate (type shopware-platform-plugin)
Namespace rootNice\Translate\src/ (PSR-4)
Version constantNiceTranslate::VERSION
PHP constraint>=8.2
Shopware constraint~6.6.9 || ~6.7.0
Database table prefixnice_translate_
System config domainNiceTranslate.config.
API route prefix/api/_action/nice-translate/
API route name prefixapi.action.nice_translate.
ACL keynice_translate (roles viewer, editor)
Provider service tagnice_translate.provider
Administration modulenice-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:

xml
<import resource="../../Api/**/*Controller.php" type="attribute"/>

Component overview

text
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.

  1. Administration to API. The module calls the Admin API through NiceTranslateApiService (apiEndpoint = '_action/nice-translate'). An ACL guard protects each route. See Admin API.

  2. JobController::create() to JobService::create(). JobConfig::fromArray() normalises the config object 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 a nice_translate_job row with the status queued, and dispatches JobStartMessage.

    If the Messenger dispatch throws, the call still returns 200 with the job id. The committed queued row is the durable start marker, and the scheduled recovery relays it.

  3. JobStartHandler. One transaction does four things: it enumerates the affected entity ids for each entity and target language, divides them into batches of the configured batchSize, writes the complete nice_translate_job_batch manifest, records the enumeration errors, and sets the job to running with its exact total_items. JobBatchDispatcher then keeps a bounded window of in-flight TranslateBatchMessages full, therefore a large catalogue does not flood the transport.

  4. 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 calls EntityTranslator::translateBatch(). The content writes and the receipt commit together. The progress consumption advances the counters and inserts nice_translate_batch_completion. After the consumption of the batch, the dispatcher refills the window by one.

  5. Finalisation. When processed + failed + skipped >= total, JobProgressUpdater::finish() sets the terminal status and inserts an immutable snapshot into nice_translate_finished_event, in the same transaction. JobFinishedEventRelay dispatches TranslationJobFinishedEvent after 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.

MessageHandlerPurpose
JobStartMessageJobStartHandlerBuild the batch manifest and start the job
TranslateBatchMessageTranslateBatchHandlerProcess or replay exactly one batch
RevertJobMessageRevertJobHandlerKeyset-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:

TaskTask nameDefault intervalHandler runs
AutoTranslateTasknice_translate.auto_translate3600 sHistory pruning on each tick, then the creation of a scheduled job when one is due
BatchDispatchTasknice_translate.dispatch_batches60 sJobBatchDispatcher::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:

#StageWhat happens
0Early exitsAn empty id list returns an empty BatchResult. The snippet pseudo-entity branches into SnippetTranslator
1Resolve the provider and the localesProviderRegistry::get(), then the Shopware locale code of the source language and the target language
2Normalise the options, resolve the fieldsThe engine merges the job options with the system config, and TranslatableFieldResolver derives the FieldSpec list
3Dual readOnce 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
4Load the fingerprintsRecordTracker::getStates() for the entity, and the cms_slot config states for CMS pages
5Phase 1, collect the unitsThe per-field skip gates decide which values the engine sends
6Phase 2, provider callsGlossary preparation, grouping by format (html / text), chunking, bisection after a failure
7Phase 3, post-processingGlossary replace-terms, placeholder restore, maximum-length enforcement
8Phase 4, writeThe 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:

  1. mode === 'missing' and a direct target value already exists → skip.
  2. No tracked state, or no target value at all → do not skip.
  3. skipUnchanged and the source hash did not change → skip.
  4. protectManualEdits and 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.

TableDAL entityPurpose
nice_translate_jobTranslationJobDefinitionThe job header: status, title, config JSON, provider, languages, counters, cost or credits, managed usage and quota, timestamps
nice_translate_job_errorJobErrorDefinitionThe per-record errors of a job, with a foreign key to the job and a cascade delete
nice_translate_glossaryGlossaryDefinitionThe glossary terms: keep or replace mode, per-language forced translations, case sensitivity, active flag
nice_translate_usageUsageDefinitionThe monthly usage per provider, unique on (provider_id, period)
nice_translate_historyTranslationHistoryDefinitionThe 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.

MechanismGuaranteeHow
Durable manifestsThe work set is knowable from the database alone, without a question to the transport about what it still holdsnice_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 receiptsA crash cannot leave translated content without its counters and managed usage, and cannot leave counters without contentThe 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 againBefore 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 outboxThe delivery is at-least-once, and the snapshots outlive the jobThe 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 serialisationTwo deliveries of the same batch cannot both enter the provider path, and a redelivery upserts the error rows instead of a duplicationJobBatchExecutionLock 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:

BoundValue
In-flight batch messages per job100
Batch size (batchSize)default 25, clamped to a minimum of 5 and a maximum of 200
Error rows persisted per job500
Recovery delay before a claim on undispatched work60 s
Safety window before a second relay of incomplete dispatches900 s
Stale window of a finished-event claim300 s
Retention of receipts and completion markers45 days
History ids per revert or re-apply request200

Directory layout

PathContents
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 pointWhere to start
Add a translation providerImplement TranslationProviderInterface and tag the service nice_translate.provider. The optional SupportsLiveModelsInterface adds the live model listing. See Custom providers
React to finished jobsThe Flow Builder trigger nice_translate.job.finished, or a PHP subscriber. See Events & Flow Builder
Call the extension from your own codeThe routes under /api/_action/nice-translate/. See Admin API
Depend on the privileges of the extensionThe roles nice_translate.viewer and nice_translate.editor. See Permissions (ACL)

modernice extensions for Shopware 6. Shopware is a trademark of shopware AG — this documentation is not affiliated with shopware AG.