feat(i18n): install react-i18next, create English locale files, and initialize i18n framework

- Install i18next, react-i18next, i18next-browser-languagedetector
- Create 6 namespace JSON files (common, collection, threads, setups, onboarding, settings)
- Initialize i18n with language detection (localStorage + navigator)
- Wire i18n import in main.tsx before React rendering

Phase 34, Plan 01
This commit is contained in:
2026-04-13 18:13:55 +02:00
parent de82eefa74
commit 8c0fb31df2
10 changed files with 323 additions and 0 deletions

39
src/client/lib/i18n.ts Normal file
View File

@@ -0,0 +1,39 @@
import i18n from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import { initReactI18next } from "react-i18next";
import enCollection from "../locales/en/collection.json";
import enCommon from "../locales/en/common.json";
import enOnboarding from "../locales/en/onboarding.json";
import enSettings from "../locales/en/settings.json";
import enSetups from "../locales/en/setups.json";
import enThreads from "../locales/en/threads.json";
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources: {
en: {
common: enCommon,
collection: enCollection,
threads: enThreads,
setups: enSetups,
onboarding: enOnboarding,
settings: enSettings,
},
},
supportedLngs: ["en", "de"],
fallbackLng: "en",
defaultNS: "common",
interpolation: {
escapeValue: false,
},
detection: {
order: ["localStorage", "navigator"],
lookupLocalStorage: "gearbox-language",
caches: ["localStorage"],
},
});
export default i18n;

View File

@@ -0,0 +1,31 @@
{
"title": "Collection",
"gear": "Gear",
"planning": "Planning",
"empty": {
"title": "Your collection is empty",
"description": "Start cataloging your gear by adding your first item. Track weight, price, and organize by category.",
"addFirst": "Add your first item"
},
"form": {
"name": "Name",
"nameRequired": "Name *",
"namePlaceholder": "e.g. Osprey Talon 22",
"weight": "Weight (g)",
"weightPlaceholder": "e.g. 680",
"price": "Price ($)",
"pricePlaceholder": "e.g. 129.99",
"quantity": "Quantity",
"category": "Category",
"notes": "Notes",
"notesPlaceholder": "Any additional notes...",
"productLink": "Product Link",
"urlPlaceholder": "https://..."
},
"classification": {
"ultralight": "Ultralight",
"light": "Light",
"medium": "Medium",
"heavy": "Heavy"
}
}

View File

@@ -0,0 +1,80 @@
{
"nav": {
"home": "Home",
"collection": "Collection",
"setups": "Setups",
"discover": "Discover",
"settings": "Settings",
"search": "Search",
"searchPlaceholder": "Search catalog...",
"profile": "Profile"
},
"actions": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"edit": "Edit",
"create": "Create",
"close": "Close",
"back": "Back",
"confirm": "Confirm",
"continue": "Continue",
"tryAgain": "Try again",
"dismiss": "Dismiss",
"saving": "Saving...",
"deleting": "Deleting...",
"creating": "Creating...",
"loading": "Loading...",
"addItem": "Add Item",
"saveChanges": "Save Changes",
"revoke": "Revoke",
"skipStep": "Skip this step"
},
"errors": {
"somethingWentWrong": "Something went wrong",
"unexpectedError": "An unexpected error occurred",
"nameRequired": "Name is required",
"positiveNumber": "Must be a positive number",
"validUrl": "Must be a valid URL (https://...)"
},
"auth": {
"signIn": "Sign in",
"signOut": "Sign out",
"joinGearBox": "Join GearBox",
"signInToGearBox": "Sign in to GearBox",
"signInDescription": "To manage your own collection, sign in or sign up.",
"createAccount": "Create account",
"redirectDescription": "You will be redirected to sign in with your account."
},
"confirm": {
"deleteItem": "Delete Item",
"deleteItemMessage": "Are you sure you want to delete <bold>{{name}}</bold>? This action cannot be undone.",
"deleteCandidate": "Delete Candidate",
"deleteCandidateMessage": "Are you sure you want to delete <bold>{{name}}</bold>? This action cannot be undone.",
"pickWinner": "Pick Winner",
"pickWinnerMessage": "Pick <bold>{{name}}</bold> as the winner? This will add it to your collection and archive the thread."
},
"externalLink": {
"title": "You are about to leave GearBox",
"redirectMessage": "You will be redirected to:"
},
"fab": {
"addToCollection": "Add to Collection",
"startNewThread": "Start New Thread",
"newSetup": "New Setup"
},
"empty": {
"noResults": "No results found",
"noItems": "No items match your search"
},
"stats": {
"items": "Items",
"totalWeight": "Total Weight",
"totalSpent": "Total Spent"
},
"filter": {
"showing": "Showing {{filtered}} of {{total}} items",
"searchItems": "Search items...",
"allCategories": "All Categories"
}
}

View File

