docs(34): create phase plans for i18n foundation
This commit is contained in:
319
.planning/phases/34-i18n-foundation/34-01-PLAN.md
Normal file
319
.planning/phases/34-i18n-foundation/34-01-PLAN.md
Normal file
@@ -0,0 +1,319 @@
|
||||
---
|
||||
phase: 34-i18n-foundation
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- package.json
|
||||
- src/client/lib/i18n.ts
|
||||
- src/client/main.tsx
|
||||
- src/client/locales/en/common.json
|
||||
- src/client/locales/en/collection.json
|
||||
- src/client/locales/en/threads.json
|
||||
- src/client/locales/en/setups.json
|
||||
- src/client/locales/en/onboarding.json
|
||||
- src/client/locales/en/settings.json
|
||||
autonomous: true
|
||||
requirements: [D-05, D-06, D-07, D-08, D-12]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "i18next, react-i18next, and i18next-browser-languagedetector are installed"
|
||||
- "i18n.ts initializes i18next with LanguageDetector and initReactI18next"
|
||||
- "English locale JSON files exist in src/client/locales/en/ with namespaces: common, collection, threads, setups, onboarding, settings"
|
||||
- "main.tsx imports i18n.ts before rendering the app"
|
||||
- "fallback language is set to en"
|
||||
- "Language detection order is localStorage then navigator"
|
||||
artifacts:
|
||||
- path: "src/client/lib/i18n.ts"
|
||||
provides: "i18next initialization with language detection and all namespaces"
|
||||
contains: "initReactI18next"
|
||||
- path: "src/client/locales/en/common.json"
|
||||
provides: "English common namespace translations"
|
||||
contains: "save"
|
||||
- path: "package.json"
|
||||
provides: "i18n dependencies"
|
||||
contains: "react-i18next"
|
||||
key_links:
|
||||
- from: "src/client/main.tsx"
|
||||
to: "src/client/lib/i18n.ts"
|
||||
via: "import statement"
|
||||
pattern: "import.*i18n"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Install the i18n framework (react-i18next) and create all English locale JSON files with namespace structure.
|
||||
|
||||
Purpose: Foundation — all other plans depend on having i18next initialized and English strings extracted into JSON files.
|
||||
Output: Working i18n setup with all English translation files, app initializes i18next before rendering.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/34-i18n-foundation/34-CONTEXT.md
|
||||
@.planning/phases/34-i18n-foundation/34-RESEARCH.md
|
||||
|
||||
<interfaces>
|
||||
From src/client/main.tsx:
|
||||
```typescript
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { createRouter, RouterProvider } from "@tanstack/react-router";
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { routeTree } from "./routeTree.gen";
|
||||
```
|
||||
|
||||
From src/client/hooks/useFormatters.ts:
|
||||
```typescript
|
||||
export function useFormatters() {
|
||||
const unit = useWeightUnit();
|
||||
const currency = useCurrency();
|
||||
return {
|
||||
weight: (grams: number | null) => formatWeight(grams, unit),
|
||||
price: (cents: number | null) => formatPrice(cents, currency),
|
||||
unit,
|
||||
currency,
|
||||
};
|
||||
}
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Install i18n packages</name>
|
||||
<files>package.json</files>
|
||||
<read_first>package.json</read_first>
|
||||
<behavior>
|
||||
- i18next is in dependencies
|
||||
- react-i18next is in dependencies
|
||||
- i18next-browser-languagedetector is in dependencies
|
||||
</behavior>
|
||||
<action>
|
||||
Run: `bun add i18next react-i18next i18next-browser-languagedetector`
|
||||
|
||||
This adds the three required packages:
|
||||
- `i18next` — core translation engine (~8KB)
|
||||
- `react-i18next` — React hooks and components (`useTranslation`)
|
||||
- `i18next-browser-languagedetector` — auto-detect browser locale from `navigator.language` (D-10)
|
||||
</action>
|
||||
<acceptance_criteria>
|
||||
- package.json contains "i18next" in dependencies
|
||||
- package.json contains "react-i18next" in dependencies
|
||||
- package.json contains "i18next-browser-languagedetector" in dependencies
|
||||
- `bun install` completes without errors
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
<automated>cd /home/jlmak/Projects/jlmak/GearBox && grep -c "i18next\|react-i18next\|i18next-browser-languagedetector" package.json</automated>
|
||||
</verify>
|
||||
<done>All three i18n packages installed</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Create English locale JSON files with all translatable strings</name>
|
||||
<files>src/client/locales/en/common.json, src/client/locales/en/collection.json, src/client/locales/en/threads.json, src/client/locales/en/setups.json, src/client/locales/en/onboarding.json, src/client/locales/en/settings.json</files>
|
||||
<read_first>src/client/components/TopNav.tsx, src/client/components/BottomTabBar.tsx, src/client/components/FabMenu.tsx, src/client/components/ConfirmDialog.tsx, src/client/components/AuthPromptModal.tsx, src/client/routes/__root.tsx, src/client/components/CollectionView.tsx, src/client/components/ItemCard.tsx, src/client/components/ItemForm.tsx, src/client/components/CategoryPicker.tsx, src/client/components/CategoryHeader.tsx, src/client/components/WeightSummaryCard.tsx, src/client/components/PlanningView.tsx, src/client/components/ThreadCard.tsx, src/client/components/ThreadTabs.tsx, src/client/components/CandidateCard.tsx, src/client/components/CandidateForm.tsx, src/client/components/ComparisonTable.tsx, src/client/components/CreateThreadModal.tsx, src/client/components/SetupsView.tsx, src/client/components/SetupCard.tsx, src/client/components/SetupImpactSelector.tsx, src/client/components/ShareModal.tsx, src/client/components/onboarding/OnboardingWelcome.tsx, src/client/components/onboarding/OnboardingHobbyPicker.tsx, src/client/components/onboarding/OnboardingItemBrowser.tsx, src/client/components/onboarding/OnboardingReview.tsx, src/client/components/onboarding/OnboardingDone.tsx, src/client/components/onboarding/OnboardingFlow.tsx, src/client/routes/settings.tsx, src/client/components/StatusBadge.tsx, src/client/components/ClassificationBadge.tsx, src/client/components/ExternalLinkDialog.tsx, src/client/components/CatalogSearchOverlay.tsx, src/client/components/AddToCollectionModal.tsx, src/client/components/AddToThreadModal.tsx, src/client/components/GlobalItemCard.tsx, src/client/components/GearImage.tsx, src/client/components/ImageUpload.tsx, src/client/components/DashboardCard.tsx, src/client/components/TotalsBar.tsx, src/client/components/ImpactDeltaBadge.tsx, src/client/routes/index.tsx, src/client/routes/login.tsx, src/client/routes/profile.tsx, src/client/components/UserMenu.tsx, src/client/components/ProfileSection.tsx, src/client/components/PublicSetupCard.tsx, src/client/components/ManualEntryForm.tsx, src/client/components/LinkToGlobalItem.tsx, src/client/components/SlideOutPanel.tsx, src/client/components/ItemPicker.tsx, src/client/components/ImageCropEditor.tsx, src/client/components/CategoryFilterDropdown.tsx</read_first>
|
||||
<behavior>
|
||||
- src/client/locales/en/common.json contains keys for: nav items, action buttons (save, cancel, delete, edit, create, back, close, search, confirm), empty states, error messages, loading states, auth prompts
|
||||
- src/client/locales/en/collection.json contains keys for: collection page, item cards, item forms, category picker, weight summary, planning view, totals bar
|
||||
- src/client/locales/en/threads.json contains keys for: thread list, thread detail, candidate cards, candidate form, comparison table, create thread modal, status badges
|
||||
- src/client/locales/en/setups.json contains keys for: setup list, setup detail, setup cards, impact preview, share modal
|
||||
- src/client/locales/en/onboarding.json contains keys for: welcome, hobby picker, item browser, review, done screens
|
||||
- src/client/locales/en/settings.json contains keys for: settings page, weight unit, currency, API keys, import/export
|
||||
</behavior>
|
||||
<action>
|
||||
Create directory `src/client/locales/en/`.
|
||||
|
||||
Read EVERY component listed in read_first. For each component, extract all hardcoded English strings (button text, headings, labels, descriptions, placeholder text, error messages, empty states, toast messages, modal titles/descriptions, confirmation dialogs) and add them to the appropriate namespace JSON file.
|
||||
|
||||
**String key convention:** Nested objects with dot notation access. Group by component/feature. Use camelCase for keys.
|
||||
|
||||
Example structure for `common.json`:
|
||||
```json
|
||||
{
|
||||
"nav": {
|
||||
"home": "Home",
|
||||
"collection": "Collection",
|
||||
"setups": "Setups",
|
||||
"discover": "Discover",
|
||||
"settings": "Settings",
|
||||
"search": "Search"
|
||||
},
|
||||
"actions": {
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"create": "Create",
|
||||
"close": "Close",
|
||||
"back": "Back",
|
||||
"confirm": "Confirm",
|
||||
"tryAgain": "Try again",
|
||||
"dismiss": "Dismiss",
|
||||
"loading": "Loading...",
|
||||
"saving": "Saving...",
|
||||
"deleting": "Deleting..."
|
||||
},
|
||||
"errors": {
|
||||
"somethingWentWrong": "Something went wrong",
|
||||
"unexpectedError": "An unexpected error occurred"
|
||||
},
|
||||
"auth": {
|
||||
"signIn": "Sign in",
|
||||
"signInRequired": "Sign in to continue",
|
||||
"signInDescription": "Create an account or sign in to start tracking your gear"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**IMPORTANT:** Read every component file thoroughly. Do NOT guess strings — extract the actual English text from the JSX. Every user-visible string in the component should become a translation key.
|
||||
|
||||
For interpolation (dynamic values), use `{{variable}}` syntax. Example: if a component shows "3 items", the key would be `"itemCount": "{{count}} items"` or use pluralization `"itemCount_one": "{{count}} item"`, `"itemCount_other": "{{count}} items"`.
|
||||
|
||||
Do NOT translate: item names, category names created by users, thread titles, candidate names, setup names — these are user-generated content (D-03).
|
||||
</action>
|
||||
<acceptance_criteria>
|
||||
- src/client/locales/en/common.json exists and is valid JSON
|
||||
- src/client/locales/en/collection.json exists and is valid JSON
|
||||
- src/client/locales/en/threads.json exists and is valid JSON
|
||||
- src/client/locales/en/setups.json exists and is valid JSON
|
||||
- src/client/locales/en/onboarding.json exists and is valid JSON
|
||||
- src/client/locales/en/settings.json exists and is valid JSON
|
||||
- common.json contains "nav" key with at least "home", "collection", "setups"
|
||||
- common.json contains "actions" key with at least "save", "cancel", "delete"
|
||||
- settings.json contains keys for "weightUnit", "currency", "apiKeys", "importExport"
|
||||
- onboarding.json contains keys for all 5 onboarding steps (welcome, hobby, items, review, done)
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
<automated>cd /home/jlmak/Projects/jlmak/GearBox && for f in common collection threads setups onboarding settings; do node -e "JSON.parse(require('fs').readFileSync('src/client/locales/en/$f.json','utf8')); console.log('$f.json: valid')"; done</automated>
|
||||
</verify>
|
||||
<done>All 6 English namespace JSON files created with strings extracted from every component</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Create i18n initialization module and wire into app entry point</name>
|
||||
<files>src/client/lib/i18n.ts, src/client/main.tsx</files>
|
||||
<read_first>src/client/main.tsx, src/client/locales/en/common.json</read_first>
|
||||
<behavior>
|
||||
- src/client/lib/i18n.ts initializes i18next with LanguageDetector and initReactI18next
|
||||
- Resources include all 6 namespaces for "en" locale
|
||||
- fallbackLng is "en"
|
||||
- defaultNS is "common"
|
||||
- interpolation.escapeValue is false (React handles XSS)
|
||||
- Detection order is ["localStorage", "navigator"]
|
||||
- Detection lookupLocalStorage is "gearbox-language"
|
||||
- Detection caches is ["localStorage"]
|
||||
- main.tsx imports i18n.ts before any React rendering (side-effect import)
|
||||
</behavior>
|
||||
<action>
|
||||
Create `src/client/lib/i18n.ts`:
|
||||
|
||||
```typescript
|
||||
import i18n from "i18next";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
|
||||
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";
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources: {
|
||||
en: {
|
||||
common: enCommon,
|
||||
collection: enCollection,
|
||||
threads: enThreads,
|
||||
setups: enSetups,
|
||||
onboarding: enOnboarding,
|
||||
settings: enSettings,
|
||||
},
|
||||
},
|
||||
fallbackLng: "en",
|
||||
defaultNS: "common",
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
detection: {
|
||||
order: ["localStorage", "navigator"],
|
||||
lookupLocalStorage: "gearbox-language",
|
||||
caches: ["localStorage"],
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
```
|
||||
|
||||
Update `src/client/main.tsx` — add `import "./lib/i18n";` as the FIRST import (before React, before QueryClient, before Router). This ensures i18next is initialized before any component tries to use `useTranslation()`. The import is a side-effect import — no named export needed.
|
||||
|
||||
The final import order in main.tsx should be:
|
||||
1. `import "./lib/i18n";` (side-effect — initializes i18next)
|
||||
2. Existing imports (QueryClient, Router, etc.)
|
||||
</action>
|
||||
<acceptance_criteria>
|
||||
- src/client/lib/i18n.ts exists
|
||||
- src/client/lib/i18n.ts contains `import { initReactI18next } from "react-i18next"`
|
||||
- src/client/lib/i18n.ts contains `fallbackLng: "en"`
|
||||
- src/client/lib/i18n.ts contains `defaultNS: "common"`
|
||||
- src/client/lib/i18n.ts contains `lookupLocalStorage: "gearbox-language"`
|
||||
- src/client/lib/i18n.ts imports all 6 en namespace JSON files
|
||||
- src/client/main.tsx first import line is `import "./lib/i18n"`
|
||||
- `bun run build` completes without errors
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
<automated>cd /home/jlmak/Projects/jlmak/GearBox && grep -c "initReactI18next\|fallbackLng\|defaultNS\|LanguageDetector" src/client/lib/i18n.ts && head -3 src/client/main.tsx | grep -c "i18n"</automated>
|
||||
</verify>
|
||||
<done>i18n initialized with language detection, all English namespaces loaded, app entry point imports i18n first</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| localStorage→i18n | Language preference read from localStorage — treated as user preference, not security-sensitive |
|
||||
| navigator.language→i18n | Browser locale — untrusted but benign (only matched against known locales) |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-34-01 | Tampering | i18n.ts localStorage | accept | Language preference is non-sensitive. Tampered localStorage key only affects UI language, not data. Validated against known locale list via i18next supportedLngs. |
|
||||
| T-34-02 | Information Disclosure | locale JSON files | accept | Translation files contain only UI strings, no secrets. Bundled in client JS — intentionally public. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `bun install` completes without errors
|
||||
- All 6 en/*.json files are valid JSON
|
||||
- `bun run build` completes without errors
|
||||
- src/client/lib/i18n.ts initializes correctly with all namespaces
|
||||
- src/client/main.tsx imports i18n before rendering
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- i18next and react-i18next installed
|
||||
- All English translation strings extracted into 6 namespace JSON files
|
||||
- i18n initialization module created with language detection
|
||||
- App entry point wires i18n before React rendering
|
||||
- Build passes
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/34-i18n-foundation/34-01-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user