Skip to content

Commit f0ea78c

Browse files
committed
fix(i18n): fix WelcomeScreen displaying raw translation keys due to module-level t() calls
WelcomeScreen.SHORTCUT_TIPS was defined at module scope, so t() ran before initI18n() (ESM import order), returning key strings like "ui.welcome.pasteImage" instead of translated text. Convert SHORTCUT_TIPS from a module-level const array to a lazy getShortcutTips() function called at render time. Also update the i18n-development SKILL.md to: - Add the WelcomeScreen real-world case to Pitfall #1 - Document the "tests pass but UI shows raw keys" subtle trap in Pitfall #5 - Expand audit commands for detecting module-level t() calls
1 parent 2d5de6a commit f0ea78c

2 files changed

Lines changed: 60 additions & 11 deletions

File tree

.agents/skills/i18n-development/SKILL.md

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,17 @@ All i18n changes have negligible performance impact:
187187
const OPTIONS = [{ label: t("ui.config.language") }]; // → "ui.config.language"
188188
```
189189

190-
**Fix**: Move `t()` into functions called at render time:
190+
**Real-world case** (`WelcomeScreen.tsx`):
191+
```typescript
192+
// ❌ BUG: SHORTCUT_TIPS defined at module scope — t() returns key strings
193+
const SHORTCUT_TIPS = [
194+
{ label: "Ctrl+V", description: t("ui.welcome.pasteImage") }, // "ui.welcome.pasteImage"
195+
];
196+
```
197+
198+
Users saw `"Tips: Ctrl+V - ui.welcome.pasteImage"` instead of `"Tips: Ctrl+V - Paste an image from the clipboard"`.
199+
200+
**Fix**: Move `t()` into functions called at render time (or into the component body):
191201

192202
```typescript
193203
// ✅ CORRECT — lazy evaluation after initI18n()
@@ -200,7 +210,33 @@ export function buildCommands() {
200210
}
201211
```
202212

203-
**Audit**: `rg -n '^\w.*t\("' src/ --include='*.ts' --include='*.tsx' | grep -v test` — no matches expected.
213+
For React components returning static arrays, wrap in a function:
214+
215+
```typescript
216+
function getShortcutTips(): Array<{ label: string; description: string }> {
217+
return [
218+
{ label: "Ctrl+V", description: t("ui.welcome.pasteImage") }, // ✅ "Paste an image from the clipboard"
219+
];
220+
}
221+
```
222+
223+
If the array is consumed inside a `useMemo(...)`, the `get*()` function is still safe because `useMemo` also runs at render time.
224+
225+
**Audit commands**:
226+
227+
1. Check for module-level `t()` calls (should be zero in source files):
228+
```bash
229+
rg -n '^\w.*t\("' src/ --include='*.ts' --include='*.tsx' | grep -v test
230+
```
231+
Expected output: no matches.
232+
233+
2. Verify `initI18n()` is called before any module that uses `t()`:
234+
```bash
235+
# Check CLI entry point calls initI18n before importing UI components
236+
rg -n 'initI18n' src/cli.tsx
237+
```
238+
239+
3. When in doubt, add a runtime guard at the start of `t()` to detect pre-init calls (development only).
204240

205241
### 2. 🚫 Missing `t` Import
206242

@@ -240,6 +276,17 @@ if (openRawModelDropdown || showSkillsDropdown || showModelDropdown || showConfi
240276

241277
Tests calling functions using `t()` must call `initI18n("en")` first, otherwise `t()` returns key strings.
242278

279+
**⚠️ Subtle trap**: Tests may pass even when `t()` returns key strings, if the test only checks non-translated fields (e.g., `tip.label` but not `tip.description`). The bug only manifests in the UI.
280+
281+
```typescript
282+
// ❌ Test passes despite t() returning key strings — description is never checked
283+
const tips = buildWelcomeTips(skills);
284+
assert.ok(tips[0].label.includes("/new")); // passes
285+
// tips[0].description === "ui.welcome.sendPrompt" — but nobody checks it!
286+
```
287+
288+
**Fix**: Always call `initI18n("en")` in `describe` or test setup when testing any function that uses `t()`. If the test doesn't care about translated output, at minimum assert that `t()` returns something other than the key itself.
289+
243290
### 6. 🚫 Translation Key Naming Mismatch
244291

245292
Run `npm run check:i18n` before PR. Also audit key usage:

src/ui/WelcomeScreen.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,16 @@ type WelcomeScreenProps = {
2020
const TITLE_PANEL_WIDTH = 70;
2121
const PANEL_CONTENT_HEIGHT = 8;
2222

23-
const SHORTCUT_TIPS = [
24-
{ label: "Enter", description: t("ui.welcome.sendPrompt") },
25-
{ label: "Shift+Enter", description: t("ui.welcome.insertNewline") },
26-
{ label: "Ctrl+V", description: t("ui.welcome.pasteImage") },
27-
{ label: "Esc", description: t("ui.welcome.interrupt") },
28-
{ label: "/", description: t("ui.welcome.openMenu") },
29-
{ label: "Ctrl+D twice", description: t("ui.welcome.quit") },
30-
];
23+
function getShortcutTips(): Array<{ label: string; description: string }> {
24+
return [
25+
{ label: "Enter", description: t("ui.welcome.sendPrompt") },
26+
{ label: "Shift+Enter", description: t("ui.welcome.insertNewline") },
27+
{ label: "Ctrl+V", description: t("ui.welcome.pasteImage") },
28+
{ label: "Esc", description: t("ui.welcome.interrupt") },
29+
{ label: "/", description: t("ui.welcome.openMenu") },
30+
{ label: "Ctrl+D twice", description: t("ui.welcome.quit") },
31+
];
32+
}
3133

3234
export function WelcomeScreen({ projectRoot, settings, skills, width }: WelcomeScreenProps): React.ReactElement {
3335
const { version } = useAppContext();
@@ -123,7 +125,7 @@ export function buildWelcomeTips(skills: SkillInfo[]): Array<{ label: string; de
123125

124126
return [
125127
...slashTips,
126-
...SHORTCUT_TIPS.filter((tip) => !BUILTIN_SLASH_COMMANDS.some((command) => command.label === tip.label)),
128+
...getShortcutTips().filter((tip) => !BUILTIN_SLASH_COMMANDS.some((command) => command.label === tip.label)),
127129
];
128130
}
129131

0 commit comments

Comments
 (0)