docs(34): add research and validation strategy
This commit is contained in:
281
.planning/phases/34-i18n-foundation/34-RESEARCH.md
Normal file
281
.planning/phases/34-i18n-foundation/34-RESEARCH.md
Normal file
@@ -0,0 +1,281 @@
|
||||
# Phase 34: i18n Foundation - Research
|
||||
|
||||
**Researched:** 2026-04-13
|
||||
**Status:** Complete
|
||||
|
||||
## Library Evaluation
|
||||
|
||||
### react-i18next vs Lingui
|
||||
|
||||
| Criterion | react-i18next | Lingui |
|
||||
|-----------|--------------|--------|
|
||||
| React 19 support | Yes (v15+) | Yes (v5+) |
|
||||
| Hook-based API | `useTranslation()` | `useLingui()` |
|
||||
| Lazy loading | Built-in (`react-i18next/icu`) backend, dynamic imports | Catalog-based lazy loading via `@lingui/loader` |
|
||||
| TypeScript support | Strong with `i18next` typed resources | Strong with compiled catalogs |
|
||||
| Bundle size | ~10kb (i18next core + react-i18next) | ~5kb (runtime only) |
|
||||
| Vite plugin | `i18next-resources-for-ts` or manual | `@lingui/vite-plugin` |
|
||||
| Extraction tooling | `i18next-parser` (CLI) | `@lingui/cli extract` (built-in) |
|
||||
| JSON file format | Native JSON key-value | PO files or JSON catalogs |
|
||||
| Bun/Hono server-side | `i18next` works standalone (no React dependency) | `@lingui/core` works standalone |
|
||||
| Community/ecosystem | Larger ecosystem, more plugins | Growing, more opinionated |
|
||||
| Namespace support | Built-in first-class | Via message IDs with prefixes |
|
||||
| Interpolation | `{{name}}` syntax | `{name}` syntax with ICU |
|
||||
|
||||
**Recommendation: react-i18next**
|
||||
|
||||
Reasons:
|
||||
1. **Namespace support is first-class** — CONTEXT.md decision D-08 requires namespaces by feature area. react-i18next has this built-in; Lingui requires manual ID prefixing.
|
||||
2. **JSON translation files** — Decision D-06 specifies JSON files in `src/client/locales/`. react-i18next uses plain JSON natively. Lingui prefers PO files or its own catalog format.
|
||||
3. **Server-side reuse** — `i18next` core (no React) can be used directly in Hono routes and MCP tool descriptions. Same translation files, same API.
|
||||
4. **Larger ecosystem** — More documentation, Stack Overflow answers, and community plugins for future needs (Crowdin/Lokalise integration mentioned in D-06).
|
||||
5. **Lazy loading** — `i18next-http-backend` or dynamic imports work cleanly with Vite code splitting.
|
||||
|
||||
### Required Packages
|
||||
|
||||
```
|
||||
i18next # Core translation engine
|
||||
react-i18next # React bindings (useTranslation hook)
|
||||
i18next-browser-languagedetector # Auto-detect browser locale (D-10)
|
||||
```
|
||||
|
||||
No additional Vite plugins needed — JSON imports work natively.
|
||||
|
||||
## Architecture Design
|
||||
|
||||
### Client-Side Setup
|
||||
|
||||
```
|
||||
src/client/
|
||||
├── locales/
|
||||
│ ├── en/
|
||||
│ │ ├── common.json # Shared: buttons, labels, navigation
|
||||
│ │ ├── collection.json # Collection page strings
|
||||
│ │ ├── threads.json # Research threads strings
|
||||
│ │ ├── setups.json # Setups strings
|
||||
│ │ ├── onboarding.json # Onboarding flow strings
|
||||
│ │ └── settings.json # Settings page strings
|
||||
│ └── de/
|
||||
│ ├── common.json
|
||||
│ ├── collection.json
|
||||
│ ├── threads.json
|
||||
│ ├── setups.json
|
||||
│ ├── onboarding.json
|
||||
│ └── settings.json
|
||||
├── lib/
|
||||
│ └── i18n.ts # i18next initialization
|
||||
```
|
||||
|
||||
**Namespace strategy (D-08):**
|
||||
- `common` — buttons ("Save", "Cancel", "Delete"), nav items, shared labels, error messages, empty states
|
||||
- `collection` — collection page, item forms, item cards
|
||||
- `threads` — thread list, thread detail, candidate forms
|
||||
- `setups` — setup list, setup detail, impact preview
|
||||
- `onboarding` — welcome, hobby picker, item browser, review, done screens
|
||||
- `settings` — settings page labels, API keys section, import/export
|
||||
|
||||
### i18n Initialization (`src/client/lib/i18n.ts`)
|
||||
|
||||
```typescript
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
|
||||
// Eager-load both locales (small app, 2 languages)
|
||||
import enCommon from "../locales/en/common.json";
|
||||
import enCollection from "../locales/en/collection.json";
|
||||
import enThreads from "../locales/en/threads.json";
|
||||
import enSetups from "../locales/en/setups.json";
|
||||
import enOnboarding from "../locales/en/onboarding.json";
|
||||
import enSettings from "../locales/en/settings.json";
|
||||
|
||||
import deCommon from "../locales/de/common.json";
|
||||
import deCollection from "../locales/de/collection.json";
|
||||
import deThreads from "../locales/de/threads.json";
|
||||
import deSetups from "../locales/de/setups.json";
|
||||
import deOnboarding from "../locales/de/onboarding.json";
|
||||
import deSettings from "../locales/de/settings.json";
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources: {
|
||||
en: {
|
||||
common: enCommon,
|
||||
collection: enCollection,
|
||||
threads: enThreads,
|
||||
setups: enSetups,
|
||||
onboarding: enOnboarding,
|
||||
settings: enSettings,
|
||||
},
|
||||
de: {
|
||||
common: deCommon,
|
||||
collection: deCollection,
|
||||
threads: deThreads,
|
||||
setups: deSetups,
|
||||
onboarding: deOnboarding,
|
||||
settings: deSettings,
|
||||
},
|
||||
},
|
||||
fallbackLng: "en",
|
||||
defaultNS: "common",
|
||||
interpolation: {
|
||||
escapeValue: false, // React handles XSS
|
||||
},
|
||||
detection: {
|
||||
order: ["localStorage", "navigator"],
|
||||
lookupLocalStorage: "gearbox-language",
|
||||
caches: ["localStorage"],
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
```
|
||||
|
||||
**Note on lazy loading:** With only 2 languages and ~200 strings, eager loading all namespaces is simpler and avoids loading spinners. Total JSON payload is <20KB gzipped. Lazy loading can be added later when more languages are added.
|
||||
|
||||
### Integration with `useFormatters()`
|
||||
|
||||
Decision D-04 specifies i18n integrates with the existing formatters hook. The formatters currently use manual string concatenation. With i18n, number and date formatting should use `Intl.NumberFormat` and `Intl.DateTimeFormat` for locale-aware output.
|
||||
|
||||
**Approach:** Extend `useFormatters()` to accept locale from i18n context and pass it to `formatWeight()` and `formatPrice()`. The format functions gain a `locale` parameter:
|
||||
|
||||
```typescript
|
||||
// formatPrice now uses Intl.NumberFormat for locale-aware number display
|
||||
export function formatPrice(cents: number | null, currency: Currency, locale: string): string {
|
||||
if (cents == null) return "--";
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: "currency",
|
||||
currency,
|
||||
minimumFractionDigits: currency === "JPY" ? 0 : 2,
|
||||
}).format(cents / 100);
|
||||
}
|
||||
```
|
||||
|
||||
This replaces the manual symbol lookup with `Intl.NumberFormat` which handles symbol placement, decimal separators, and grouping per locale (e.g., German: `1.234,56 €` vs English: `$1,234.56`).
|
||||
|
||||
### Language Setting Storage
|
||||
|
||||
Following the `useWeightUnit()` and `useCurrency()` pattern:
|
||||
|
||||
```typescript
|
||||
// src/client/hooks/useLanguage.ts
|
||||
export function useLanguage(): string {
|
||||
const { data } = useSetting("language");
|
||||
return data && VALID_LANGUAGES.includes(data) ? data : "en";
|
||||
}
|
||||
```
|
||||
|
||||
**Key difference from weight/currency:** Language changes need to call `i18n.changeLanguage()` in addition to persisting via `useSetting()`. A `useEffect` in the root layout (or the `useLanguage` hook) syncs the i18n instance when the setting changes.
|
||||
|
||||
### Server-Side Translation
|
||||
|
||||
MCP tool descriptions and API error messages need server-side translation. Since Hono runs on Bun (not browser), use `i18next` core directly:
|
||||
|
||||
```typescript
|
||||
// src/server/lib/i18n.ts
|
||||
import i18next from "i18next";
|
||||
import en from "../../client/locales/en/common.json";
|
||||
import de from "../../client/locales/de/common.json";
|
||||
|
||||
const serverI18n = i18next.createInstance();
|
||||
serverI18n.init({
|
||||
lng: "en", // Default server language
|
||||
resources: { en: { common: en }, de: { common: de } },
|
||||
defaultNS: "common",
|
||||
});
|
||||
|
||||
export function t(key: string, lng?: string): string {
|
||||
return serverI18n.t(key, { lng });
|
||||
}
|
||||
```
|
||||
|
||||
**MCP tool descriptions:** These are registered once at server start and consumed by AI clients. They should remain in English — AI models work best with English tool descriptions. Server-side i18n applies to API error messages returned to the browser, not MCP tool descriptions.
|
||||
|
||||
### String Key Convention
|
||||
|
||||
**Nested keys with dot notation:**
|
||||
|
||||
```json
|
||||
{
|
||||
"nav": {
|
||||
"collection": "Collection",
|
||||
"setups": "Setups",
|
||||
"discover": "Discover",
|
||||
"settings": "Settings"
|
||||
},
|
||||
"actions": {
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"create": "Create"
|
||||
},
|
||||
"empty": {
|
||||
"noItems": "No items yet",
|
||||
"noThreads": "No research threads yet"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Access pattern: `t("nav.collection")`, `t("actions.save")`.
|
||||
|
||||
### Language Picker UX
|
||||
|
||||
Reuse the pill-toggle pattern from weight unit and currency in settings:
|
||||
|
||||
```tsx
|
||||
const LANGUAGES = [
|
||||
{ value: "en", label: "English" },
|
||||
{ value: "de", label: "Deutsch" },
|
||||
];
|
||||
```
|
||||
|
||||
Place in the settings page above weight unit, since language is the most fundamental preference.
|
||||
|
||||
### String Extraction Strategy
|
||||
|
||||
Given ~200 strings across ~50 components, extraction should be done systematically by feature area matching the namespace structure:
|
||||
|
||||
1. **Common** — TopNav, BottomTabBar, FabMenu, ConfirmDialog, AuthPromptModal, empty states
|
||||
2. **Collection** — CollectionView, ItemCard, ItemForm, CategoryPicker, CategoryHeader, WeightSummaryCard
|
||||
3. **Threads** — ThreadCard, ThreadTabs, CandidateCard, CandidateForm, ComparisonTable, CreateThreadModal
|
||||
4. **Setups** — SetupsView, SetupCard, SetupImpactSelector, ShareModal
|
||||
5. **Onboarding** — OnboardingWelcome, OnboardingHobbyPicker, OnboardingItemBrowser, OnboardingReview, OnboardingDone
|
||||
6. **Settings** — SettingsPage (weight, currency, language, API keys, import/export)
|
||||
|
||||
## Validation Architecture
|
||||
|
||||
### Test Strategy
|
||||
|
||||
1. **Unit tests** — i18n initialization loads both locales without error
|
||||
2. **Unit tests** — `useLanguage()` hook returns correct language from settings
|
||||
3. **Unit tests** — `formatPrice()` with locale produces correct output for en and de
|
||||
4. **Unit tests** — `formatWeight()` with locale produces correct output for en and de
|
||||
5. **Integration test** — Language change via settings API persists and takes effect
|
||||
6. **E2E test** — Switch language in settings, verify UI text changes to German
|
||||
|
||||
### Completeness Checks
|
||||
|
||||
- Every `en/*.json` key has a corresponding `de/*.json` key (no missing translations)
|
||||
- No hardcoded English strings remain in components that have been extracted
|
||||
- `i18n.ts` registers all namespaces for both languages
|
||||
- Language picker appears in settings and persists selection
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Risk | Impact | Mitigation |
|
||||
|------|--------|------------|
|
||||
| React 19 compatibility with react-i18next | Medium | react-i18next v15+ supports React 19. Pin compatible version. |
|
||||
| Bundle size increase | Low | i18next + react-i18next is ~10KB gzipped. JSON files add <10KB per language. |
|
||||
| String extraction misses some strings | Low | Incremental approach — extract by namespace area, verify each area. |
|
||||
| German translation quality | Low | AI-generated is acceptable per D-14. User corrects organically. |
|
||||
| Formatter locale breaking existing tests | Medium | Update test helpers to pass locale. Existing tests keep "en" default. |
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **Phase 33 (Currency System):** Language detection should follow the same browser auto-detection pattern. The `useCurrency` hook pattern is the model for `useLanguage`. Phase 33 may add market auto-detection; language auto-detection is independent but similar.
|
||||
- **No schema changes needed:** Language preference stored in existing `settings` table via `useSetting("language")`.
|
||||
|
||||
## RESEARCH COMPLETE
|
||||
79
.planning/phases/34-i18n-foundation/34-VALIDATION.md
Normal file
79
.planning/phases/34-i18n-foundation/34-VALIDATION.md
Normal file
@@ -0,0 +1,79 @@
|
||||
---
|
||||
phase: 34
|
||||
slug: i18n-foundation
|
||||
status: draft
|
||||
nyquist_compliant: false
|
||||
wave_0_complete: false
|
||||
created: 2026-04-13
|
||||
---
|
||||
|
||||
# Phase 34 — Validation Strategy
|
||||
|
||||
> Per-phase validation contract for feedback sampling during execution.
|
||||
|
||||
---
|
||||
|
||||
## Test Infrastructure
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Framework** | Bun test runner + Playwright |
|
||||
| **Config file** | `bunfig.toml` / `playwright.config.ts` |
|
||||
| **Quick run command** | `bun test` |
|
||||
| **Full suite command** | `bun test && bun run test:e2e` |
|
||||
| **Estimated runtime** | ~15 seconds (unit) + ~60 seconds (e2e) |
|
||||
|
||||
---
|
||||
|
||||
## Sampling Rate
|
||||
|
||||
- **After every task commit:** Run `bun test`
|
||||
- **After every plan wave:** Run `bun test && bun run build`
|
||||
- **Before `/gsd-verify-work`:** Full suite must be green
|
||||
- **Max feedback latency:** 15 seconds
|
||||
|
||||
---
|
||||
|
||||
## Per-Task Verification Map
|
||||
|
||||
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|
||||
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
|
||||
| 34-01-01 | 01 | 1 | D-05 | — | N/A | unit | `bun test tests/i18n/init.test.ts` | ❌ W0 | ⬜ pending |
|
||||
| 34-01-02 | 01 | 1 | D-06 | — | N/A | unit | `bun test tests/i18n/locales.test.ts` | ❌ W0 | ⬜ pending |
|
||||
| 34-02-01 | 02 | 1 | D-01 | — | N/A | unit | `bun test` | ✅ | ⬜ pending |
|
||||
| 34-03-01 | 03 | 2 | D-04 | — | N/A | unit | `bun test tests/i18n/formatters.test.ts` | ❌ W0 | ⬜ pending |
|
||||
| 34-04-01 | 04 | 2 | D-09, D-10, D-11 | — | N/A | unit | `bun test tests/i18n/language-hook.test.ts` | ❌ W0 | ⬜ pending |
|
||||
| 34-05-01 | 05 | 3 | D-13, D-14 | — | N/A | manual | Visual check: German UI text | N/A | ⬜ pending |
|
||||
|
||||
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
|
||||
|
||||
---
|
||||
|
||||
## Wave 0 Requirements
|
||||
|
||||
- [ ] `tests/i18n/init.test.ts` — i18n initialization loads both locales
|
||||
- [ ] `tests/i18n/locales.test.ts` — all en keys have corresponding de keys
|
||||
- [ ] `tests/i18n/formatters.test.ts` — locale-aware formatting produces correct output
|
||||
- [ ] `tests/i18n/language-hook.test.ts` — language hook returns correct value from settings
|
||||
|
||||
---
|
||||
|
||||
## Manual-Only Verifications
|
||||
|
||||
| Behavior | Requirement | Why Manual | Test Instructions |
|
||||
|----------|-------------|------------|-------------------|
|
||||
| German UI text renders correctly | D-13 | Visual quality of AI-generated translations | Switch to German in settings, navigate all pages, verify text is natural German |
|
||||
| Language picker pill-toggle UX | D-11 | Visual layout consistency with weight/currency toggles | Open settings, verify language picker matches existing toggle patterns |
|
||||
|
||||
---
|
||||
|
||||
## Validation Sign-Off
|
||||
|
||||
- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
|
||||
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
|
||||
- [ ] Wave 0 covers all MISSING references
|
||||
- [ ] No watch-mode flags
|
||||
- [ ] Feedback latency < 15s
|
||||
- [ ] `nyquist_compliant: true` set in frontmatter
|
||||
|
||||
**Approval:** pending
|
||||
Reference in New Issue
Block a user