diff --git a/apps/dokploy/components/dashboard/settings/appearance-form.tsx b/apps/dokploy/components/dashboard/settings/appearance-form.tsx index 52142fcd6..a64a66b00 100644 --- a/apps/dokploy/components/dashboard/settings/appearance-form.tsx +++ b/apps/dokploy/components/dashboard/settings/appearance-form.tsx @@ -20,6 +20,15 @@ import { FormMessage, } from "@/components/ui/form"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import useLocale from "@/utils/hooks/use-locale"; +import { useTranslation } from "next-i18next"; import { useTheme } from "next-themes"; import { useEffect } from "react"; import { toast } from "sonner"; @@ -28,6 +37,9 @@ const appearanceFormSchema = z.object({ theme: z.enum(["light", "dark", "system"], { required_error: "Please select a theme.", }), + language: z.enum(["en", "zh-Hans"], { + required_error: "Please select a language.", + }), }); type AppearanceFormValues = z.infer; @@ -35,10 +47,14 @@ type AppearanceFormValues = z.infer; // This can come from your database or API. const defaultValues: Partial = { theme: "system", + language: "en", }; export function AppearanceForm() { const { setTheme, theme } = useTheme(); + const { locale, setLocale } = useLocale(); + const { t } = useTranslation("settings"); + const form = useForm({ resolver: zodResolver(appearanceFormSchema), defaultValues, @@ -47,19 +63,23 @@ export function AppearanceForm() { useEffect(() => { form.reset({ theme: (theme ?? "system") as AppearanceFormValues["theme"], + language: locale ?? "en", }); - }, [form, theme]); + }, [form, theme, locale]); function onSubmit(data: AppearanceFormValues) { setTheme(data.theme); + setLocale(data.language); toast.success("Preferences Updated"); } return ( - Appearance + + {t("settings.appearance.title")} + - Customize the theme of your dashboard. + {t("settings.appearance.description")} @@ -131,6 +151,42 @@ export function AppearanceForm() { }} /> + { + return ( + + Language + + Select a language for your dashboard + + + + + ); + }} + /> + diff --git a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx index 799227001..910e8bcd9 100644 --- a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx +++ b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx @@ -52,7 +52,7 @@ const randomImages = [ export const ProfileForm = () => { const { data, refetch } = api.auth.get.useQuery(); const { mutateAsync, isLoading } = api.auth.update.useMutation(); - const { t } = useTranslation("common"); + const { t } = useTranslation("settings"); const form = useForm({ defaultValues: { @@ -94,7 +94,7 @@ export const ProfileForm = () => {
- {t("dashboard.settings.profile.title")} + {t("settings.profile.title")} Change the details of your profile here. diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index 9a281b796..df8e7c254 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -86,6 +86,7 @@ "drizzle-zod": "0.5.1", "i18next": "^23.16.4", "input-otp": "^1.2.4", + "js-cookie": "^3.0.5", "js-yaml": "4.1.0", "lodash": "4.17.21", "lucia": "^3.0.1", @@ -122,6 +123,7 @@ "devDependencies": { "@types/adm-zip": "^0.5.5", "@types/bcrypt": "5.0.2", + "@types/js-cookie": "^3.0.6", "@types/js-yaml": "4.0.9", "@types/lodash": "4.17.4", "@types/node": "^18.17.0", diff --git a/apps/dokploy/pages/dashboard/settings/appearance.tsx b/apps/dokploy/pages/dashboard/settings/appearance.tsx index af317b0a6..f074f2898 100644 --- a/apps/dokploy/pages/dashboard/settings/appearance.tsx +++ b/apps/dokploy/pages/dashboard/settings/appearance.tsx @@ -2,9 +2,11 @@ import { AppearanceForm } from "@/components/dashboard/settings/appearance-form" import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { SettingsLayout } from "@/components/layouts/settings-layout"; import { appRouter } from "@/server/api/root"; +import { getLocale } from "@/utils/i18n"; import { validateRequest } from "@dokploy/server"; import { createServerSideHelpers } from "@trpc/react-query/server"; import type { GetServerSidePropsContext } from "next"; +import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import React, { type ReactElement } from "react"; import superjson from "superjson"; @@ -30,6 +32,7 @@ export async function getServerSideProps( ) { const { req, res } = ctx; const { user, session } = await validateRequest(req, res); + const locale = getLocale(req.cookies); const helpers = createServerSideHelpers({ router: appRouter, @@ -63,6 +66,7 @@ export async function getServerSideProps( return { props: { trpcState: helpers.dehydrate(), + ...(await serverSideTranslations(locale, ["settings"])), }, }; } diff --git a/apps/dokploy/pages/dashboard/settings/profile.tsx b/apps/dokploy/pages/dashboard/settings/profile.tsx index 4dd68768f..9303354f1 100644 --- a/apps/dokploy/pages/dashboard/settings/profile.tsx +++ b/apps/dokploy/pages/dashboard/settings/profile.tsx @@ -78,7 +78,7 @@ export async function getServerSideProps( return { props: { trpcState: helpers.dehydrate(), - ...(await serverSideTranslations(locale, ["common"])), + ...(await serverSideTranslations(locale, ["settings"])), }, }; } diff --git a/apps/dokploy/public/locales/en/common.json b/apps/dokploy/public/locales/en/common.json index 4759dc898..0967ef424 100644 --- a/apps/dokploy/public/locales/en/common.json +++ b/apps/dokploy/public/locales/en/common.json @@ -1,9 +1 @@ -{ - "dashboard": { - "settings": { - "profile": { - "title": "Account" - } - } - } -} +{} diff --git a/apps/dokploy/public/locales/en/settings.json b/apps/dokploy/public/locales/en/settings.json new file mode 100644 index 000000000..1b3fcdc86 --- /dev/null +++ b/apps/dokploy/public/locales/en/settings.json @@ -0,0 +1,11 @@ +{ + "settings": { + "profile": { + "title": "Account" + }, + "appearance": { + "title": "Appearance", + "description": "Customize the theme of your dashboard." + } + } +} diff --git a/apps/dokploy/public/locales/zh-Hans/common.json b/apps/dokploy/public/locales/zh-Hans/common.json index c18b04ecb..0967ef424 100644 --- a/apps/dokploy/public/locales/zh-Hans/common.json +++ b/apps/dokploy/public/locales/zh-Hans/common.json @@ -1,9 +1 @@ -{ - "dashboard": { - "settings": { - "profile": { - "title": "账户偏好" - } - } - } -} +{} diff --git a/apps/dokploy/public/locales/zh-Hans/settings.json b/apps/dokploy/public/locales/zh-Hans/settings.json new file mode 100644 index 000000000..636e43c40 --- /dev/null +++ b/apps/dokploy/public/locales/zh-Hans/settings.json @@ -0,0 +1,11 @@ +{ + "settings": { + "profile": { + "title": "账户偏好" + }, + "appearance": { + "title": "外观", + "description": "自定义仪表板主题。" + } + } +} diff --git a/apps/dokploy/utils/hooks/use-locale.ts b/apps/dokploy/utils/hooks/use-locale.ts new file mode 100644 index 000000000..a07478b78 --- /dev/null +++ b/apps/dokploy/utils/hooks/use-locale.ts @@ -0,0 +1,21 @@ +import Cookies from "js-cookie"; + +const SUPPORTED_LOCALES = ["en", "zh-Hans"] as const; +type Locale = (typeof SUPPORTED_LOCALES)[number]; +type PossibleLocale = (typeof SUPPORTED_LOCALES)[number] | undefined | null; + +export default function useLocale() { + const currentLocale = Cookies.get("DOKPLOY_LOCALE") as PossibleLocale; + + console.log(currentLocale); + + const setLocale = (locale: Locale) => { + Cookies.set("DOKPLOY_LOCALE", locale); + window.location.reload(); + }; + + return { + locale: currentLocale, + setLocale, + }; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b50b18002..9dd089aa9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -253,6 +253,9 @@ importers: input-otp: specifier: ^1.2.4 version: 1.2.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + js-cookie: + specifier: ^3.0.5 + version: 3.0.5 js-yaml: specifier: 4.1.0 version: 4.1.0 @@ -356,6 +359,9 @@ importers: '@types/bcrypt': specifier: 5.0.2 version: 5.0.2 + '@types/js-cookie': + specifier: ^3.0.6 + version: 3.0.6 '@types/js-yaml': specifier: 4.0.9 version: 4.0.9 @@ -3224,6 +3230,9 @@ packages: '@types/http-errors@2.0.4': resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} + '@types/js-cookie@3.0.6': + resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==} + '@types/js-yaml@4.0.9': resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} @@ -9344,6 +9353,8 @@ snapshots: '@types/http-errors@2.0.4': {} + '@types/js-cookie@3.0.6': {} + '@types/js-yaml@4.0.9': {} '@types/json-schema@7.0.15': {}