diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 94e1ce8..4148a64 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,9 +10,12 @@ "dependencies": { "@tailwindcss/postcss": "^4.1.18", "axios": "^1.13.2", + "i18next": "^25.8.0", + "i18next-browser-languagedetector": "^8.2.0", "lucide-react": "^0.561.0", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-i18next": "^16.5.3", "react-router-dom": "^7.10.1", "zustand": "^5.0.9" }, @@ -280,6 +283,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", @@ -2988,6 +3000,55 @@ "hermes-estree": "0.25.1" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/i18next": { + "version": "25.8.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.0.tgz", + "integrity": "sha512-urrg4HMFFMQZ2bbKRK7IZ8/CTE7D8H4JRlAwqA2ZwDRFfdd0K/4cdbNNLgfn9mo+I/h9wJu61qJzH7jCFAhUZQ==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz", + "integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3731,6 +3792,33 @@ "react": "^19.2.1" } }, + "node_modules/react-i18next": { + "version": "16.5.3", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.3.tgz", + "integrity": "sha512-fo+/NNch37zqxOzlBYrWMx0uy/yInPkRfjSuy4lqKdaecR17nvCHnEUt3QyzA8XjQ2B/0iW/5BhaHR3ZmukpGw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 25.6.2", + "react": ">= 16.8.0", + "typescript": "^5" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", @@ -3977,7 +4065,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -4059,6 +4147,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/vite": { "version": "7.2.7", "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.7.tgz", @@ -4134,6 +4231,15 @@ } } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 80ee3b6..7d0a8f6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,9 +12,12 @@ "dependencies": { "@tailwindcss/postcss": "^4.1.18", "axios": "^1.13.2", + "i18next": "^25.8.0", + "i18next-browser-languagedetector": "^8.2.0", "lucide-react": "^0.561.0", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-i18next": "^16.5.3", "react-router-dom": "^7.10.1", "zustand": "^5.0.9" }, diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b1d8c3d..9f4a031 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,6 @@ import { useEffect } from 'react'; import { BrowserRouter, Routes, Route, Navigate, useLocation } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; import Login from './pages/Login'; import FamilyView from './pages/FamilyView'; import AdminPanel from './pages/AdminPanel'; @@ -10,6 +11,7 @@ import { authApi } from './api/client'; import { Loader2 } from 'lucide-react'; function AppContent() { + const { t } = useTranslation(); const { user, isAuthenticated, isLoading, setUser, setIsLoading } = useStore(); const location = useLocation(); @@ -41,7 +43,7 @@ function AppContent() {
Список покупок пуст
+{t('shopping.empty')}
- Требуются права администратора + {t('admin.requiresAdmin')}
- Управление семьями + {t('admin.subtitle')}
- Семьи не найдены + {t('admin.noFamilies')}
- Создайте первую семью + {t('admin.createFirst')}
) : ( @@ -222,7 +224,7 @@ export default function AdminPanel() { className="w-full sm:w-auto flex items-center justify-center gap-2 px-5 py-3 bg-red-500 hover:bg-red-600 text-white rounded-xl transition-all font-semibold shadow-md hover:shadow-lg" >Общий лимит
+{t('family.totalLimit')}
{getTotalLimit().toFixed(2)} ₽
Общий остаток
+{t('family.totalRemaining')}
{getTotalRemaining().toFixed(2)} ₽
@@ -299,7 +301,7 @@ export default function FamilyView() { className="w-full flex items-center justify-center gap-2 px-6 py-3 bg-gradient-to-r from-green-500 to-emerald-600 text-white rounded-2xl hover:shadow-xl transition-all duration-300 font-semibold" >- {percentage.toFixed(0)}% осталось + {percentage.toFixed(0)}{t('category.percentRemaining')}
Нет трат
+{t('expense.noExpenses')}
) : (- Войдите, чтобы продолжить + {t('login.subtitle')}
@@ -69,7 +71,7 @@ export default function Login() { d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" /> - Войти через Google + {t('login.googleButton')} > )} diff --git a/frontend/src/pages/NoFamily.tsx b/frontend/src/pages/NoFamily.tsx index ac8dcb5..418be37 100644 --- a/frontend/src/pages/NoFamily.tsx +++ b/frontend/src/pages/NoFamily.tsx @@ -1,10 +1,12 @@ import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; import { useStore } from '../store/useStore'; import { authApi, familyApi, inviteLinkApi } from '../api/client'; import { Users, LogOut, Settings, Plus, Loader2, Eye, EyeOff } from 'lucide-react'; export default function NoFamily() { + const { t } = useTranslation(); const navigate = useNavigate(); const { user, logout, setUser } = useStore(); @@ -37,9 +39,9 @@ export default function NoFamily() { } } catch (err: any) { if (err.response?.status === 400) { - setError('Ссылка-приглашение недействительна или истекла'); + setError(t('noFamily.invalidInvite')); } else { - setError('Ошибка при присоединении к семье'); + setError(t('noFamily.joinError')); } } finally { setJoiningFamily(false); @@ -64,7 +66,7 @@ export default function NoFamily() { e.preventDefault(); if (!familyName.trim()) { - setError('Введите название семьи'); + setError(t('noFamily.enterFamilyName')); return; } @@ -87,12 +89,12 @@ export default function NoFamily() { if (err && typeof err === 'object' && 'response' in err) { const axiosError = err as { response?: { status?: number } }; if (axiosError.response?.status === 409) { - setError('Вы уже состоите в семье'); + setError(t('noFamily.alreadyInFamily')); } else { - setError('Ошибка при создании семьи'); + setError(t('noFamily.createError')); } } else { - setError('Ошибка при создании семьи'); + setError(t('noFamily.createError')); } console.error(err); } finally { @@ -105,7 +107,7 @@ export default function NoFamily() {{user?.email || user?.username} @@ -129,7 +131,7 @@ export default function NoFamily() {