translations · git:20260825.86be70d · 2026-08-25 · sha256 1f24421f47655a04
translations git:20260825.86be70dA
Immutable. This exact content is served forever at /api/v1/blob/1f24421f47655a04.
---
name: translations
description: "Translate a Mendix app's user-visible strings with MDL — DESCRIBE TRANSLATIONS to export one file per language, CREATE [OR MODIFY|REPLACE] TRANSLATIONS to write them back. Use when localising an app, filling in a language with an LLM, or auditing which strings are still untranslated. Covers why a translation can be stored and still never appear."
---
# Translations
Every user-visible string of a Mendix app — page titles, widget captions, button
labels, validation messages, enum captions, menu items — is a `Texts$Text` with
one translation per language. MDL treats them in bulk: one file per language.
## The loop
```bash
# 1. export: an untranslated string comes back with an EMPTY target
mxcli -p app.mpr -c "describe translations for de_DE" > de_DE.mdl
# 2. fill in the right-hand sides (by hand, or hand the file to an LLM)
# 3. write them back
mxcli exec de_DE.mdl -p app.mpr
```
That is the whole design: `DESCRIBE` emits the `CREATE` form, so the export
format and the import format are the same file, and the empty targets are the
prompt.
## Statements
```sql
describe translations [in <Module>] for <lang>;
create translations [in <Module>] for <lang> ( 'src' as 'target', ... );
create or modify translations [in <Module>] for <lang> ( 'src' as 'target', ... );
create or replace translations [in <Module>] for <lang> ( 'src' as 'target', ... );
```
Entries use `as`, not a colon: a translation maps a user-provided name to another
name.
| verb | meaning |
|------|---------|
| `create` | the "add a language" form — **refuses** if that language already has translations anywhere in scope |
| `create or modify` | merge; a source string the file does not name keeps whatever it has |
| `create or replace` | the file is authoritative — a translation whose source the file does **not** name is **removed**, and the run names what it deleted |
`IN <Module>` scopes both directions, and under `OR REPLACE` it **bounds the
deletion** — without it, a set of per-module files would wipe each other on every
run.
## The trap: a language that is not enabled
**A translation for a language the project has not enabled is stored, passes
`mx check`, and is discarded at build time.** Measured with
`mxbuild --target=deploy`: the string reaches nothing under `deployment/` — no
`translations_<code>.properties` is produced at all — while the model and Studio
Pro both keep it happily.
So translating 400 strings into German can produce exactly no German in the app.
`create translations` warns when the language is not enabled; enable it in
project settings first (see the **project-settings** skill), then re-run.
A stock app makes this easy to hit by accident: it enables **one** language while
its marketplace modules ship translations in **nine**, so "other languages
already have translations here" is true and misleading.
> `show languages` lists languages that have **translations**, not the enabled
> ones — a stock app reports 8 while 1 is enabled. `describe settings` has the
> enabled list.
## Drift: a source string that was edited
The dictionary is keyed on the **source string**, so `Save` is translated once
for all the places it occurs. The flip side: editing a source string after the
file was written stops it matching, which would leave the translation attached to
a string that no longer exists.
A key that matches nothing is **reported, not skipped** — and where the
translation identifies the moved source unambiguously, the run names the fix:
```
Warning: 1 source string(s) in the file matched nothing in the project.
"Thingz" as "Grejer"
No text has "Thingz" as its source. A text now reads "Things" and carries
the sv_SE "Grejer" — the source was probably edited. Change the file to:
"Things" as "Grejer"
```
## Notes
- **Scale.** A whole app is a few hundred distinct source strings (411 on a real
project measured), so one file per language is comfortably practical.
- **A rewrite does not lose other languages.** Re-executing a page or microflow
keeps the translations MDL cannot express; you do not have to re-import a
language after editing a document.
- **The source language** is the project's default: the left-hand column is what
`DESCRIBE` shows for that language, so change `DefaultLanguageCode` before
exporting if you want a different source.
- Translating **into** the source language is refused — it would overwrite the
strings everything else is keyed on.
## Common tasks
```sql
-- what is still untranslated, and how much
describe translations for de_DE; -- ends with "N source string(s), M translated, K to go."
-- one module at a time
describe translations in Sales for de_DE;
-- make a file authoritative for one module only
create or replace translations in Sales for de_DE ( 'Order' as 'Bestellung' );
-- remove a language's translations entirely
create or replace translations for de_DE ( );
```