Skip to content

Commit e05f052

Browse files
os-zhuangclaude
andauthored
feat(i18n): translate the four highest-traffic namespaces into the eight trailing locales (#2872 part a) (#2903)
Backfills `console`, `home`, `topbar` and `layout` — 193 keys x 8 packs, 1,544 strings — so a ja/ko/de/fr/es/pt/ru/ar admin sees the AI console, the home screen, the top bar and the system navigation in their own language instead of silently falling back to English. The gap in those eight packs drops from 469-471 keys to 277-279; en and zh stay at exact parity. This is the "high-frequency namespaces only" strategy, not a full backfill: `grid` (101), `gantt` (58), `dashboard` (25) and the long tail stay on English fallback and remain tracked under #2872. Four keys are deliberately NOT translated, and that is the load-bearing part of this change: console.ai.planApproveMessage console.ai.planApproveDefaultsMessage console.ai.planAnswerMessage console.ai.changesConfirmMessage These are not labels — they are text a button TRANSMITS to the agent, and the cloud confirm gate (service-ai-studio confirm-gate.ts APPROVAL_RE) decides whether it reads as approval. It recognises Chinese and English, nothing else. AiChatPage picks them by the language of the CONVERSATION, not the UI, so the t() call is expected to miss in every non-Chinese pack and fall through to its English defaultValue. Translating them would make a German user's "Build it" send German, the gate stop matching, and the agent re-propose instead of building — the button looks inert while nothing visibly errors. #2900 shipped exactly that bug for changesConfirmMessage, which had been added to all ten packs; this removes it from the eight and restores the English fallback. A new guard pins the invariant in both directions and was mutation-tested against re-adding an outbound key and against dropping an ordinary label. Translations are model-generated and would benefit from native review; every string's placeholder set was verified programmatically against the English source. Claude-Session: https://claude.ai/code/session_01TubWYdWquVkS9dj733sDmC Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1bb77aa commit e05f052

10 files changed

Lines changed: 2058 additions & 8 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
---
2+
"@object-ui/i18n": patch
3+
---
4+
5+
feat(i18n): translate the four highest-traffic namespaces into the eight trailing locales (objectui#2872 part a)
6+
7+
Backfills `console`, `home`, `topbar` and `layout` — 193 keys × 8 packs, 1,544
8+
strings — so a ja/ko/de/fr/es/pt/ru/ar admin sees the AI console, the home
9+
screen, the top bar and the system navigation in their own language instead of
10+
silently falling back to English.
11+
12+
The gap in those eight packs drops from **469–471 keys to 277–279**. `en` and
13+
`zh` remain at exact parity (2499 : 2499, zero difference in both directions).
14+
15+
This is the "high-frequency namespaces only" strategy from the objectui#2872
16+
discussion, not a full backfill: `grid` (101), `gantt` (58), `dashboard` (25)
17+
and the long tail stay on English fallback and remain tracked there.
18+
19+
**Four keys are deliberately left untranslated**, and that is the load-bearing
20+
part of this change:
21+
22+
```
23+
console.ai.planApproveMessage
24+
console.ai.planApproveDefaultsMessage
25+
console.ai.planAnswerMessage
26+
console.ai.changesConfirmMessage
27+
```
28+
29+
These are not labels. They are the text a button *transmits to the agent*, and
30+
the cloud confirm gate (`service-ai-studio` `confirm-gate.ts` `APPROVAL_RE`)
31+
decides whether that text reads as approval. It recognises Chinese and English
32+
— nothing else. `AiChatPage` therefore selects them by the language of the
33+
CONVERSATION rather than of the UI, and the `t()` call is *expected* to miss in
34+
every non-Chinese pack and fall through to its English `defaultValue`.
35+
36+
Translating them would be an outright regression: a German user's "Build it"
37+
would start sending German, the gate would stop matching, and the agent would
38+
re-propose instead of building — the button looks inert while nothing visibly
39+
errors.
40+
41+
objectui#2900 shipped precisely that bug for `changesConfirmMessage`, which had
42+
been added to all ten packs. **This change removes it from the eight**,
43+
restoring the English fallback. A new guard,
44+
`packages/i18n/src/__tests__/outbound-agent-messages.test.ts`, pins the
45+
invariant in both directions: the four keys must be absent from the eight packs
46+
AND present in `en`/`zh`, while every *other* `console.ai` label must be
47+
translated — so the narrow fix can't be over-applied into an excuse for leaving
48+
surrounding labels in English.
49+
50+
Translations are model-generated and would benefit from native review; the
51+
placeholder set of every string was verified programmatically against the
52+
English source.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Guard: the strings the console SENDS to the AI agent must not be translated
5+
* into arbitrary locales.
6+
*
7+
* Most `console.ai.*` keys are labels and should be localized everywhere. Four
8+
* are not labels — they are the message text a button transmits to the agent,
9+
* and the cloud confirm gate (`service-ai-studio` `confirm-gate.ts`
10+
* `APPROVAL_RE`) decides whether that text counts as approval. It recognises
11+
* Chinese and English. Nothing else.
12+
*
13+
* So `AiChatPage` picks them by the language of the CONVERSATION, not the UI:
14+
*
15+
* const planApproveMessage = convZh
16+
* ? '确认,开始搭建。' // matches the gate
17+
* : t('console.ai.planApproveMessage', { defaultValue: … });
18+
*
19+
* That `t()` call is expected to MISS in every non-Chinese pack and fall
20+
* through to its English `defaultValue`. Add a German translation and a German
21+
* user's "Build it" click starts sending German, the gate stops matching, and
22+
* the agent re-proposes instead of building — the button looks inert while
23+
* nothing visibly errors. objectui#2900 shipped exactly that for
24+
* `changesConfirmMessage`; this test is why it can't happen twice.
25+
*
26+
* If the gate ever learns more languages, delete the offending entry here in
27+
* the same change that teaches it — not before.
28+
*/
29+
import { describe, it, expect } from 'vitest';
30+
import { en, zh, ja, ko, de, fr, es, pt, ru, ar } from '../locales';
31+
32+
const OUTBOUND_KEYS = [
33+
'console.ai.planApproveMessage',
34+
'console.ai.planApproveDefaultsMessage',
35+
'console.ai.planAnswerMessage',
36+
'console.ai.changesConfirmMessage',
37+
] as const;
38+
39+
const GATE_LOCALES: Record<string, unknown> = { en, zh };
40+
const NON_GATE_LOCALES: Record<string, unknown> = { ja, ko, de, fr, es, pt, ru, ar };
41+
42+
function lookup(pack: unknown, dotted: string): unknown {
43+
return dotted
44+
.split('.')
45+
.reduce<unknown>((node, part) => (node as Record<string, unknown>)?.[part], pack);
46+
}
47+
48+
describe('outbound agent messages stay on gate-recognised languages', () => {
49+
it.each(Object.keys(NON_GATE_LOCALES))(
50+
'the %s pack defines none of them, so t() falls through to the English default',
51+
(lang) => {
52+
const offenders = OUTBOUND_KEYS.filter(
53+
(k) => lookup(NON_GATE_LOCALES[lang], k) !== undefined,
54+
);
55+
expect(offenders).toEqual([]);
56+
},
57+
);
58+
59+
it.each(Object.keys(GATE_LOCALES))('the %s pack defines all of them', (lang) => {
60+
for (const k of OUTBOUND_KEYS) {
61+
expect(typeof lookup(GATE_LOCALES[lang], k)).toBe('string');
62+
}
63+
});
64+
65+
it('every OTHER console.ai key IS localized — this guard is narrow on purpose', () => {
66+
// Guards against over-correcting: the fix for the outbound keys must not
67+
// become a reason to leave the surrounding labels untranslated.
68+
const aiLabels = Object.keys((en as any).console.ai).filter(
69+
(k) => typeof (en as any).console.ai[k] === 'string',
70+
);
71+
const outboundLeaf = new Set(OUTBOUND_KEYS.map((k) => k.split('.').pop()));
72+
const labels = aiLabels.filter((k) => !outboundLeaf.has(k));
73+
expect(labels.length).toBeGreaterThan(20);
74+
for (const k of labels) {
75+
expect((de as any).console.ai[k], `de is missing console.ai.${k}`).toBeTypeOf('string');
76+
}
77+
});
78+
});

0 commit comments

Comments
 (0)