Skip to content

Commit 0372b51

Browse files
engalarclaude
andcommitted
docs: revise i18n design per review feedback
- Drop SHOW TRANSLATIONS (overlaps with SHOW LANGUAGES + QUAL005) - Add concrete ANTLR integration into propertyValueV3 - Detail read-modify-write impact on writer architecture - Reorder phases: P1 = DESCRIBE WITH TRANSLATIONS (highest value, zero risk) - Add ALTER PAGE SET + translation map interaction - Sort translations by language code for deterministic output - Fix author attribution Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 267dbea commit 0372b51

File tree

1 file changed

+69
-45
lines changed

1 file changed

+69
-45
lines changed

docs/plans/2026-04-03-mdl-i18n-design.md

Lines changed: 69 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# MDL Internationalization (i18n) Support
22

3-
**Date:** 2026-04-03
4-
**Status:** Proposal
5-
**Author:** @anthropics/claude-code
3+
**Date:** 2026-04-03 (updated 2026-04-03)
4+
**Status:** Proposal (revised per review feedback)
5+
**Author:** @engalar
66

77
## Problem
88

@@ -11,16 +11,15 @@ MDL currently handles all translatable text fields (page titles, widget captions
1111
This means:
1212
- `DESCRIBE PAGE` output loses translations — roundtripping a page strips non-default languages
1313
- `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
14+
- No way to see all translations in context (note: `SHOW LANGUAGES` and `QUAL005 MissingTranslations` linter rule already provide language inventory and gap detection via the catalog `strings` table)
1515

1616
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.
1717

1818
## Scope
1919

2020
**In scope (syntax-layer extension):**
21-
- Inline multi-language text literal syntax for CREATE/ALTER
21+
- Inline multi-language text literal syntax for CREATE/ALTER/ALTER PAGE SET
2222
- DESCRIBE WITH TRANSLATIONS output mode
23-
- SHOW TRANSLATIONS query command
2423
- Writer changes to serialize multi-language BSON correctly
2524

2625
**Out of scope:**
@@ -48,17 +47,35 @@ Title: {
4847

4948
**Grammar (ANTLR4):**
5049

50+
New rule:
51+
5152
```antlr
52-
translatedText
53-
: STRING_LITERAL
54-
| '{' translationEntry (',' translationEntry)* ','? '}'
53+
translationMap
54+
: LBRACE translationEntry (COMMA translationEntry)* COMMA? RBRACE
5555
;
5656
5757
translationEntry
58-
: IDENTIFIER ':' STRING_LITERAL
58+
: IDENTIFIER COLON STRING_LITERAL
5959
;
6060
```
6161

62+
Integration into `propertyValueV3` (MDLParser.g4 line ~1961):
63+
64+
```antlr
65+
propertyValueV3
66+
: STRING_LITERAL
67+
| translationMap // NEW: { en_US: 'Hello', zh_CN: '你好' }
68+
| NUMBER_LITERAL
69+
| booleanLiteral
70+
| qualifiedName
71+
| IDENTIFIER
72+
| H1 | H2 | H3 | H4 | H5 | H6
73+
| LBRACKET (expression (COMMA expression)*)? RBRACKET
74+
;
75+
```
76+
77+
**Disambiguation from widget body `{`**: `translationMap` only appears inside `propertyValueV3`, which follows `COLON` or `EQUALS` in property definitions. Widget bodies (`widgetBodyV3`) follow `)` at statement level, never after `:`. The parser sees `Caption: {` and enters `propertyValueV3 → translationMap` — there is no ambiguity because `widgetBodyV3` is a separate production in `widgetStatementV3` that requires `(...)` before `{`.
78+
6279
**AST node:**
6380

6481
```go
@@ -113,34 +130,24 @@ withTranslationsClause
113130
- DESCRIBE ENUMERATION — value captions
114131
- DESCRIBE WORKFLOW — task names, descriptions, outcome captions
115132

116-
### 3. SHOW TRANSLATIONS
117-
118-
```sql
119-
-- All translations in a module
120-
SHOW TRANSLATIONS IN Module;
133+
### 3. ALTER PAGE SET with Translation Maps
121134

122-
-- Only missing translations
123-
SHOW TRANSLATIONS IN Module MISSING;
135+
Translation maps work in ALTER PAGE SET, enabling in-place translation updates:
124136

125-
-- All translations project-wide
126-
SHOW TRANSLATIONS MISSING;
137+
```sql
138+
ALTER PAGE Module.MyPage
139+
SET WIDGET saveButton Caption: { en_US: 'Save', zh_CN: '保存' };
127140
```
128141

129-
**Output (tabular):**
142+
This reuses the `translationMap` rule inside `propertyValueV3` — no additional grammar changes needed since ALTER PAGE SET already uses `propertyValueV3` for values.
130143

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-
```
144+
### 4. Relationship to Existing Translation Features
138145

139-
`` indicates a missing translation. The `MISSING` filter shows only rows with at least one gap.
146+
`SHOW LANGUAGES` (commit a060152) already lists project languages with string counts. `QUAL005 MissingTranslations` linter rule already detects missing translations. The catalog `strings` FTS5 table already stores per-language text with `SELECT * FROM CATALOG.strings WHERE Language = 'nl_NL'`.
140147

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.
148+
This proposal does **not** duplicate those features. It addresses the gap they cannot fill: **writing and round-tripping multi-language text in MDL syntax**.
142149

143-
### 4. Writer Layer Changes
150+
### 5. Writer Layer Changes
144151

145152
When executing CREATE/ALTER with multi-language text, the writer serializes all provided translations into the standard Mendix BSON format:
146153

@@ -156,17 +163,33 @@ for langCode, text := range translatedText.Translations {
156163
}
157164
```
158165

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
166+
**Merge semantics for bare strings (architectural change):**
167+
168+
Currently, all writer functions construct `Texts$Text` from scratch — e.g. `writer_pages.go:219-247` builds a new `Items` array every time. Bare-string merge semantics require a **read-modify-write cycle**:
169+
170+
1. Read the existing `Texts$Text` BSON from the MPR via `GetRawUnit`
171+
2. Parse existing `Items` array to find the entry for `DefaultLanguageCode`
172+
3. Update that entry's `Text` field (or insert if missing)
173+
4. Preserve all other `Texts$Translation` entries unchanged
174+
5. Write back the modified `Items` array
175+
176+
This is a significant change to writer architecture. A shared helper should be introduced:
164177

165-
**Affected writer functions:**
178+
```go
179+
// mergeTranslation reads existing Texts$Text, merges new translations, returns updated BSON.
180+
// For bare strings: translations = {defaultLang: text}
181+
// For maps: translations = the full map
182+
func mergeTranslation(existingBSON bson.D, translations map[string]string) bson.D
183+
```
184+
185+
**Affected writer functions (11+ call sites):**
166186
- `writer_pages.go` — Page Title, widget Caption/Placeholder
167187
- `writer_enumeration.go` — EnumerationValue Caption
168188
- `writer_microflow.go` — StringTemplate (log/show/validation messages)
169189
- `writer_widgets.go` — all widget Caption/Placeholder properties
190+
- `writer_widgets_action.go`, `writer_widgets_display.go`, `writer_widgets_input.go`
191+
192+
**Serialization ordering:** Translations within `Items` array must be sorted by language code for deterministic BSON output and diff-friendly DESCRIBE.
170193

171194
## Translatable Fields Inventory
172195

@@ -184,16 +207,17 @@ The following fields use `Texts$Text` and are affected by this proposal:
184207

185208
## Implementation Phases
186209

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 |
210+
| Phase | Scope | Dependency | Risk |
211+
|-------|-------|------------|------|
212+
| **P1** | DESCRIBE WITH TRANSLATIONS: all describe commands output multi-language | None — read-only, no grammar change | Low |
213+
| **P2** | Grammar + AST: `translationMap` rule, `TranslatedText` node | None | Low |
214+
| **P3** | Visitor: parse `{ lang: 'text' }` into AST | P2 | Low |
215+
| **P4** | Writer `mergeTranslation` helper + multi-lang BSON write | P3 | **High** — architectural change to writer, must test against Studio Pro |
216+
| **P5** | Widget translation indexing: extend catalog builder for widget-level translations | None (independent) | Low |
217+
218+
P1 first — highest user value, zero risk. P4 is the riskiest phase.
195219
196-
Each phase is independently deliverable and testable.
220+
**Dropped**: SHOW TRANSLATIONS command — `SHOW LANGUAGES` + `QUAL005` + `SELECT ... FROM CATALOG.strings` already cover translation auditing.
197221
198222
## Compatibility
199223

0 commit comments

Comments
 (0)