Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(i18n): add i18n support #665

Draft
wants to merge 3 commits into
base: canary
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 65 additions & 9 deletions apps/dokploy/components/dashboard/settings/appearance-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -28,17 +37,24 @@ 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>;

// 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,
Expand All @@ -47,19 +63,23 @@ export function AppearanceForm() {
useEffect(() => {
form.reset({
theme: (theme ?? "system") as AppearanceFormValues["theme"],
language: locale,
});
}, [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">
Expand All @@ -72,9 +92,9 @@ export function AppearanceForm() {
render={({ field }) => {
return (
<FormItem className="space-y-1 ">
<FormLabel>Theme</FormLabel>
<FormLabel>{t("settings.appearance.theme")}</FormLabel>
<FormDescription>
Select a theme for your dashboard
{t("settings.appearance.themeDescription")}
</FormDescription>
<FormMessage />
<RadioGroup
Expand All @@ -92,7 +112,7 @@ export function AppearanceForm() {
<img src="/images/theme-light.svg" alt="light" />
</div>
<span className="block w-full p-2 text-center font-normal">
Light
{t("settings.appearance.themes.light")}
</span>
</FormLabel>
</FormItem>
Expand All @@ -105,7 +125,7 @@ export function AppearanceForm() {
<img src="/images/theme-dark.svg" alt="dark" />
</div>
<span className="block w-full p-2 text-center font-normal">
Dark
{t("settings.appearance.themes.dark")}
</span>
</FormLabel>
</FormItem>
Expand All @@ -121,7 +141,7 @@ export function AppearanceForm() {
<img src="/images/theme-system.svg" alt="system" />
</div>
<span className="block w-full p-2 text-center font-normal">
System
{t("settings.appearance.themes.system")}
</span>
</FormLabel>
</FormItem>
Expand All @@ -131,7 +151,43 @@ export function AppearanceForm() {
}}
/>

<Button type="submit">Save</Button>
<FormField
control={form.control}
name="language"
defaultValue={form.control._defaultValues.language}
render={({ field }) => {
return (
<FormItem className="space-y-1">
<FormLabel>{t("settings.appearance.language")}</FormLabel>
<FormDescription>
{t("settings.appearance.languageDescription")}
</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">{t("settings.common.save")}</Button>
</form>
</Form>
</CardContent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { Input } from "@/components/ui/input";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslation } from "next-i18next";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
Expand Down Expand Up @@ -51,6 +52,7 @@ const randomImages = [
export const ProfileForm = () => {
const { data, refetch } = api.auth.get.useQuery();
const { mutateAsync, isLoading } = api.auth.update.useMutation();
const { t } = useTranslation("settings");

const form = useForm<Profile>({
defaultValues: {
Expand Down Expand Up @@ -91,10 +93,10 @@ export const ProfileForm = () => {
<Card className="bg-transparent">
<CardHeader className="flex flex-row gap-2 flex-wrap justify-between items-center">
<div>
<CardTitle className="text-xl">Account</CardTitle>
<CardDescription>
Change the details of your profile here.
</CardDescription>
<CardTitle className="text-xl">
{t("settings.profile.title")}
</CardTitle>
<CardDescription>{t("settings.profile.description")}</CardDescription>
</div>
{!data?.is2FAEnabled ? <Enable2FA /> : <Disable2FA />}
</CardHeader>
Expand All @@ -107,9 +109,12 @@ export const ProfileForm = () => {
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormLabel>{t("settings.profile.email")}</FormLabel>
<FormControl>
<Input placeholder="Email" {...field} />
<Input
placeholder={t("settings.profile.email")}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
Expand All @@ -120,11 +125,11 @@ export const ProfileForm = () => {
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormLabel>{t("settings.profile.password")}</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Password"
placeholder={t("settings.profile.password")}
{...field}
value={field.value || ""}
/>
Expand All @@ -139,7 +144,7 @@ export const ProfileForm = () => {
name="image"
render={({ field }) => (
<FormItem>
<FormLabel>Avatar</FormLabel>
<FormLabel>{t("settings.profile.avatar")}</FormLabel>
<FormControl>
<RadioGroup
onValueChange={(e) => {
Expand Down Expand Up @@ -177,7 +182,7 @@ export const ProfileForm = () => {
</div>
<div>
<Button type="submit" isLoading={isLoading}>
Save
{t("settings.common.save")}
</Button>
</div>
</form>
Expand Down
9 changes: 9 additions & 0 deletions apps/dokploy/next-i18next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/** @type {import('next-i18next').UserConfig} */
module.exports = {
i18n: {
defaultLocale: "en",
locales: ["en", "zh-Hans"],
localeDetection: false,
},
fallbackLng: "en",
};
5 changes: 5 additions & 0 deletions apps/dokploy/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,16 @@
"dotenv": "16.4.5",
"drizzle-orm": "^0.30.8",
"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",
"lucide-react": "^0.312.0",
"nanoid": "3",
"next": "^15.0.1",
"next-i18next": "^15.3.1",
"next-themes": "^0.2.1",
"node-pty": "1.0.0",
"node-schedule": "2.1.1",
Expand All @@ -100,6 +103,7 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.49.3",
"react-i18next": "^15.1.0",
"recharts": "^2.12.7",
"slugify": "^1.6.6",
"sonner": "^1.4.0",
Expand All @@ -119,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",
Expand Down
4 changes: 3 additions & 1 deletion apps/dokploy/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import "@/styles/globals.css";
import { Toaster } from "@/components/ui/sonner";
import { api } from "@/utils/api";
import type { NextPage } from "next";
import { appWithTranslation } from "next-i18next";
import { ThemeProvider } from "next-themes";
import type { AppProps } from "next/app";
import { Inter } from "next/font/google";
Expand All @@ -27,6 +28,7 @@ const MyApp = ({
pageProps: { ...pageProps },
}: AppPropsWithLayout) => {
const getLayout = Component.getLayout ?? ((page) => page);

return (
<>
<style jsx global>{`
Expand Down Expand Up @@ -59,4 +61,4 @@ const MyApp = ({
);
};

export default api.withTRPC(MyApp);
export default api.withTRPC(appWithTranslation(MyApp));
4 changes: 4 additions & 0 deletions apps/dokploy/pages/dashboard/settings/appearance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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,
Expand Down Expand Up @@ -63,6 +66,7 @@ export async function getServerSideProps(
return {
props: {
trpcState: helpers.dehydrate(),
...(await serverSideTranslations(locale, ["settings"])),
},
};
}
4 changes: 4 additions & 0 deletions apps/dokploy/pages/dashboard/settings/profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
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";

Expand Down Expand Up @@ -41,6 +43,7 @@ export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { req, res } = ctx;
const locale = getLocale(req.cookies);
const { user, session } = await validateRequest(req, res);

const helpers = createServerSideHelpers({
Expand Down Expand Up @@ -75,6 +78,7 @@ export async function getServerSideProps(
return {
props: {
trpcState: helpers.dehydrate(),
...(await serverSideTranslations(locale, ["settings"])),
},
};
}
1 change: 1 addition & 0 deletions apps/dokploy/public/locales/en/common.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
27 changes: 27 additions & 0 deletions apps/dokploy/public/locales/en/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"settings": {
"common": {
"save": "Save"
},
"profile": {
"title": "Account",
"description": "Change the details of your profile here.",
"email": "Email",
"password": "Password",
"avatar": "Avatar"
},
"appearance": {
"title": "Appearance",
"description": "Customize the theme of your dashboard.",
"theme": "Theme",
"themeDescription": "Select a theme for your dashboard",
"themes": {
"light": "Light",
"dark": "Dark",
"system": "System"
},
"language": "Language",
"languageDescription": "Select a language for your dashboard"
}
}
}
1 change: 1 addition & 0 deletions apps/dokploy/public/locales/zh-Hans/common.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
Loading