Every component with hardcoded English strings uses useTranslation() hook
t() function calls reference keys that exist in the English locale JSON files from Plan 01
User-generated content (item names, category names, thread titles, setup names) is NOT wrapped in t()
All components import useTranslation from react-i18next
No hardcoded English strings remain in UI chrome elements (buttons, labels, headings, nav items, empty states, error messages, toasts)
path
provides
contains
src/client/components/TopNav.tsx
Translated navigation
useTranslation
path
provides
contains
src/client/components/BottomTabBar.tsx
Translated tab labels
useTranslation
path
provides
contains
src/client/routes/settings.tsx
Translated settings page
useTranslation
from
to
via
pattern
src/client/components/TopNav.tsx
src/client/locales/en/common.json
useTranslation('common')
t(
Replace all hardcoded English strings in UI components with i18n t() calls.
Purpose: Complete string extraction — after this plan, all UI chrome text comes from translation files instead of hardcoded strings.
Output: Every component uses useTranslation() hook, all strings reference keys from en/*.json.
function MyComponent() {
const { t } = useTranslation("common"); // or specific namespace
return {t("actions.save")};
}
For multiple namespaces in one component:
```typescript
const { t } = useTranslation(["common", "collection"]);
// Access: t("common:actions.save"), t("collection:itemCard.title")
// Or with default namespace: t("actions.save") uses first in array
Task 1: Extract strings from navigation and global UI components
src/client/components/TopNav.tsx, src/client/components/BottomTabBar.tsx, src/client/components/FabMenu.tsx, src/client/components/UserMenu.tsx, src/client/routes/__root.tsx
src/client/components/TopNav.tsx, src/client/components/BottomTabBar.tsx, src/client/components/FabMenu.tsx, src/client/components/UserMenu.tsx, src/client/routes/__root.tsx, src/client/locales/en/common.json
- TopNav.tsx uses t() for "Collection", "Setups", "Discover" nav labels and search placeholder
- BottomTabBar.tsx uses t() for "Home", "Collection", "Search", "Setups" tab labels
- FabMenu.tsx uses t() for all menu item labels
- UserMenu.tsx uses t() for menu items like "Settings", "Sign out", profile-related labels
- __root.tsx uses t() for "Something went wrong", "Try again", "Delete Candidate", "Pick Winner" dialog text
- All components import { useTranslation } from "react-i18next"
For each component listed in files, add `import { useTranslation } from "react-i18next"` and destructure `const { t } = useTranslation("common")` at the top of the component function body (or use the appropriate namespace).
TopNav.tsx:
Replace "Collection" with t("nav.collection")
Replace "Setups" with t("nav.setups")
Replace "Discover" with t("nav.discover")
Replace search input placeholder with t("nav.searchPlaceholder")
Replace "GearBox" brand text — leave as-is (brand name, not translatable)
BottomTabBar.tsx:
Replace "Home" with t("nav.home")
Replace "Collection" with t("nav.collection")
Replace "Search" with t("nav.search")
Replace "Setups" with t("nav.setups")
FabMenu.tsx:
Replace all menu item labels with t("fab.addItem"), t("fab.newThread"), t("fab.newSetup") etc. (read the component to find exact labels)
UserMenu.tsx:
Replace "Settings" with t("nav.settings")
Replace "Sign out" / "Log out" with t("auth.signOut")
Replace other menu text with appropriate t() keys
__root.tsx:
Replace "Something went wrong" with t("errors.somethingWentWrong")
Replace "Try again" with t("actions.tryAgain")
Replace "Delete Candidate" dialog title/text with t("common:actions.deleteCandidate") etc.
Replace "Pick Winner" dialog with t("threads:resolve.title") etc.
Replace "Cancel" buttons with t("actions.cancel")
Replace "Delete" buttons with t("actions.delete")
IMPORTANT: Do NOT wrap user-generated content (candidateName, thread title, etc.) in t() — only UI chrome.
If any new keys are needed that were not included in Plan 01's locale files, add them to the appropriate en/*.json file as part of this task.
<acceptance_criteria>
- TopNav.tsx contains useTranslation import and t( calls
- BottomTabBar.tsx contains useTranslation import and t( calls for all tab labels
- FabMenu.tsx contains useTranslation import and t( calls
- UserMenu.tsx contains useTranslation import and t( calls
- __root.tsx contains useTranslation import and t( calls for dialog text
- No hardcoded English nav/tab labels remain in these 5 files
- bun run build succeeds
</acceptance_criteria>
cd /home/jlmak/Projects/jlmak/GearBox && for f in TopNav BottomTabBar FabMenu UserMenu; do grep -c "useTranslation" src/client/components/$f.tsx; done && grep -c "useTranslation" src/client/routes/__root.tsx
Navigation and global UI components fully internationalized
Task 2: Extract strings from collection and item components
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/TotalsBar.tsx, src/client/components/DashboardCard.tsx, src/client/components/ClassificationBadge.tsx, src/client/components/ImageUpload.tsx, src/client/components/GearImage.tsx, src/client/components/GlobalItemCard.tsx, src/client/components/ManualEntryForm.tsx, src/client/components/LinkToGlobalItem.tsx, src/client/components/CategoryFilterDropdown.tsx, src/client/components/ItemPicker.tsx, src/client/components/ProfileSection.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/TotalsBar.tsx, src/client/components/DashboardCard.tsx, src/client/components/ClassificationBadge.tsx, src/client/components/ImageUpload.tsx, src/client/components/GlobalItemCard.tsx, src/client/components/ManualEntryForm.tsx, src/client/components/LinkToGlobalItem.tsx, src/client/components/CategoryFilterDropdown.tsx, src/client/components/ItemPicker.tsx, src/client/components/ProfileSection.tsx, src/client/locales/en/collection.json, src/client/locales/en/common.json
- All listed components import { useTranslation } from "react-i18next"
- Each component uses const { t } = useTranslation("collection") (or "common" for shared strings)
- Headings like "Your Collection", "Items", "Weight Summary" use t() calls
- Form labels like "Name", "Brand", "Model", "Weight", "Price", "Notes" use t() calls
- Empty states like "No items yet" use t() calls
- Action buttons already covered by common namespace t() calls
- Weight classification labels ("Ultralight", "Light", "Medium", "Heavy") use t() calls
- User-generated content (item names, category names) is NOT wrapped in t()
For each component in the files list:
1. Add `import { useTranslation } from "react-i18next"`
2. Add `const { t } = useTranslation("collection")` (or `["collection", "common"]` for components that need both namespaces)
3. Replace every hardcoded English string with the corresponding `t()` call
Key mappings (use namespace-prefixed keys when mixing namespaces):
Common-namespace strings (buttons, actions) accessed via t("common:actions.save") or by passing array ["collection", "common"].
For components that only use common strings (like ImageUpload, GearImage): use useTranslation("common").
If a component has no translatable strings (purely renders user data with no UI chrome), skip it — do NOT add unnecessary imports.
Add any new keys needed to src/client/locales/en/collection.json and src/client/locales/en/common.json.
<acceptance_criteria>
- CollectionView.tsx contains useTranslation import and t() calls
- ItemForm.tsx uses t() for all form labels (name, brand, model, weight, price, notes)
- CategoryPicker.tsx uses t() for search placeholder and "Create category" text
- WeightSummaryCard.tsx uses t() for summary labels
- ClassificationBadge.tsx uses t() for classification names
- No hardcoded English strings remain in UI chrome of these components
- bun run build succeeds
</acceptance_criteria>
cd /home/jlmak/Projects/jlmak/GearBox && for f in CollectionView ItemCard ItemForm CategoryPicker WeightSummaryCard; do echo -n "$f: "; grep -c "useTranslation" src/client/components/$f.tsx; done
Collection and item components fully internationalized
Task 3: Extract strings from thread and candidate components
src/client/components/ThreadCard.tsx, src/client/components/ThreadTabs.tsx, src/client/components/CandidateCard.tsx, src/client/components/CandidateForm.tsx, src/client/components/CandidateListItem.tsx, src/client/components/ComparisonTable.tsx, src/client/components/CreateThreadModal.tsx, src/client/components/StatusBadge.tsx, src/client/components/AddToThreadModal.tsx
src/client/components/ThreadCard.tsx, src/client/components/ThreadTabs.tsx, src/client/components/CandidateCard.tsx, src/client/components/CandidateForm.tsx, src/client/components/CandidateListItem.tsx, src/client/components/ComparisonTable.tsx, src/client/components/CreateThreadModal.tsx, src/client/components/StatusBadge.tsx, src/client/components/AddToThreadModal.tsx, src/client/locales/en/threads.json
- All listed components import useTranslation from react-i18next
- Thread components use "threads" namespace
- Status labels ("Active", "Resolved", "Archived") use t() calls
- Thread creation modal labels use t() calls
- Candidate form labels use t() calls
- Comparison table headers use t() calls
- Thread/candidate names are NOT wrapped in t() (user-generated content)
For each component:
1. Add `import { useTranslation } from "react-i18next"`
2. Add `const { t } = useTranslation("threads")` (or `["threads", "common"]`)
3. Replace all hardcoded English UI chrome strings with t() calls
Add any new keys to src/client/locales/en/threads.json.
<acceptance_criteria>
- ThreadCard.tsx contains useTranslation import and t() calls
- CandidateForm.tsx uses t() for all form labels
- ComparisonTable.tsx uses t() for column headers
- StatusBadge.tsx uses t() for status labels
- CreateThreadModal.tsx uses t() for modal title and form labels
- No hardcoded English status labels remain
- bun run build succeeds
</acceptance_criteria>
cd /home/jlmak/Projects/jlmak/GearBox && for f in ThreadCard CandidateForm ComparisonTable StatusBadge CreateThreadModal; do echo -n "$f: "; grep -c "useTranslation" src/client/components/$f.tsx; done
Thread and candidate components fully internationalized
Task 4: Extract strings from setup, modal, and route components
src/client/components/SetupsView.tsx, src/client/components/SetupCard.tsx, src/client/components/SetupImpactSelector.tsx, src/client/components/ShareModal.tsx, src/client/components/PublicSetupCard.tsx, src/client/components/ImpactDeltaBadge.tsx, src/client/components/ConfirmDialog.tsx, src/client/components/ExternalLinkDialog.tsx, src/client/components/AddToCollectionModal.tsx, src/client/components/SlideOutPanel.tsx, src/client/routes/index.tsx, src/client/routes/login.tsx, src/client/routes/profile.tsx, src/client/routes/collection/index.tsx, src/client/routes/collection/gear.tsx, src/client/routes/items/$itemId.tsx, src/client/routes/threads/index.tsx, src/client/routes/threads/$threadId.tsx, src/client/routes/setups/$setupId.tsx, src/client/routes/global-items/index.tsx, src/client/routes/global-items/$itemId.tsx, src/client/routes/users/$userId.tsx
src/client/components/SetupsView.tsx, src/client/components/SetupCard.tsx, src/client/components/SetupImpactSelector.tsx, src/client/components/ShareModal.tsx, src/client/components/PublicSetupCard.tsx, src/client/components/ConfirmDialog.tsx, src/client/components/ExternalLinkDialog.tsx, src/client/components/AddToCollectionModal.tsx, src/client/routes/index.tsx, src/client/routes/login.tsx, src/client/routes/profile.tsx, src/client/routes/collection/index.tsx, src/client/routes/items/$itemId.tsx, src/client/routes/threads/index.tsx, src/client/routes/threads/$threadId.tsx, src/client/routes/setups/$setupId.tsx, src/client/routes/global-items/index.tsx, src/client/routes/global-items/$itemId.tsx, src/client/routes/users/$userId.tsx, src/client/locales/en/setups.json, src/client/locales/en/common.json
- Setup components use "setups" namespace for setup-specific strings
- Modal/dialog components use "common" namespace
- Route pages use their respective namespace (collection routes use "collection", thread routes use "threads", etc.)
- Landing page (index.tsx) strings use "common" namespace
- Login page strings use "common" namespace
- All user-generated content (setup names, thread titles, user names) is NOT wrapped in t()
For each component/route:
1. Add `import { useTranslation } from "react-i18next"`
2. Add `const { t } = useTranslation(...)` with appropriate namespace
3. Replace all hardcoded English UI chrome strings with t() calls
Route pages: Use the matching namespace. Page-level headings and descriptions get t() calls. Links back ("Back") use t("common:actions.back").
Add any new keys to the appropriate en/*.json files.
<acceptance_criteria>
- SetupsView.tsx contains useTranslation import
- SetupCard.tsx uses t() for card labels
- ShareModal.tsx uses t() for share dialog text
- ConfirmDialog.tsx uses t() for confirmation dialog text
- Login page (routes/login.tsx) uses t() for login page text
- Landing page (routes/index.tsx) uses t() for discovery page text
- Route pages use appropriate namespaces
- bun run build succeeds
</acceptance_criteria>
cd /home/jlmak/Projects/jlmak/GearBox && grep -rl "useTranslation" src/client/components/ src/client/routes/ | wc -l
Setups, modals, dialogs, and all route pages fully internationalized
Task 5: Extract strings from onboarding and settings components
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/components/onboarding/StepIndicator.tsx, src/client/components/onboarding/HobbyCard.tsx, src/client/components/onboarding/SelectableItemCard.tsx, src/client/routes/settings.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/components/onboarding/StepIndicator.tsx, src/client/components/onboarding/HobbyCard.tsx, src/client/components/onboarding/SelectableItemCard.tsx, src/client/routes/settings.tsx, src/client/locales/en/onboarding.json, src/client/locales/en/settings.json
- Onboarding components use "onboarding" namespace
- Welcome screen: title, subtitle, CTA button text use t()
- Hobby picker: heading, description use t() (hobby names MAY be translatable if they are system-defined)
- Item browser: heading, description, search placeholder use t()
- Review: heading, description use t()
- Done: heading, description, CTA button use t()
- Settings page uses "settings" namespace
- Settings labels (Weight Unit, Currency, API Keys, Import/Export) use t()
- Settings descriptions use t()
For each onboarding component:
1. Add `import { useTranslation } from "react-i18next"`
2. Add `const { t } = useTranslation("onboarding")`
3. Replace all hardcoded strings with t() calls
"Export your gear collection as a CSV...": t("importExport.description")
"Export CSV": t("importExport.export")
"Import CSV": t("importExport.import")
"Importing...": t("importExport.importing")
Import result messages
Add any new keys to the appropriate en/*.json files.
<acceptance_criteria>
- All 9 onboarding component files contain useTranslation import
- OnboardingWelcome.tsx uses t() for title, subtitle, and CTA
- OnboardingDone.tsx uses t() for done screen text
- settings.tsx uses t() for all section headings, labels, descriptions
- settings.tsx "Weight Unit" label uses t("weightUnit.title")
- settings.tsx "API Keys" section uses t("apiKeys.title")
- No hardcoded English strings remain in onboarding or settings UI chrome
- bun run build succeeds
</acceptance_criteria>
cd /home/jlmak/Projects/jlmak/GearBox && grep -c "useTranslation" src/client/routes/settings.tsx && for f in OnboardingWelcome OnboardingHobbyPicker OnboardingItemBrowser OnboardingReview OnboardingDone; do echo -n "$f: "; grep -c "useTranslation" src/client/components/onboarding/$f.tsx; done
Onboarding flow and settings page fully internationalized
<threat_model>
Trust Boundaries
Boundary
Description
translation files→DOM
Translation strings rendered in JSX — React escapes by default
STRIDE Threat Register
Threat ID
Category
Component
Disposition
Mitigation Plan
T-34-03
Injection
t() output in JSX
accept
i18next interpolation escapeValue is false BUT React's JSX escaping prevents XSS. Translation strings are bundled static content, not user input.
</threat_model>
- `bun run build` succeeds
- grep -rl "useTranslation" finds matches in all major component and route files
- No hardcoded English UI chrome strings remain in extracted components
<success_criteria>
All UI components use useTranslation() hook
All hardcoded English strings replaced with t() calls
User-generated content is NOT wrapped in t()
Build passes
</success_criteria>
After completion, create `.planning/phases/34-i18n-foundation/34-02-SUMMARY.md`