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:
15
bun.lock
15
bun.lock
@@ -17,11 +17,14 @@
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"framer-motion": "^12.38.0",
|
||||
"hono": "^4.12.8",
|
||||
"i18next": "^26.0.4",
|
||||
"i18next-browser-languagedetector": "^8.2.1",
|
||||
"lucide-react": "^0.577.0",
|
||||
"postgres": "^3.4.9",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-easy-crop": "^5.5.7",
|
||||
"react-i18next": "^17.0.2",
|
||||
"recharts": "^3.8.0",
|
||||
"sharp": "^0.34.5",
|
||||
"sonner": "^2.0.7",
|
||||
@@ -166,6 +169,8 @@
|
||||
|
||||
"@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A=="],
|
||||
|
||||
"@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
|
||||
|
||||
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
|
||||
|
||||
"@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
|
||||
@@ -794,8 +799,14 @@
|
||||
|
||||
"hono": ["hono@4.12.8", "", {}, "sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A=="],
|
||||
|
||||
"html-parse-stringify": ["html-parse-stringify@3.0.1", "", { "dependencies": { "void-elements": "3.1.0" } }, "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg=="],
|
||||
|
||||
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||
|
||||
"i18next": ["i18next@26.0.4", "", { "dependencies": { "@babel/runtime": "^7.29.2" }, "peerDependencies": { "typescript": "^5 || ^6" }, "optionalPeers": ["typescript"] }, "sha512-gXF7U9bfioXPLv7mw8Qt2nfO7vij5MyINvPgVv99pX3fL1Y01pw2mKBFrlYpRxRCl2wz3ISenj6VsMJT2isfuA=="],
|
||||
|
||||
"i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.1", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
@@ -964,6 +975,8 @@
|
||||
|
||||
"react-easy-crop": ["react-easy-crop@5.5.7", "", { "dependencies": { "normalize-wheel": "^1.0.1", "tslib": "^2.0.1" }, "peerDependencies": { "react": ">=16.4.0", "react-dom": ">=16.4.0" } }, "sha512-kYo4NtMeXFQB7h1U+h5yhUkE46WQbQdq7if54uDlbMdZHdRgNehfvaFrXnFw5NR1PNoUOJIfTwLnWmEx/MaZnA=="],
|
||||
|
||||
"react-i18next": ["react-i18next@17.0.2", "", { "dependencies": { "@babel/runtime": "^7.29.2", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 26.0.1", "react": ">= 16.8.0", "typescript": "^5 || ^6" }, "optionalPeers": ["typescript"] }, "sha512-shBftH2vaTWK2Bsp7FiL+cevx3xFJlvFxmsDFQSrJc+6twHkP0tv/bGa01VVWzpreUVVwU+3Hev5iFqRg65RwA=="],
|
||||
|
||||
"react-is": ["react-is@19.2.4", "", {}, "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA=="],
|
||||
|
||||
"react-redux": ["react-redux@9.2.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="],
|
||||
@@ -1102,6 +1115,8 @@
|
||||
|
||||
"vite": ["vite@8.0.0", "", { "dependencies": { "@oxc-project/runtime": "0.115.0", "lightningcss": "^1.32.0", "picomatch": "^4.0.3", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.9", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.0.0-alpha.31", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q=="],
|
||||
|
||||
"void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="],
|
||||
|
||||
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
@@ -48,11 +48,14 @@
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"framer-motion": "^12.38.0",
|
||||
"hono": "^4.12.8",
|
||||
"i18next": "^26.0.4",
|
||||
"i18next-browser-languagedetector": "^8.2.1",
|
||||
"lucide-react": "^0.577.0",
|
||||
"postgres": "^3.4.9",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-easy-crop": "^5.5.7",
|
||||
"react-i18next": "^17.0.2",
|
||||
"recharts": "^3.8.0",
|
||||
"sharp": "^0.34.5",
|
||||
"sonner": "^2.0.7",
|
||||
|
||||
39
src/client/lib/i18n.ts
Normal file
39
src/client/lib/i18n.ts
Normal 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;
|
||||
31
src/client/locales/en/collection.json
Normal file
31
src/client/locales/en/collection.json
Normal 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"
|
||||
}
|
||||
}
|
||||
80
src/client/locales/en/common.json
Normal file
80
src/client/locales/en/common.json
Normal 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"
|
||||
}
|
||||
}
|
||||
34
src/client/locales/en/onboarding.json
Normal file
34
src/client/locales/en/onboarding.json
Normal 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"
|
||||
}
|
||||
}
|
||||
32
src/client/locales/en/settings.json
Normal file
32
src/client/locales/en/settings.json
Normal 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."
|
||||
}
|
||||
}
|
||||
43
src/client/locales/en/setups.json
Normal file
43
src/client/locales/en/setups.json
Normal 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"
|
||||
}
|
||||
}
|
||||
45
src/client/locales/en/threads.json
Normal file
45
src/client/locales/en/threads.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user