diff --git a/.gitignore b/.gitignore index 4536beb..627e94b 100644 --- a/.gitignore +++ b/.gitignore @@ -197,3 +197,4 @@ yarn.lock app/dashboard/build/ dashboard/.vite/deps_temp_f42dd32d/package.json +dashboard/.vite/deps diff --git a/dashboard/package.json b/dashboard/package.json index aa2b59d..f1f3403 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -29,11 +29,17 @@ "@tanstack/react-router": "^1.20.1", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", + "dayjs": "^1.11.10", + "i18next": "^23.10.1", + "i18next-browser-languagedetector": "^7.2.0", + "i18next-http-backend": "^2.5.0", "lucide-react": "^0.359.0", "next-themes": "^0.3.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hook-form": "^7.51.1", + "react-i18next": "^14.1.0", + "react-native-path": "^0.0.5", "react-resizable-panels": "^2.0.13", "sonner": "^1.4.3", "tailwind-merge": "^2.2.2", diff --git a/dashboard/pnpm-lock.yaml b/dashboard/pnpm-lock.yaml index f5f7298..b476b5e 100644 --- a/dashboard/pnpm-lock.yaml +++ b/dashboard/pnpm-lock.yaml @@ -62,6 +62,18 @@ dependencies: clsx: specifier: ^2.1.0 version: 2.1.0 + dayjs: + specifier: ^1.11.10 + version: 1.11.10 + i18next: + specifier: ^23.10.1 + version: 23.10.1 + i18next-browser-languagedetector: + specifier: ^7.2.0 + version: 7.2.0 + i18next-http-backend: + specifier: ^2.5.0 + version: 2.5.0 lucide-react: specifier: ^0.359.0 version: 0.359.0(react@18.2.0) @@ -77,6 +89,12 @@ dependencies: react-hook-form: specifier: ^7.51.1 version: 7.51.1(react@18.2.0) + react-i18next: + specifier: ^14.1.0 + version: 14.1.0(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0) + react-native-path: + specifier: ^0.0.5 + version: 0.0.5 react-resizable-panels: specifier: ^2.0.13 version: 2.0.13(react-dom@18.2.0)(react@18.2.0) @@ -2016,6 +2034,14 @@ packages: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: true + /cross-fetch@4.0.0: + resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + dev: false + /cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -2039,6 +2065,10 @@ packages: '@babel/runtime': 7.24.0 dev: true + /dayjs@1.11.10: + resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==} + dev: false + /debug@4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -2415,6 +2445,32 @@ packages: dependencies: function-bind: 1.1.2 + /html-parse-stringify@3.0.1: + resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + dependencies: + void-elements: 3.1.0 + dev: false + + /i18next-browser-languagedetector@7.2.0: + resolution: {integrity: sha512-U00DbDtFIYD3wkWsr2aVGfXGAj2TgnELzOX9qv8bT0aJtvPV9CRO77h+vgmHFBMe7LAxdwvT/7VkCWGya6L3tA==} + dependencies: + '@babel/runtime': 7.24.0 + dev: false + + /i18next-http-backend@2.5.0: + resolution: {integrity: sha512-Z/aQsGZk1gSxt2/DztXk92DuDD20J+rNudT7ZCdTrNOiK8uQppfvdjq9+DFQfpAnFPn3VZS+KQIr1S/W1KxhpQ==} + dependencies: + cross-fetch: 4.0.0 + transitivePeerDependencies: + - encoding + dev: false + + /i18next@23.10.1: + resolution: {integrity: sha512-NDiIzFbcs3O9PXpfhkjyf7WdqFn5Vq6mhzhtkXzj51aOcNuPNcTwuYNuXCpHsanZGHlHKL35G7huoFeVic1hng==} + dependencies: + '@babel/runtime': 7.24.0 + dev: false + /ignore@5.3.1: resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} engines: {node: '>= 4'} @@ -2651,6 +2707,18 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + dev: false + /node-releases@2.0.14: resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} dev: true @@ -2863,6 +2931,30 @@ packages: react: 18.2.0 dev: false + /react-i18next@14.1.0(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-3KwX6LHpbvGQ+sBEntjV4sYW3Zovjjl3fpoHbUwSgFHf0uRBcbeCBLR5al6ikncI5+W0EFb71QXZmfop+J6NrQ==} + peerDependencies: + i18next: '>= 23.2.3' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + dependencies: + '@babel/runtime': 7.24.0 + html-parse-stringify: 3.0.1 + i18next: 23.10.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /react-native-path@0.0.5: + resolution: {integrity: sha512-WJr256xBquk7X2O83QYWKqgLg43Zg3SrgjPc/kr0gCD2LoXA+2L72BW4cmstH12GbGeutqs/eXk3jgDQ2iCSvQ==} + dev: false + /react-remove-scroll-bar@2.3.6(@types/react@18.2.64)(react@18.2.0): resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==} engines: {node: '>=10'} @@ -3176,6 +3268,10 @@ packages: dependencies: is-number: 7.0.0 + /tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: false + /ts-api-utils@1.3.0(typescript@5.2.2): resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} engines: {node: '>=16'} @@ -3322,6 +3418,22 @@ packages: fsevents: 2.3.3 dev: true + /void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + dev: false + + /webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: false + + /whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + dev: false + /which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} diff --git a/dashboard/public/locales/en.json b/dashboard/public/locales/en.json new file mode 100644 index 0000000..f668ba9 --- /dev/null +++ b/dashboard/public/locales/en.json @@ -0,0 +1,7 @@ +{ + "name": "Name", + "users": "Users", + "nodes": "Nodes", + "services": "Services", + "hosts": "Hosts" +} diff --git a/dashboard/src/app.tsx b/dashboard/src/app.tsx index a1ac7b2..a816dc6 100644 --- a/dashboard/src/app.tsx +++ b/dashboard/src/app.tsx @@ -1,6 +1,7 @@ import React, { StrictMode } from 'react' import ReactDOM from 'react-dom/client' import { RouterProvider, createRouter } from '@tanstack/react-router' +import '@marzneshin/features/i18n' import './globals.css' import { routeTree } from './routeTree.gen' diff --git a/dashboard/src/features/i18n/index.ts b/dashboard/src/features/i18n/index.ts new file mode 100644 index 0000000..49afb7b --- /dev/null +++ b/dashboard/src/features/i18n/index.ts @@ -0,0 +1,46 @@ +import dayjs from 'dayjs'; +import i18n from 'i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; +import HttpApi from 'i18next-http-backend'; +import { initReactI18next } from 'react-i18next'; +import path from 'react-native-path'; + +declare module 'i18next' { + interface CustomTypeOptions { + returnNull: false; + } +} + +i18n + .use(LanguageDetector) + .use(initReactI18next) + .use(HttpApi) + .init( + { + debug: import.meta.env.NODE_ENV === 'development', + returnNull: false, + fallbackLng: 'en', + interpolation: { + escapeValue: false, + }, + react: { + useSuspense: false, + }, + load: 'languageOnly', + detection: { + caches: ['localStorage', 'sessionStorage', 'cookie'], + }, + backend: { + loadPath: path.resolve(import.meta.env.BASE_URL, 'locales/{{lng}}.json'), + }, + }, + function() { + dayjs.locale(i18n.language); + } + ); + +i18n.on('languageChanged', (lng) => { + dayjs.locale(lng); +}); + +export default i18n; diff --git a/dashboard/src/routes/index.lazy.tsx b/dashboard/src/routes/index.lazy.tsx index 682c0c7..7f957cd 100644 --- a/dashboard/src/routes/index.lazy.tsx +++ b/dashboard/src/routes/index.lazy.tsx @@ -1,16 +1,18 @@ import { Button } from '@marzneshin/components' import { ThemeToggle } from '@marzneshin/features/theme-switch' import { createLazyFileRoute } from '@tanstack/react-router' +import { useTranslation } from 'react-i18next'; export const Route = createLazyFileRoute('/')({ component: Index, }) function Index() { + const { t } = useTranslation(); return (
- +
) } diff --git a/dashboard/src/utils.ts b/dashboard/src/utils/cn.ts similarity index 80% rename from dashboard/src/utils.ts rename to dashboard/src/utils/cn.ts index d084cca..03aaa4b 100644 --- a/dashboard/src/utils.ts +++ b/dashboard/src/utils/cn.ts @@ -2,5 +2,5 @@ import { type ClassValue, clsx } from "clsx" import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) + return twMerge(clsx(inputs)) } diff --git a/dashboard/src/utils/index.ts b/dashboard/src/utils/index.ts new file mode 100644 index 0000000..c5c1882 --- /dev/null +++ b/dashboard/src/utils/index.ts @@ -0,0 +1 @@ +export * from './cn' diff --git a/dashboard/tsconfig.json b/dashboard/tsconfig.json index 777da6e..9ed11be 100644 --- a/dashboard/tsconfig.json +++ b/dashboard/tsconfig.json @@ -26,6 +26,12 @@ "paths": { "@marzneshin/*": [ "./src/*" + ], + "@marzneshin/components": [ + "./src/components/*" + ], + "@marzneshin/features": [ + "./src/features/*" ] } },