feat(i18n): add language select into appearance tab

This commit is contained in:
JiPai 2024-11-08 02:16:15 +08:00
parent 0ca8ee17be
commit 7f0a92f224
11 changed files with 124 additions and 24 deletions

View File

@ -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<typeof appearanceFormSchema>;
@ -35,10 +47,14 @@ type AppearanceFormValues = z.infer<typeof appearanceFormSchema>;
// This can come from your database or API.
const defaultValues: Partial<AppearanceFormValues> = {
theme: "system",
language: "en",
};
export function AppearanceForm() {
const { setTheme, theme } = useTheme();
const { locale, setLocale } = useLocale();
const { t } = useTranslation("settings");
const form = useForm<AppearanceFormValues>({
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 (
<Card className="bg-transparent">
<CardHeader>
<CardTitle className="text-xl">Appearance</CardTitle>
<CardTitle className="text-xl">
{t("settings.appearance.title")}
</CardTitle>
<CardDescription>
Customize the theme of your dashboard.
{t("settings.appearance.description")}
</CardDescription>
</CardHeader>
<CardContent className="space-y-2">
@ -131,6 +151,42 @@ export function AppearanceForm() {
}}
/>
<FormField
control={form.control}
name="language"
defaultValue={form.control._defaultValues.language}
render={({ field }) => {
return (
<FormItem className="space-y-1">
<FormLabel>Language</FormLabel>
<FormDescription>
Select a language for your dashboard
</FormDescription>
<FormMessage />
<Select
onValueChange={field.onChange}
defaultValue={field.value}
value={field.value}
>
<SelectTrigger>
<SelectValue placeholder="No preset selected" />
</SelectTrigger>
<SelectContent>
{[
{ label: "English", value: "en" },
{ label: "简体中文", value: "zh-Hans" },
].map((preset) => (
<SelectItem key={preset.label} value={preset.value}>
{preset.label}
</SelectItem>
))}
</SelectContent>
</Select>
</FormItem>
);
}}
/>
<Button type="submit">Save</Button>
</form>
</Form>

View File

@ -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<Profile>({
defaultValues: {
@ -94,7 +94,7 @@ export const ProfileForm = () => {
<CardHeader className="flex flex-row gap-2 flex-wrap justify-between items-center">
<div>
<CardTitle className="text-xl">
{t("dashboard.settings.profile.title")}
{t("settings.profile.title")}
</CardTitle>
<CardDescription>
Change the details of your profile here.

View File

@ -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",

View File

@ -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"])),
},
};
}

View File

@ -78,7 +78,7 @@ export async function getServerSideProps(
return {
props: {
trpcState: helpers.dehydrate(),
...(await serverSideTranslations(locale, ["common"])),
...(await serverSideTranslations(locale, ["settings"])),
},
};
}

View File

@ -1,9 +1 @@
{
"dashboard": {
"settings": {
"profile": {
"title": "Account"
}
}
}
}
{}

View File

@ -0,0 +1,11 @@
{
"settings": {
"profile": {
"title": "Account"
},
"appearance": {
"title": "Appearance",
"description": "Customize the theme of your dashboard."
}
}
}

View File

@ -1,9 +1 @@
{
"dashboard": {
"settings": {
"profile": {
"title": "账户偏好"
}
}
}
}
{}

View File

@ -0,0 +1,11 @@
{
"settings": {
"profile": {
"title": "账户偏好"
},
"appearance": {
"title": "外观",
"description": "自定义仪表板主题。"
}
}
}

View File

@ -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,
};
}

View File

@ -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': {}