@@ -0,0 +1,34 @@
{
"welcome": {
"title": "Welcome to GearBox",
"subtitle": "Tell us what you're into, and we'll help you set up your collection with gear that people actually use.",
"cta": "Let's go"
},
"hobby": {
"title": "What are you into?",
"subtitle": "Pick one or more — we'll show you popular gear for each.",
"continue": "Continue"
},
"items": {
"title": "Popular gear for {{hobby}}",
"titleMultiple": "Popular gear for your hobbies",
"subtitle": "Tap items you already own. We'll add them to your collection.",
"noCatalog": "No gear cataloged yet",
"noCatalogDescription": "We're still building our catalog for this hobby. You can skip this step and add gear manually later.",
"reviewCount": "Review {{count}} items",
"reviewCount_one": "Review {{count}} item"
},
"review": {
"title": "Your starting collection",
"itemsReady": "{{count}} items ready to add",
"itemsReady_one": "{{count}} item ready to add",
"noItemsSelected": "No items selected — you can always add gear later from the catalog.",
"addToCollection": "Add to my collection",
"adding": "Adding..."
},
"done": {
"title": "You're all set!",
"subtitle": "Your collection is ready. Browse the catalog anytime to discover more gear.",
"cta": "Start exploring"
}
}

View File

@@ -0,0 +1,32 @@
{
"title": "Settings",
"language": {
"title": "Language",
"description": "Change the display language of the app"
},
"weightUnit": {
"title": "Weight Unit",
"description": "Choose the unit used to display weights across the app"
},
"currency": {
"title": "Currency",
"description": "Changes the currency symbol displayed. This does not convert values."
},
"apiKeys": {
"title": "API Keys",
"description": "API keys allow programmatic access to GearBox (e.g., from Claude Desktop or scripts).",
"copyWarning": "Copy this key now — it won't be shown again:",
"namePlaceholder": "Key name (e.g., claude-desktop)"
},
"importExport": {
"title": "Import / Export",
"description": "Export your gear collection as a CSV file, or import items from a CSV.",
"export": "Export CSV",
"import": "Import CSV",
"importing": "Importing...",
"imported": "{{count}} items imported.",
"imported_one": "{{count}} item imported.",
"newCategories": "New categories: {{categories}}",
"noItemsFound": "No items found in the CSV."
}
}

View File

@@ -0,0 +1,43 @@
{
"title": "Setups",
"create": "New Setup",
"empty": {
"title": "No setups yet",
"description": "Create a setup to organize gear for specific trips or activities."
},
"card": {
"items": "{{count}} items",
"items_one": "{{count}} item",
"weight": "Weight",
"price": "Price"
},
"share": {
"title": "Share Setup",
"shareLinks": "Share Links",
"createLink": "Create Link",
"noLinks": "No share links yet",
"copyLink": "Copy link",
"revokeLink": "Revoke link",
"copied": "Copied!",
"noExpiration": "No expiration",
"expired": "Expired",
"expiresToday": "Expires today",
"expiresTomorrow": "Expires tomorrow",
"expiresInDays": "Expires in {{days}} days",
"daysOption": "{{days}} days",
"deactivateWarning": "Switching to private will deactivate all share links. They can be reactivated by switching back."
},
"visibility": {
"private": "Private",
"privateDescription": "Only you can access",
"link": "Link sharing",
"linkDescription": "Anyone with the link",
"public": "Public",
"publicDescription": "Visible on your profile"
},
"impact": {
"title": "Impact Preview",
"adding": "Adding",
"removing": "Removing"
}
}

View File

@@ -0,0 +1,45 @@
{
"title": "Research Threads",
"create": {
"title": "New Thread",
"threadName": "Thread name",
"namePlaceholder": "e.g. Lightweight sleeping bag",
"category": "Category",
"nameRequired": "Thread name is required",
"selectCategory": "Please select a category",
"createFailed": "Failed to create thread",
"createThread": "Create Thread"
},
"status": {
"active": "Active",
"researching": "Researching",
"ordered": "Ordered",
"arrived": "Arrived",
"resolved": "Resolved",
"archived": "Archived"
},
"candidate": {
"name": "Name",
"price": "Price",
"weight": "Weight",
"url": "URL",
"pros": "Pros",
"cons": "Cons",
"notes": "Notes",
"addCandidate": "Add Candidate"
},
"comparison": {
"weight": "Weight",
"price": "Price",
"pros": "Pros",
"cons": "Cons"
},
"resolve": {
"title": "Pick Winner",
"message": "Pick <bold>{{name}}</bold> as the winner? This will add it to your collection and archive the thread."
},
"empty": {
"noThreads": "No research threads yet",
"noCandidates": "No candidates yet"
}
}

View File

@@ -1,3 +1,4 @@
import "./lib/i18n";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createRouter, RouterProvider } from "@tanstack/react-router";
import { StrictMode } from "react";