Files
GearBox/.planning/phases/34-i18n-foundation/34-01-PLAN.md

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
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
true
D-05
D-06
D-07
D-08
D-12
truths artifacts key_links
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
path provides contains
src/client/lib/i18n.ts i18next initialization with language detection and all namespaces initReactI18next
path provides contains
src/client/locales/en/common.json English common namespace translations save
path provides contains
package.json i18n dependencies react-i18next
from to via pattern
src/client/main.tsx src/client/lib/i18n.ts import statement import.*i18n
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.

<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,
  };
}
Task 1: Install i18n packages package.json package.json - i18next is in dependencies - react-i18next is in dependencies - i18next-browser-languagedetector is in dependencies 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) <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> cd /home/jlmak/Projects/jlmak/GearBox && grep -c "i18next|react-i18next|i18next-browser-languagedetector" package.json All three i18n packages installed
Task 2: Create English locale JSON files with all translatable strings 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 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 - 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 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:

{
  "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:

  1. import "./lib/i18n"; (side-effect — initializes i18next)
  2. 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 build completes 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>
- `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

<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>
After completion, create `.planning/phases/34-i18n-foundation/34-01-SUMMARY.md`