Appearance
Translations
Overview
Translations let you provide your menu and customer-facing content in every language you have configured. You edit translations per language, and you can auto-translate any content that is still missing a translation.
Key Purpose: Translate menu and customer-facing content into your configured languages.
Purpose
This page lets you view and edit translations for each non-default language, and trigger auto-translation for content that is incomplete. The default language is treated as the source text; every other configured language can be filled in manually or generated automatically.
Key Concepts
- Translation Entities: The translatable entity types are defined by the
TranslationEntitiesenum, which has these cases:Item,DisplayGroup,VariantGroup,Category,Modifier,Loyalty,Notification,Receipt,OrderInstructions,PagerInstructions,TableSection,WelcomePopup, andUpsellScreen. Not all of them are wired into the manual editor or the bulk auto-translator (see below).NotificationandReceiptare placeholders (TODO no-ops) in every code path. Pager content is handled inside Order Instructions rather than as its ownPagerInstructionseditor entity. - Manual editor entities (FullscreenTranslationDialog): The manual editor always shows tabs for Items, Display Groups, Categories, Modifiers (modifier groups and their individual modifiers), Order Instructions (labeled "Customer Flow", which also covers pager title/subtitle), Welcome Popup, Upsell Screen, and Table Sections. Variant Groups is shown only in test/staging environments or for Square-integrated merchants. Loyalty is shown only in test/staging environments.
- Bulk auto-translate entities: Bulk auto-translate (
bulkTranslate) handles Items, Display Groups, Variant Groups, Categories, Modifiers, Loyalty, Order Instructions (including pager), Pager Instructions, Table Sections, Upsell Screen and Welcome Popup. The auto-translate dialog UI offers Items, Variant Groups, Display Groups, Categories, Modifiers, Loyalty, Order Instructions ("Customer Flow"), Table Sections and Welcome Popup, filtered to those that are non-empty and incomplete.
Welcome Popup was previously offered in the dialog but silently ignored — the bulk dispatcher had no case for it, so selecting it did nothing and still reported success. The arm exists now. Because the save-time job already fans a popup message out to every language, bulk only matters for a language created after the popup was last saved.
- Default vs. Translating Language: Content is stored as a bag keyed by language. The
defaultkey holds the read-only source text; each other key holds the translation for that language. For most menu entities the bag lives underdetails(e.g.details.default.name,details.<Language>.name); for individual modifiers and loyalty rewards it lives undername(e.g.name.default,name.<Language>). - Language Keys Are Names, Not Codes: Language bags are keyed by the full English language name (
Dutch,French,German,Spanish,Portuguese,Italian,English), not the two-letter ISO code. The ISO code is only used internally when calling the AI provider. - Auto-Translation via Azure OpenAI: Auto-translation calls an Azure OpenAI chat-completions deployment (default deployment
translation-4o-mini) using a restaurant/menu-specific prompt. It is configured through theservices.azure_translationconfig, backed by theAZURE_OPENAI_TRANSLATION_ENDPOINT,AZURE_OPENAI_TRANSLATION_KEY,AZURE_OPENAI_TRANSLATION_DEPLOYMENT, andAZURE_OPENAI_TRANSLATION_API_VERSIONenvironment variables. - Incompleteness Detection:
LanguageService::checkTranslationqueries each checked entity per language and reports, for each entity type, whether anything is still untranslated (status) and whether that entity type has any records at all (is_empty). It checks Items, Display Groups, Variant Groups, Categories, Modifiers, Loyalty, Table Sections, Order Instructions (including pager), and Upsell Screen. It does not check Welcome Popup, Notifications, or Receipts. The completeness query only looks at thename(ortitle/subtitle) fields — the description-field check is present in code but commented out. The UI uses these flags to decide which languages are incomplete and which entity types are selectable for auto-translate.
Actions
Edit Translations Manually
Open a language's translation editor (the fullscreen translation dialog) to see source text side-by-side with the translation field for that language. Pick an entity type tab (Items, Categories, Modifiers, Order Instructions, Welcome Popup, Upsell Screen, etc.) and enter or edit translations row by row.
Clearing a field works. Emptying a Customer Flow (pager / order instruction) translation and saving used to silently keep the old text: the platform converts a submitted empty string to null, and the presence check read "cleared" as "not submitted", so the write was skipped. An explicitly emptied field is now persisted as empty, while a section the editor never submitted still leaves its stored translation untouched.
Find a Row by PLU
The Items and Variant Groups tabs show each row's PLU as a badge, and the search box matches on it as well as on the source and translated names. Other entity types have no PLU and search only the two names.
Auto-Translate Missing Content
For a language with incomplete content, open the auto-translate dialog, select which entity types to translate (only types that currently have missing translations are selectable), and run it. The system fills in only the slots that have default-language source text and no existing translation. The auto-translate dialog covers Items, Variant Groups, Display Groups, Categories, Modifiers, Loyalty, Order Instructions (including pager), Table Sections and Welcome Popup.
The run happens in the background: the dialog closes as soon as the job is queued and a progress bar appears on the Translations page showing which entity type is being processed and how many are done. You can leave the page — reopening it picks the progress bar back up while the job is still running. Only one bulk translation can run per merchant at a time; starting a second while one is in flight is rejected. (Verified: TranslationController::bulkTranslate returns 202, or 409 when hasActiveBulkTranslateJob().)
Review Translation Status
The languages list flags each non-default language as complete or incomplete. The auto-translate dialog breaks incompleteness down by entity type.
Location
- Backoffice Route:
/settings/translations(namesettings-translations, registered insrc/plugins/1.router/additional-routes.ts) - Backoffice Component:
src/views/settings/TranslationsComponent.vue, which opensTranslations/FullscreenTranslationDialog.vue(manual editor) andTranslations/AutoTranslateDialog.vue(bulk). Per-entity editors live undersrc/views/settings/Translations/, e.g.ItemTranslationComponent.vue,ModifierTranslationComponent.vue,UpsellScreenTranslationComponent.vue,WelcomePopupTranslationComponent.vue,OrderInstructionsTranslationComponent.vue. Store module:src/store/modules/translation.ts. - Backend Controller:
app/Http/Controllers/Api/TranslationController.php→TranslationOrchestrator→TranslationService - Backend Service:
app/Services/BackOffice/Settings/TranslationService.php(completeness viaLanguageService::checkTranslation) - Permissions:
view-translation(read),edit-translation(save / auto-translate)
API Endpoints
All routes are under the /back-office/settings/translations prefix.
| Method | Path | Purpose | Permission |
|---|---|---|---|
| GET | / | Get translations for an entity type in a given language | view-translation |
| POST | / | Save a single entity's translation | edit-translation |
| GET | /datatable | Paginated translation rows for an entity type | view-translation |
| POST | /translate | Translate one text string on demand | edit-translation |
| GET | /bulk/{id} | Per-language completeness overview by entity type | view-translation |
| POST | /bulk/{id} | Queue a bulk auto-translate for the selected entity types — returns 202 Accepted, or 409 Conflict if a job is already running for the vendor | edit-translation |
| GET | /bulk-progress | Current bulk-translate job progress for the vendor (null when none) | view-translation |
Bulk auto-translate is asynchronous
POST /bulk/{languageId} does not translate inline. It resets the vendor's progress record, dispatches a BulkTranslateJob to the queue, and immediately returns HTTP 202 with the initial progress record wrapped as { "success": true, "data": { ... } }. If a job is already running for the vendor the endpoint short-circuits with HTTP 409 and { "success": false, "message": "A bulk translation job is already running for this vendor." } — one bulk job per vendor at a time.
Progress is then polled from GET /bulk-progress, which returns { "success": true, "data": <progress|null> }. The progress record carries id, language_id, status (pending / processing / completed / failed), total_entities, completed_entities, current_entity, progress_percentage, error_message, created_at, and updated_at.
In the back office two things poll after you start a run:
- The Translations page polls
GET /bulk-progressevery 2 seconds and drives a progress bar showing the current entity type and an "n of m" count. The bar is visible only while the status ispendingorprocessing. The page also polls on mount, so reloading mid-run picks the progress bar back up. When the job leaves an active status, polling stops and the language list is refetched. (Verified:useTranslationProgress().startPollingdefaultintervalMs = 2000,src/composables/useTranslationProgress.tslines 24-41; both call sites pass2000explicitly insrc/views/settings/TranslationsComponent.vuelines 51 and 63; the bar isTranslations/TranslationProgressBar.vue.) - The auto-translate dialog separately re-checks completeness every 5 seconds, up to 24 attempts (about 2 minutes), and closes out with a success toast once the target language no longer reports incomplete entities. Hitting the attempt cap just stops polling quietly — the job keeps running server-side. (Verified:
POLL_INTERVAL_MS = 5000,POLL_MAX_ATTEMPTS = 24,src/views/settings/Translations/AutoTranslateDialog.vuelines 21-22 and 99-145.)
(Verified: TranslationController::bulkTranslate / getBulkTranslateProgress, upvendo-backend app/Http/Controllers/Api/TranslationController.php lines 64-95; TranslationOrchestrator::bulkTranslate / getBulkTranslateProgress / hasActiveBulkTranslateJob, lines 58-92; routes routes/api/backoffice/settings/translations.php lines 12-14; front-end action src/store/modules/translation.ts lines 336-341 and the TranslationJobProgress type at lines 10-23.)
What Can Be Translated
Menu Items, Display Groups, Variant Groups, Categories
| Field | Translatable | Storage |
|---|---|---|
| Name | ✓ | details.<Language>.name |
| Description | ✓ | details.<Language>.description |
Modifiers
| Field | Translatable | Storage |
|---|---|---|
| Modifier Group Name | ✓ | details.<Language>.name |
| Modifier Group Description | ✓ | details.<Language>.description |
| Individual Modifier (Option) Name | ✓ | name.<Language> |
Loyalty
| Field | Translatable | Storage |
|---|---|---|
| Reward Name | ✓ | name.<Language> |
Table Sections
| Field | Translatable | Storage |
|---|---|---|
| Section Name | ✓ | details.<Language>.name |
Order & Pager Instructions (one "Customer Flow" entity)
Order and pager instructions live on the device profile and are edited together under the single "Customer Flow" (Order Instructions) entity — there is no separate Pager Instructions editor tab. Both are auto-translatable.
| Field | Translatable | Storage |
|---|---|---|
| Order Instructions (all / for here / takeout) | ✓ | device profile order_instructions.translations.<Language> |
| Pager Title / Subtitle | ✓ | device profile pager_instructions.translations.<Language> |
Welcome Popup & Upsell Screen
| Field | Translatable | Auto-translate | Storage |
|---|---|---|---|
| Welcome Popup Message | ✓ | ✓ | location online_ordering_setting.welcome_popup.message.<Language> |
| Upsell Group Title / Subtitle | ✓ | ✓ | upsell group details.<Language>.title / .subtitle |
Welcome Popup is editable in the manual editor and covered by bulk auto-translate. It is still not included in the completeness check, so a language with an untranslated popup message is not flagged as incomplete.
The storefront reads the published constants, not the database. A translated welcome-popup message only reaches customers once those constants are re-published. Auto-translating a popup message now triggers that republish itself after the per-language slots are written. Previously it relied on the save's own republish, which runs concurrently on the same queue and usually won the race — mirroring the message while it still held only the source language, so customers stayed on the source text until some unrelated reload happened to fix it.
Notifications and Receipts appear in the
TranslationEntitiesenum but are not yet handled by the translation pipeline (TODO no-ops in every path). Receipt custom text is auto-translated separately when you add a language or save Receipt Settings — see Receipts.
How Translations Are Stored
Each translatable field is stored as a per-language bag. The default key is the read-only source text; every other key is the translation for that language, keyed by the full English language name:
json
{
"details": {
"default": {
"name": "Margherita Pizza",
"description": "Verse mozzarella, San Marzano tomaten, basilicum"
},
"English": {
"name": "Margherita Pizza",
"description": "Fresh mozzarella, San Marzano tomatoes, basil"
},
"French": {
"name": "Pizza Margherita",
"description": "Mozzarella fraîche, tomates San Marzano, basilic"
}
}
}Individual modifiers and loyalty rewards instead use a name bag:
json
{
"name": {
"default": "Large",
"French": "Grand",
"German": "Groß"
}
}When a translation for the selected language is missing, the system falls back to the default source text.
Auto-Translation
Auto-translation is provided by an Azure OpenAI chat-completions deployment, not a dedicated translation API. The default deployment name is translation-4o-mini. It is configured by:
| Environment Variable | Purpose |
|---|---|
AZURE_OPENAI_TRANSLATION_ENDPOINT | Azure OpenAI endpoint |
AZURE_OPENAI_TRANSLATION_KEY | API key |
AZURE_OPENAI_TRANSLATION_DEPLOYMENT | Deployment name (default translation-4o-mini) |
AZURE_OPENAI_TRANSLATION_API_VERSION | API version (default 2024-08-01-preview) |
If the endpoint or key is not configured, auto-translation logs an error and returns nothing — translations are left unchanged.
Behaviour:
- Uses a restaurant/menu-specific prompt (the "QSR translation rules") that translates menu labels word-by-word, preserves brand and dish names, and leaves already-foreign words verbatim. The prompt also appends merchant context (business category and country) when available.
- The Azure call runs at temperature 0 with a 30-second timeout, and retries up to twice on 429/500/502/503/504 with exponential backoff (~1s, ~2s).
- Bulk auto-translate only fills slots that have default-language source text and no existing translation; it never overwrites an existing translation.
- Texts are batched (up to 50 per call, requested as a JSON
{"translations": [...]}object); if a batch call fails or the count doesn't match, the service falls back to translating each text individually. - Successful translations are cached (database cache) for 30 days, keyed by text + target ISO code + source ISO code + a rules-version tag; caching is disabled in local/docker environments.
- A length guard discards an obviously over-long result (more than 2x the original length) and keeps the original instead.
- Running auto-translate (or saving a manual translation) fires a
ReloadMenuevent for every location so connected devices pick up the change.
Auto-translations are best-effort machine translations and should be reviewed for restaurant-specific terminology. (The accuracy of the external Azure OpenAI model output is not verified here.)
Translation Status
For each language, checkTranslation returns, per entity type, two flags:
| Flag | Meaning |
|---|---|
status | true if at least one record of that entity type is still untranslated for this language |
is_empty | true if there are no records of that entity type at all |
The languages list marks a language as incomplete when any entity type's status is true. The auto-translate dialog only lets you select entity types that are both non-empty and incomplete.
Business Logic
Translation Fallback
Display text needed for selected language
│
▼
Bag has a value for that language?
├── Yes → Use it
│
└── No → Fall back to default (source) textAuto-Translate (Bulk)
Select target language (incomplete only)
│
▼
Select entity types to translate
│
▼
POST /bulk/{languageId}
├── A job is already running for this vendor? → 409 Conflict, stop
└── Otherwise → reset progress record, queue BulkTranslateJob, return 202
│
▼
Dialog closes; page polls GET /bulk-progress every 2s and shows a progress bar
│
▼
Job: for each entity / field:
├── Has default-language source text AND no existing translation?
│ ├── Yes → Send to Azure OpenAI (batched, cached), save translation
│ └── No → Skip
│
▼
Fire ReloadMenu for every location
│
▼
status = completed → polling stops, language list refetchedCustomer Impact
Online Ordering & Kiosk
- Item, display group, variant group, category, and modifier names/descriptions shown in the selected language (falling back to the default language where untranslated).
- Loyalty reward names, table section names, welcome popup message, and upsell screen title/subtitle shown in the selected language.
Order / Pager Instructions
- Order instructions and pager prompts shown in the selected language on devices that have them enabled.
How each channel (Online Ordering, Kiosk, devices) actually renders the chosen-language bag is handled in those channels and is not verified here; this page covers how the translations are authored and stored, with
defaultas the fallback.
Relations
Depends On
- Languages: Which languages exist and which is the default (source) language.
- Menu Items / Categories / Modifiers / etc.: The content being translated.
Affects
- Online Ordering: Displayed content.
- Kiosk: Displayed content.
Related Features
Business Rules
- Auto-translation calls an Azure OpenAI chat-completions deployment (default
translation-4o-mini); ifAZURE_OPENAI_TRANSLATION_ENDPOINTorAZURE_OPENAI_TRANSLATION_KEYis not configured, the call fails and content is left untranslated. - Translations are stored as per-language bags keyed by the full English language name. Most menu entities use the
detailsbag (details.<Language>.name/.description); individual modifiers and loyalty rewards use thenamebag (name.<Language>). When a language key is missing, the system falls back todefault. - The
TranslationEntitiesenum definesItem,DisplayGroup,VariantGroup,Category,Modifier,Loyalty,Notification,Receipt,OrderInstructions,PagerInstructions,TableSection,WelcomePopup, andUpsellScreen. Manual-editor tabs are Items, Display Groups, Categories, Modifiers, Order Instructions (covers pager), Welcome Popup, Upsell Screen, and Table Sections; Variant Groups appears only in test/staging or for Square-integrated merchants, and Loyalty only in test/staging.NotificationandReceiptare TODO no-ops in every code path; there is no standalonePagerInstructionseditor entity. - Bulk auto-translate (
bulkTranslate) handles Items, Display Groups, Variant Groups, Categories, Modifiers, Loyalty, Order Instructions (including pager), Table Sections, and Upsell Screen. Welcome Popup, Notifications, and Receipts are not bulk-translated. - Bulk auto-translate only fills slots with default-language source text and no existing translation; existing translations are never overwritten.
- Bulk auto-translate runs as a queued background job.
POST /bulk/{languageId}returns 202 with an initial progress record; progress is polled fromGET /bulk-progress(statusespending/processing/completed/failed). Only one bulk job may run per vendor at a time — a second request while one is active is rejected with 409. (Verified:TranslationController::bulkTranslate,TranslationOrchestrator::hasActiveBulkTranslateJob.) - Modifier-group translations also translate each child modifier's name; loyalty translations translate each reward's name; both are fetched via the parent's relationship methods and carry both the default and the translating-language value.
- Saving a translation (manual or auto) fires a
ReloadMenuevent for every location so devices refresh. - The datatable endpoint supports pagination, sorting, and search on the default name, built with MongoDB aggregation
$addFieldsfor thedefault_name/translating_namecomputed fields. - Completeness (
checkTranslation) is computed over Items, Display Groups, Variant Groups, Categories, Modifiers, Loyalty, Table Sections, Order Instructions (including pager), and Upsell Screen — not Welcome Popup, Notifications, or Receipts. The query only looks at name/title/subtitle fields (the description check is commented out), so a missing description never marks a language incomplete.
FAQs
- "Which translation provider does the system use?" An Azure OpenAI chat-completions deployment (default
translation-4o-mini), configured via theAZURE_OPENAI_TRANSLATION_*environment variables. (It is not Google Cloud Translate or DeepL.) - "Are auto-translations final?" No. They are best-effort machine translations and should be reviewed, especially for food and restaurant-specific terminology.
- "Will auto-translate overwrite my existing translations?" No. Bulk auto-translate only fills empty slots that have source text; it leaves existing translations alone.
- "What happens if a translation is missing for a language?" The system falls back to the default (source) language text, so the customer still sees content.
- "Can I translate receipt text here?" Not on this page. Receipt custom text is auto-translated separately when you save Receipt Settings; receipts and notifications are not yet part of the Translations pipeline.
- "How do I know which content is incomplete?" The languages list flags incomplete languages, and the auto-translate dialog breaks incompleteness down by entity type, only offering entity types that have missing translations. (Welcome Popup is not counted toward completeness.)
- "Why don't I see Variant Groups or Loyalty tabs?" The manual editor only shows Variant Groups in test/staging or for Square-integrated merchants, and Loyalty only in test/staging.
- "Can I auto-translate the welcome popup?" No. Welcome Popup is edited manually only; it is not part of bulk auto-translate. Upsell Screen and Order Instructions (including pager) can be auto-translated.
- "How long does auto-translate take, and do I have to wait on the page?" It runs in the background. The dialog closes as soon as the job is queued and a progress bar on the Translations page shows the entity type being processed and an "n of m" count. You can navigate away and come back — the page picks the progress back up on load.
- "I started an auto-translation and got an error saying one is already running." Only one bulk translation can run per merchant at a time. Wait for the current job to finish (watch the progress bar), then start the next language.
- "The progress bar disappeared but some content is still untranslated." The job reports
completedwhen it has finished processing the entity types you selected. Content that had no default-language source text is skipped by design, and Welcome Popup is never bulk-translated. Re-open the auto-translate dialog to see what is still flagged incomplete.
Troubleshooting
Problem: Translation not showing
Causes:
- No translation entered for that language
- Language not enabled/published
- Device cache not refreshed
Solutions:
- Add the translation (manually or via auto-translate)
- Enable/publish the language in Languages settings
- Wait for the
ReloadMenurefresh or reload the device
Problem: Auto-translate not working
Causes:
- Azure OpenAI endpoint or key not configured
- Selected language has no incomplete entity types
- Provider rate-limited or unavailable
Solutions:
- Configure
AZURE_OPENAI_TRANSLATION_ENDPOINTandAZURE_OPENAI_TRANSLATION_KEY - Confirm there is untranslated content for that language
- Retry — the service retries transient 429/5xx errors automatically
Problem: Wrong or odd machine translation
Causes:
- Menu-specific terminology the model handled imperfectly
- Brand/dish name that should have been preserved
Solutions:
- Edit the translation manually in the fullscreen editor
- Re-enter the correct value; manual edits are not overwritten by future auto-translate runs
Examples
Menu Item Translation Bag
json
{
"id": "item-123",
"details": {
"default": {
"name": "Margherita Pizza",
"description": "Verse mozzarella, San Marzano tomaten, basilicum"
},
"English": {
"name": "Margherita Pizza",
"description": "Fresh mozzarella, San Marzano tomatoes, basil"
},
"German": {
"name": "Margherita Pizza",
"description": "Frischer Mozzarella, San Marzano Tomaten, Basilikum"
},
"French": {
"name": "Pizza Margherita",
"description": "Mozzarella fraîche, tomates San Marzano, basilic"
}
}
}Category Translation Bag
json
{
"id": "cat-456",
"details": {
"default": { "name": "Voorgerechten", "description": "Begin uw maaltijd" },
"English": { "name": "Starters", "description": "Begin your meal" },
"German": { "name": "Vorspeisen", "description": "Beginnen Sie Ihre Mahlzeit" },
"French": { "name": "Entrées", "description": "Commencez votre repas" }
}
}Modifier Group + Modifier Translations
json
{
"id": "modgroup-789",
"details": {
"default": { "name": "Grootte" },
"French": { "name": "Taille" },
"German": { "name": "Größe" }
},
"modifiers": [
{
"id": "mod-1",
"name": { "default": "Klein", "French": "Petit", "German": "Klein" }
},
{
"id": "mod-2",
"name": { "default": "Groot", "French": "Grand", "German": "Groß" }
}
]
}Save a Single Translation (POST /)
json
{
"id": "item-123",
"entity": "items",
"language": "French",
"translating": {
"name": "Pizza Margherita",
"description": "Mozzarella fraîche, tomates San Marzano, basilic"
}
}Auto-Translate Selected Entities (POST /bulk/{languageId})
Request:
json
{
"entities": ["items", "categories", "modifiers"]
}Response — 202 Accepted, the initial progress record:
json
{
"success": true,
"data": {
"language_id": "lang-french",
"status": "pending",
"total_entities": 3,
"completed_entities": 0,
"current_entity": null,
"progress_percentage": 0,
"error_message": null
}
}If a bulk job is already running for the vendor — 409 Conflict:
json
{
"success": false,
"message": "A bulk translation job is already running for this vendor."
}Poll Bulk Progress (GET /bulk-progress)
json
{
"success": true,
"data": {
"language_id": "lang-french",
"status": "processing",
"total_entities": 3,
"completed_entities": 1,
"current_entity": "categories",
"progress_percentage": 33,
"error_message": null
}
}When no job has ever run for the vendor, data is null.