15 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 34-i18n-foundation | 01 | execute | 1 |
|
true |
|
|
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.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_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 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:
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,
};
}
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 fromnavigator.language(D-10) <acceptance_criteria>- package.json contains "i18next" in dependencies
- package.json contains "react-i18next" in dependencies
- package.json contains "i18next-browser-languagedetector" in dependencies
bun installcompletes without errors </acceptance_criteria> cd /home/jlmak/Projects/jlmak/GearBox && grep -c "i18next|react-i18next|i18next-browser-languagedetector" package.json All three i18n packages installed
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:
{
"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). <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> 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 All 6 English namespace JSON files created with strings extracted from every component
Task 3: Create i18n initialization module and wire into app entry point src/client/lib/i18n.ts, src/client/main.tsx src/client/main.tsx, src/client/locales/en/common.json - 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) Create `src/client/lib/i18n.ts`: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:
import "./lib/i18n";(side-effect — initializes i18next)- Existing imports (QueryClient, Router, etc.)
<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 buildcompletes without errors </acceptance_criteria> 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" i18n initialized with language detection, all English namespaces loaded, app entry point imports i18n first
<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> |
<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>