diff --git a/.circleci/config.yml b/.circleci/config.yml index 07991599a..f47a561a5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -18,8 +18,10 @@ jobs: docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN if [ "${CIRCLE_BRANCH}" == "main" ]; then TAG="latest" - else + elif [ "${CIRCLE_BRANCH}" == "canary" ]; then TAG="canary" + else + TAG="feature" fi docker build --platform linux/amd64 -t dokploy/dokploy:${TAG}-amd64 . docker push dokploy/dokploy:${TAG}-amd64 @@ -41,8 +43,10 @@ jobs: docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN if [ "${CIRCLE_BRANCH}" == "main" ]; then TAG="latest" - else + elif [ "${CIRCLE_BRANCH}" == "canary" ]; then TAG="canary" + else + TAG="feature" fi docker build --platform linux/arm64 -t dokploy/dokploy:${TAG}-arm64 . docker push dokploy/dokploy:${TAG}-arm64 @@ -72,12 +76,18 @@ jobs: dokploy/dokploy:${TAG}-amd64 \ dokploy/dokploy:${TAG}-arm64 docker manifest push dokploy/dokploy:${VERSION} - else + elif [ "${CIRCLE_BRANCH}" == "canary" ]; then TAG="canary" docker manifest create dokploy/dokploy:${TAG} \ dokploy/dokploy:${TAG}-amd64 \ dokploy/dokploy:${TAG}-arm64 docker manifest push dokploy/dokploy:${TAG} + else + TAG="feature" + docker manifest create dokploy/dokploy:${TAG} \ + dokploy/dokploy:${TAG}-amd64 \ + dokploy/dokploy:${TAG}-arm64 + docker manifest push dokploy/dokploy:${TAG} fi workflows: @@ -89,12 +99,14 @@ workflows: only: - main - canary + - pull/665 - build-arm64: filters: branches: only: - main - canary + - pull/665 - combine-manifests: requires: - build-amd64 @@ -104,3 +116,4 @@ workflows: only: - main - canary + - pull/665 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 05256d571..c9d1049c4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,10 +14,12 @@ We have a few guidelines to follow when contributing to this project: ## Commit Convention + Before you create a Pull Request, please make sure your commit message follows the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification. ### Commit Message Format + ``` [optional scope]: @@ -235,7 +237,7 @@ export function generate(schema: Schema): Template { 5. Add the logo or image of the template to `public/templates/plausible.svg` -### Recomendations +### Recommendations - Use the same name of the folder as the id of the template. - The logo should be in the public folder. diff --git a/apps/dokploy/components/dashboard/settings/appearance-form.tsx b/apps/dokploy/components/dashboard/settings/appearance-form.tsx index 52142fcd6..a10b0d051 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, }); - }, [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")} @@ -72,9 +92,9 @@ export function AppearanceForm() { render={({ field }) => { return ( - Theme + {t("settings.appearance.theme")} - Select a theme for your dashboard + {t("settings.appearance.themeDescription")} - Light + {t("settings.appearance.themes.light")} @@ -105,7 +125,7 @@ export function AppearanceForm() { dark - Dark + {t("settings.appearance.themes.dark")} @@ -121,7 +141,7 @@ export function AppearanceForm() { system - System + {t("settings.appearance.themes.system")} @@ -131,7 +151,43 @@ export function AppearanceForm() { }} /> - + { + return ( + + {t("settings.appearance.language")} + + {t("settings.appearance.languageDescription")} + + + + + ); + }} + /> + + diff --git a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx index 3c8d51bb9..e90eb5e56 100644 --- a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx +++ b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx @@ -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"; @@ -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({ defaultValues: { @@ -91,10 +93,10 @@ export const ProfileForm = () => {
- Account - - Change the details of your profile here. - + + {t("settings.profile.title")} + + {t("settings.profile.description")}
{!data?.is2FAEnabled ? : }
@@ -107,9 +109,12 @@ export const ProfileForm = () => { name="email" render={({ field }) => ( - Email + {t("settings.profile.email")} - + @@ -120,11 +125,11 @@ export const ProfileForm = () => { name="password" render={({ field }) => ( - Password + {t("settings.profile.password")} @@ -139,7 +144,7 @@ export const ProfileForm = () => { name="image" render={({ field }) => ( - Avatar + {t("settings.profile.avatar")} { @@ -177,7 +182,7 @@ export const ProfileForm = () => {
diff --git a/apps/dokploy/components/dashboard/settings/servers/actions/show-dokploy-actions.tsx b/apps/dokploy/components/dashboard/settings/servers/actions/show-dokploy-actions.tsx index 49f6772b5..3e4790739 100644 --- a/apps/dokploy/components/dashboard/settings/servers/actions/show-dokploy-actions.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/actions/show-dokploy-actions.tsx @@ -11,10 +11,12 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { api } from "@/utils/api"; +import { useTranslation } from "next-i18next"; import { toast } from "sonner"; import { ShowModalLogs } from "../../web-server/show-modal-logs"; export const ShowDokployActions = () => { + const { t } = useTranslation("settings"); const { mutateAsync: reloadServer, isLoading } = api.settings.reloadServer.useMutation(); @@ -22,11 +24,13 @@ export const ShowDokployActions = () => { - Actions + + {t("settings.server.webServer.actions")} + { }); }} > - Reload + {t("settings.server.webServer.reload")} - Watch logs + {t("settings.server.webServer.watchLogs")} diff --git a/apps/dokploy/components/dashboard/settings/servers/actions/show-storage-actions.tsx b/apps/dokploy/components/dashboard/settings/servers/actions/show-storage-actions.tsx index b3f9c3345..7163332d0 100644 --- a/apps/dokploy/components/dashboard/settings/servers/actions/show-storage-actions.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/actions/show-storage-actions.tsx @@ -11,12 +11,14 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { api } from "@/utils/api"; +import { useTranslation } from "next-i18next"; import { toast } from "sonner"; interface Props { serverId?: string; } export const ShowStorageActions = ({ serverId }: Props) => { + const { t } = useTranslation("settings"); const { mutateAsync: cleanAll, isLoading: cleanAllIsLoading } = api.settings.cleanAll.useMutation(); @@ -64,11 +66,13 @@ export const ShowStorageActions = ({ serverId }: Props) => { } variant="outline" > - Space + {t("settings.server.webServer.storage.label")} - Actions + + {t("settings.server.webServer.actions")} + { }); }} > - Clean unused images + + {t("settings.server.webServer.storage.cleanUnusedImages")} + { }); }} > - Clean unused volumes + + {t("settings.server.webServer.storage.cleanUnusedVolumes")} + { }); }} > - Clean stopped containers + + {t("settings.server.webServer.storage.cleanStoppedContainers")} + { }); }} > - Clean Docker Builder & System + + {t("settings.server.webServer.storage.cleanDockerBuilder")} + {!serverId && ( { }); }} > - Clean Monitoring + + {t("settings.server.webServer.storage.cleanMonitoring")} + )} @@ -168,7 +182,7 @@ export const ShowStorageActions = ({ serverId }: Props) => { }); }} > - Clean all + {t("settings.server.webServer.storage.cleanAll")} diff --git a/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx b/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx index a0ea3f5e8..5488712c7 100644 --- a/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx @@ -23,6 +23,7 @@ import { api } from "@/utils/api"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; +import { useTranslation } from "next-i18next"; import { EditTraefikEnv } from "../../web-server/edit-traefik-env"; import { ShowModalLogs } from "../../web-server/show-modal-logs"; @@ -30,6 +31,7 @@ interface Props { serverId?: string; } export const ShowTraefikActions = ({ serverId }: Props) => { + const { t } = useTranslation("settings"); const { mutateAsync: reloadTraefik, isLoading: reloadTraefikIsLoading } = api.settings.reloadTraefik.useMutation(); @@ -51,11 +53,13 @@ export const ShowTraefikActions = ({ serverId }: Props) => { isLoading={reloadTraefikIsLoading || toggleDashboardIsLoading} variant="outline" > - Traefik + {t("settings.server.webServer.traefik.label")} - Actions + + {t("settings.server.webServer.actions")} + { }); }} > - Reload + {t("settings.server.webServer.reload")} - Watch logs + {t("settings.server.webServer.watchLogs")} e.preventDefault()} className="w-full cursor-pointer space-x-3" > - Modify Env + {t("settings.server.webServer.traefik.modifyEnv")} diff --git a/apps/dokploy/components/dashboard/settings/web-domain.tsx b/apps/dokploy/components/dashboard/settings/web-domain.tsx index 354f11589..00f54904d 100644 --- a/apps/dokploy/components/dashboard/settings/web-domain.tsx +++ b/apps/dokploy/components/dashboard/settings/web-domain.tsx @@ -24,6 +24,7 @@ import { } from "@/components/ui/select"; 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"; @@ -49,6 +50,7 @@ const addServerDomain = z type AddServerDomain = z.infer; export const WebDomain = () => { + const { t } = useTranslation("settings"); const { data: user, refetch } = api.admin.one.useQuery(); const { mutateAsync, isLoading } = api.settings.assignDomainServer.useMutation(); @@ -89,9 +91,11 @@ export const WebDomain = () => {
- Server Domain + + {t("settings.server.domain.title")} + - Add a domain to your server application. + {t("settings.server.domain.description")} @@ -106,7 +110,9 @@ export const WebDomain = () => { render={({ field }) => { return ( - Domain + + {t("settings.server.domain.form.domain")} + { render={({ field }) => { return ( - Letsencrypt Email + + {t("settings.server.domain.form.letsEncryptEmail")} + { render={({ field }) => { return ( - Certificate + + {t("settings.server.domain.form.certificate.label")} + @@ -169,7 +189,7 @@ export const WebDomain = () => { />
diff --git a/apps/dokploy/components/dashboard/settings/web-server.tsx b/apps/dokploy/components/dashboard/settings/web-server.tsx index 188b3db8a..2ac88d9ae 100644 --- a/apps/dokploy/components/dashboard/settings/web-server.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server.tsx @@ -7,6 +7,7 @@ import { } from "@/components/ui/card"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; +import { useTranslation } from "next-i18next"; import React from "react"; import { ShowDokployActions } from "./servers/actions/show-dokploy-actions"; import { ShowStorageActions } from "./servers/actions/show-storage-actions"; @@ -18,6 +19,7 @@ interface Props { className?: string; } export const WebServer = ({ className }: Props) => { + const { t } = useTranslation("settings"); const { data } = api.admin.one.useQuery(); const { data: dokployVersion } = api.settings.getDokployVersion.useQuery(); @@ -25,8 +27,12 @@ export const WebServer = ({ className }: Props) => { return ( - Web server settings - Reload or clean the web server. + + {t("settings.server.webServer.title")} + + + {t("settings.server.webServer.description")} +
diff --git a/apps/dokploy/next-i18next.config.js b/apps/dokploy/next-i18next.config.js new file mode 100644 index 000000000..5c20bbea8 --- /dev/null +++ b/apps/dokploy/next-i18next.config.js @@ -0,0 +1,10 @@ +/** @type {import('next-i18next').UserConfig} */ +module.exports = { + i18n: { + defaultLocale: "en", + locales: ["en", "zh-Hans"], + localeDetection: false, + }, + fallbackLng: "en", + keySeparator: false, +}; diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index ebee5cac0..24a8fa47d 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -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", @@ -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", @@ -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", diff --git a/apps/dokploy/pages/_app.tsx b/apps/dokploy/pages/_app.tsx index 41b4d50bf..b5fcb1319 100644 --- a/apps/dokploy/pages/_app.tsx +++ b/apps/dokploy/pages/_app.tsx @@ -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"; @@ -27,6 +28,7 @@ const MyApp = ({ pageProps: { ...pageProps }, }: AppPropsWithLayout) => { const getLayout = Component.getLayout ?? ((page) => page); + return ( <>