diff --git 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 (
<div className="p-2">
<ThemeToggle />
- <Button>Hello</Button>
+ <Button>{t('name')}</Button>
</div>
)
}
diff --git a/dashboard/src/utils.ts b/dashboard/src/utils/cn.ts
rename from dashboard/src/utils.ts
rename to 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
@@ -0,0 +1 @@
+export * from './cn'
diff --git a/dashboard/tsconfig.json b/dashboard/tsconfig.json
@@ -26,6 +26,12 @@
"paths": {
"@marzneshin/*": [
"./src/*"
+ ],
+ "@marzneshin/components": [
+ "./src/components/*"
+ ],
+ "@marzneshin/features": [
+ "./src/features/*"
]
}
},