Skip to content

Commit 267dbea

Browse files
engalarclaude
andcommitted
docs: add MDL i18n design proposal
Proposes syntax-layer i18n support for MDL: inline translation map literals `{ en_US: 'Hello', zh_CN: '你好' }`, DESCRIBE WITH TRANSLATIONS output mode, and SHOW TRANSLATIONS query command. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 425dfdb commit 267dbea

File tree

1 file changed

+210
-0
lines changed

1 file changed

+210
-0
lines changed
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
# MDL Internationalization (i18n) Support
2+
3+
**Date:** 2026-04-03
4+
**Status:** Proposal
5+
**Author:** @anthropics/claude-code
6+
7+
## Problem
8+
9+
MDL currently handles all translatable text fields (page titles, widget captions, enumeration captions, microflow message templates) as single-language strings. When creating or describing model elements, only the default language is read or written. All other translations are silently dropped.
10+
11+
This means:
12+
- `DESCRIBE PAGE` output loses translations — roundtripping a page strips non-default languages
13+
- `CREATE PAGE` can only set one language — multi-language projects require Studio Pro for translation
14+
- No way to audit translation coverage from the CLI
15+
16+
Mendix stores translations as `Texts$Text` objects containing an array of `Texts$Translation` entries (one per language). The mxcli internal model (`model.Text`) already represents translations as `map[string]string`, and the BSON reader/writer already handles multi-language serialization. The gap is purely at the MDL syntax and command layer.
17+
18+
## Scope
19+
20+
**In scope (syntax-layer extension):**
21+
- Inline multi-language text literal syntax for CREATE/ALTER
22+
- DESCRIBE WITH TRANSLATIONS output mode
23+
- SHOW TRANSLATIONS query command
24+
- Writer changes to serialize multi-language BSON correctly
25+
26+
**Out of scope:**
27+
- Batch export/import (CSV, XLIFF) — future proposal
28+
- ALTER TRANSLATION standalone command — future proposal
29+
- Translation memory or machine translation integration
30+
31+
## Design
32+
33+
### 1. Translated Text Literal Syntax
34+
35+
Any MDL property that accepts a string literal `'text'` can alternatively accept a translation map:
36+
37+
```sql
38+
-- Single language (backward compatible, unchanged)
39+
Title: 'Hello World'
40+
41+
-- Multi-language
42+
Title: {
43+
en_US: 'Hello World',
44+
zh_CN: '你好世界',
45+
nl_NL: 'Hallo Wereld'
46+
}
47+
```
48+
49+
**Grammar (ANTLR4):**
50+
51+
```antlr
52+
translatedText
53+
: STRING_LITERAL
54+
| '{' translationEntry (',' translationEntry)* ','? '}'
55+
;
56+
57+
translationEntry
58+
: IDENTIFIER ':' STRING_LITERAL
59+
;
60+
```
61+
62+
**AST node:**
63+
64+
```go
65+
type TranslatedText struct {
66+
Translations map[string]string // languageCode → text
67+
IsMultiLang bool // false = single bare string
68+
}
69+
```
70+
71+
**Semantics:**
72+
- Bare string `'text'` writes to the project's `DefaultLanguageCode`. Existing translations in other languages are preserved.
73+
- Map `{ lang: 'text', ... }` writes the specified languages. Languages not mentioned in the map are preserved (merge, not replace).
74+
- No syntax for deleting a translation (use Studio Pro).
75+
76+
### 2. DESCRIBE WITH TRANSLATIONS
77+
78+
```sql
79+
-- Default: single language output (backward compatible)
80+
DESCRIBE PAGE Module.MyPage;
81+
-- Output: Title: 'Hello World'
82+
83+
-- New: all translations
84+
DESCRIBE PAGE Module.MyPage WITH TRANSLATIONS;
85+
-- Output:
86+
-- Title: {
87+
-- en_US: 'Hello World',
88+
-- zh_CN: '你好世界'
89+
-- }
90+
```
91+
92+
**Rules:**
93+
- Without `WITH TRANSLATIONS`: outputs only the default language as a bare string (current behavior).
94+
- With `WITH TRANSLATIONS`: if only one language exists, still uses bare string; if ≥2 languages, uses map syntax.
95+
- Output must be re-parseable by the MDL parser (roundtrip guarantee).
96+
97+
**Grammar:**
98+
99+
```antlr
100+
describeStatement
101+
: DESCRIBE objectType qualifiedName withTranslationsClause?
102+
;
103+
104+
withTranslationsClause
105+
: WITH TRANSLATIONS
106+
;
107+
```
108+
109+
**Affected commands:**
110+
- DESCRIBE PAGE / SNIPPET — Title, widget Caption, Placeholder
111+
- DESCRIBE ENTITY — validation rule messages
112+
- DESCRIBE MICROFLOW / NANOFLOW — LogMessage, ShowMessage, ValidationFeedback templates
113+
- DESCRIBE ENUMERATION — value captions
114+
- DESCRIBE WORKFLOW — task names, descriptions, outcome captions
115+
116+
### 3. SHOW TRANSLATIONS
117+
118+
```sql
119+
-- All translations in a module
120+
SHOW TRANSLATIONS IN Module;
121+
122+
-- Only missing translations
123+
SHOW TRANSLATIONS IN Module MISSING;
124+
125+
-- All translations project-wide
126+
SHOW TRANSLATIONS MISSING;
127+
```
128+
129+
**Output (tabular):**
130+
131+
```
132+
Element Context en_US zh_CN nl_NL
133+
─────────────────────────────────────────────────────────────────────────────
134+
Module.MyPage page_title Hello World 你好世界 ✗
135+
Module.MyPage.SaveButton caption Save 保存 ✗
136+
Module.Status.Active enum_caption Active 活跃 ✗
137+
```
138+
139+
`` indicates a missing translation. The `MISSING` filter shows only rows with at least one gap.
140+
141+
**Implementation:** Reuses the existing catalog `strings` FTS5 table. Pivots rows by language code into a wide-format table. Requires `REFRESH CATALOG FULL` to index strings first.
142+
143+
### 4. Writer Layer Changes
144+
145+
When executing CREATE/ALTER with multi-language text, the writer serializes all provided translations into the standard Mendix BSON format:
146+
147+
```go
148+
titleItems := bson.A{int32(2)} // marker for non-empty
149+
for langCode, text := range translatedText.Translations {
150+
titleItems = append(titleItems, bson.D{
151+
{Key: "$ID", Value: generateUUID()},
152+
{Key: "$Type", Value: "Texts$Translation"},
153+
{Key: "LanguageCode", Value: langCode},
154+
{Key: "Text", Value: text},
155+
})
156+
}
157+
```
158+
159+
**Merge semantics for bare strings:**
160+
When a bare string `'text'` is used, the writer must:
161+
1. Read the existing `Texts$Text` from the MPR
162+
2. Update only the `DefaultLanguageCode` entry
163+
3. Preserve all other language entries unchanged
164+
165+
**Affected writer functions:**
166+
- `writer_pages.go` — Page Title, widget Caption/Placeholder
167+
- `writer_enumeration.go` — EnumerationValue Caption
168+
- `writer_microflow.go` — StringTemplate (log/show/validation messages)
169+
- `writer_widgets.go` — all widget Caption/Placeholder properties
170+
171+
## Translatable Fields Inventory
172+
173+
The following fields use `Texts$Text` and are affected by this proposal:
174+
175+
| Category | StringContext | Count | Examples |
176+
|----------|-------------|-------|---------|
177+
| Page metadata | `page_title` | 1 | Page.Title |
178+
| Enumeration values | `enum_caption` | per value | EnumerationValue.Caption |
179+
| Microflow actions | `log_message`, `show_message`, `validation_message` | 3 | LogMessageAction, ShowMessageAction |
180+
| Workflow objects | `task_name`, `task_description`, `outcome_caption`, `activity_caption` | 4 | UserTask.Name, UserTask.Description |
181+
| Widget properties | `caption`, `placeholder` | 7+ | ActionButton.Caption, TextInput.Placeholder |
182+
183+
**Note:** Widget-level translations (caption, placeholder) are not currently indexed in the catalog `strings` table. A follow-up task should extend `catalog/builder_strings.go` to extract these.
184+
185+
## Implementation Phases
186+
187+
| Phase | Scope | Dependency |
188+
|-------|-------|------------|
189+
| **P1** | Grammar + AST: `translatedText` rule, `TranslatedText` node | None |
190+
| **P2** | Visitor: parse `{ lang: 'text' }` into AST | P1 |
191+
| **P3** | DESCRIBE WITH TRANSLATIONS: all describe commands output multi-language | P1 (reuses AST) |
192+
| **P4** | Writer: CREATE/ALTER write multi-language BSON | P1 + P2 |
193+
| **P5** | SHOW TRANSLATIONS: catalog query command | None (independent) |
194+
| **P6** | Widget translation indexing: extend catalog builder for widget-level translations | P5 |
195+
196+
Each phase is independently deliverable and testable.
197+
198+
## Compatibility
199+
200+
- **Backward compatible**: existing MDL scripts with bare strings continue to work identically.
201+
- **Forward compatible**: MDL scripts using `{ lang: 'text' }` syntax will fail gracefully on older mxcli versions with a parse error pointing to the `{` token.
202+
- **DESCRIBE roundtrip**: `DESCRIBE ... WITH TRANSLATIONS` output can be fed back to `CREATE OR REPLACE` to reproduce the same translations.
203+
204+
## Risks
205+
206+
| Risk | Mitigation |
207+
|------|-----------|
208+
| `{` ambiguity with widget body blocks | Grammar context: `translatedText` only appears in property value position, not statement position. Widget bodies follow `)` not `:`. |
209+
| Translation ordering in BSON | Mendix does not depend on translation order within `Items` array. Sort by language code for deterministic output. |
210+
| Large translation maps cluttering DESCRIBE output | `WITH TRANSLATIONS` is opt-in; default remains single-language. |

0 commit comments

Comments
 (0)