Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ web-ui/playwright-report/
!.kiro/steering/tech-public.md
!.kiro/steering/versioning.md
!.kiro/steering/steering-policy.md
!.kiro/steering/i18n.md

# Config (contains secrets)
infra/config.yaml
Expand Down
78 changes: 78 additions & 0 deletions .kiro/steering/i18n.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<!-- PUBLIC: This file is git-tracked and visible in the public repository. -->

# Web UI: i18n

## Architecture

- Library: **next-intl** (en / ja), no i18n routing — the app is a static-export SPA
- `LocaleProvider` (`src/i18n/LocaleProvider.tsx`) wraps the app at RootLayout
- Locale persists in `localStorage` (`sdpm-locale`), browser-language auto-detection (ja → 日本語, else English)
- First paint is always `en`, then hydrates from localStorage after mount (hydration-mismatch avoidance)
- Message bundles: `web-ui/messages/en.json` / `ja.json`, statically imported, switched client-side
- Locale switcher: Settings → Language

## Rules

### Message files
- **en/ja key parity is mandatory** — every key must exist in both files
- Namespace per component/page (`stylesPage`, `compose`, `modelPicker`, ...); shared strings in `common`
- ICU MessageFormat for interpolation and plurals: `{count, plural, one {# step} other {# steps}}`
(ja uses `other` only — Japanese has no plural distinction)
- Rich text (bold names in confirm dialogs, etc.) via `t.rich()` with tags:
`"deleteDescription": "Are you sure you want to delete <name>{name}</name>?"`

### Components
- User-facing strings (including `aria-label`, `placeholder`, `title` attributes) must go through `useTranslations()`
- **Rules of Hooks**: call `useTranslations()` before any early return
- Unknown/dynamic tool names: guard with `t.has(key) ? t(key) : derivedFallback`
- Non-component code (pure functions like `activityLabel`) accepts an optional translator
parameter instead of importing hooks; falls back to English when absent
- Tests: use `renderWithIntl` (`src/test/renderWithIntl.tsx`) for components that call `useTranslations()`

### Do NOT translate
- Agent/model-generated dynamic text (compose `purpose`, error message bodies from the backend)
- `agentErrors.ts` user-facing messages — guarded by a Python↔TS parity test (`tests/test_agent_errors.py`);
changing these requires coordinated changes on both sides
- shadcn/ui primitives under `src/components/ui/` (upstream-generated code)

### Scope
- **Both modes**: styles / templates pages, StyleChat, style picker are reachable in cloud mode too
(nav links in AppShell) — do not assume they are local-only
- Local-only surfaces (e.g. `AgentSettingsDialog`) are lower priority but follow the same rules when touched

## Japanese translation conventions

Match the existing tone in `ja.json` — natural product UI Japanese, not literal translation:

- Half-width space between Japanese and Latin words/numbers: 「スライド {count} 枚」「AI で作成」「URL をコピー」
- Labels/headings: 体言止め(「スタイルを削除」「アイコンを検索中」)
- Confirm dialog titles: 「〜しますか?」(「このスタイルを削除しますか?」)
- Errors: 「〜できませんでした」(「テンプレートをアップロードできませんでした」)
- Destructive warnings: 「この操作は元に戻せません。」
- Established UI terms: 再試行 (retry), キャンセル (cancel), お気に入り (favorites), ピン留め (pin),
デッキ (deck), コンポーザー (composer)
- Prefer meaning over form: "When should this template be used?" → 「このテンプレートを使う場面を入力…」
- Placeholder hints keep shortcut notation: 「作りたいスタイルを説明… ⌘↵ で送信」

## Verification checklist

```bash
# key parity (en/ja must match exactly)
node -e "
const en=require('./web-ui/messages/en.json'), ja=require('./web-ui/messages/ja.json');
const flat=(o,p='')=>Object.entries(o).flatMap(([k,v])=>typeof v==='object'?flat(v,p+k+'.'):[p+k]);
const e=new Set(flat(en)), j=new Set(flat(ja));
console.log(e.size===j.size && [...e].every(k=>j.has(k)) ? 'OK' : 'MISMATCH');
"
cd web-ui && npx eslint src/ --quiet && npx tsc --noEmit && npm test && npm run build:cloud
```

Finding untranslated strings:

```bash
# components missing useTranslations
for f in $(find web-ui/src/components web-ui/src/app -name "*.tsx" ! -name "*.test.tsx"); do
grep -L "useTranslations" "$f"; done
# hardcoded UI strings in attributes
grep -rnE '(placeholder|aria-label|title)="[A-Z][a-z]+' web-ui/src --include="*.tsx" | grep -v '{t('
```
Loading
Loading