Merge pull request #829 from Dokploy/refactor/enhancement-languages

refactor: improve I18N
This commit is contained in:
Mauricio Siu 2024-12-07 14:02:05 -06:00 committed by GitHub
commit 5f71a393be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 1525 additions and 1550 deletions

View File

@ -99,14 +99,14 @@ workflows:
only:
- main
- canary
- fix/build-i18n
- refactor/enhancement-languages
- build-arm64:
filters:
branches:
only:
- main
- canary
- fix/build-i18n
- refactor/enhancement-languages
- combine-manifests:
requires:
- build-amd64
@ -116,4 +116,4 @@ workflows:
only:
- main
- canary
- fix/build-i18n
- refactor/enhancement-languages

View File

@ -35,7 +35,7 @@ RUN apt-get update && apt-get install -y curl unzip apache2-utils && rm -rf /var
COPY --from=build /prod/dokploy/.next ./.next
COPY --from=build /prod/dokploy/dist ./dist
COPY --from=build /prod/dokploy/next.config.mjs ./next.config.mjs
COPY --from=build /prod/dokploy/next-i18next.config.cjs ./next-i18next.config.cjs
# COPY --from=build /prod/dokploy/next-i18next.config.cjs ./next-i18next.config.cjs
COPY --from=build /prod/dokploy/public ./public
COPY --from=build /prod/dokploy/package.json ./package.json
COPY --from=build /prod/dokploy/drizzle ./drizzle

View File

@ -26,6 +26,7 @@ if (typeof window === "undefined") {
const baseApp: ApplicationNested = {
applicationId: "",
herokuVersion: "",
applicationStatus: "done",
appName: "",
autoDeploy: true,

View File

@ -6,6 +6,7 @@ import { expect, test } from "vitest";
const baseApp: ApplicationNested = {
applicationId: "",
herokuVersion: "",
applicationStatus: "done",
appName: "",
autoDeploy: true,

View File

@ -68,7 +68,7 @@ export const ComposeActions = ({ composeId }: Props) => {
Open Terminal
</Button>
</DockerTerminalModal>
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
<span className="text-sm font-medium">Autodeploy</span>
<Switch
aria-label="Toggle italic"

View File

@ -1,94 +1,96 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import dynamic from "next/dynamic";
import { useState } from "react";
const Terminal = dynamic(
() => import("./docker-terminal").then((e) => e.DockerTerminal),
{
ssr: false,
}
() => import("./docker-terminal").then((e) => e.DockerTerminal),
{
ssr: false,
},
);
interface Props {
containerId: string;
serverId?: string;
children?: React.ReactNode;
containerId: string;
serverId?: string;
children?: React.ReactNode;
}
export const DockerTerminalModal = ({
children,
containerId,
serverId,
children,
containerId,
serverId,
}: Props) => {
const [mainDialogOpen, setMainDialogOpen] = useState(false);
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
const [mainDialogOpen, setMainDialogOpen] = useState(false);
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
const handleMainDialogOpenChange = (open: boolean) => {
if (!open) {
setConfirmDialogOpen(true);
} else {
setMainDialogOpen(true);
}
};
const handleMainDialogOpenChange = (open: boolean) => {
if (!open) {
setConfirmDialogOpen(true);
} else {
setMainDialogOpen(true);
}
};
const handleConfirm = () => {
setConfirmDialogOpen(false);
setMainDialogOpen(false);
};
const handleConfirm = () => {
setConfirmDialogOpen(false);
setMainDialogOpen(false);
};
const handleCancel = () => {
setConfirmDialogOpen(false);
};
return (
<Dialog open={mainDialogOpen} onOpenChange={handleMainDialogOpenChange}>
<DialogTrigger asChild>
<DropdownMenuItem
className="w-full cursor-pointer space-x-3"
onSelect={(e) => e.preventDefault()}
>
{children}
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl">
<DialogHeader>
<DialogTitle>Docker Terminal</DialogTitle>
<DialogDescription>
Easy way to access to docker container
</DialogDescription>
</DialogHeader>
const handleCancel = () => {
setConfirmDialogOpen(false);
};
return (
<Dialog open={mainDialogOpen} onOpenChange={handleMainDialogOpenChange}>
<DialogTrigger asChild>
<DropdownMenuItem
className="w-full cursor-pointer space-x-3"
onSelect={(e) => e.preventDefault()}
>
{children}
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl">
<DialogHeader>
<DialogTitle>Docker Terminal</DialogTitle>
<DialogDescription>
Easy way to access to docker container
</DialogDescription>
</DialogHeader>
<Terminal
id="terminal"
containerId={containerId}
serverId={serverId || ""}
/>
<Dialog open={confirmDialogOpen} onOpenChange={setConfirmDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you sure you want to close the terminal?</DialogTitle>
<DialogDescription>
By clicking the confirm button, the terminal will be closed.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={handleCancel}>
Cancel
</Button>
<Button onClick={handleConfirm}>Confirm</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</DialogContent>
</Dialog>
);
<Terminal
id="terminal"
containerId={containerId}
serverId={serverId || ""}
/>
<Dialog open={confirmDialogOpen} onOpenChange={setConfirmDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>
Are you sure you want to close the terminal?
</DialogTitle>
<DialogDescription>
By clicking the confirm button, the terminal will be closed.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={handleCancel}>
Cancel
</Button>
<Button onClick={handleConfirm}>Confirm</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</DialogContent>
</Dialog>
);
};

View File

@ -27,6 +27,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Languages } from "@/lib/languages";
import useLocale from "@/utils/hooks/use-locale";
import { useTranslation } from "next-i18next";
import { useTheme } from "next-themes";
@ -37,25 +38,9 @@ const appearanceFormSchema = z.object({
theme: z.enum(["light", "dark", "system"], {
required_error: "Please select a theme.",
}),
language: z.enum(
[
"en",
"pl",
"ru",
"fr",
"de",
"tr",
"zh-Hant",
"kz",
"zh-Hans",
"fa",
"ko",
"pt-br",
],
{
required_error: "Please select a language.",
},
),
language: z.nativeEnum(Languages, {
required_error: "Please select a language.",
}),
});
type AppearanceFormValues = z.infer<typeof appearanceFormSchema>;
@ -63,7 +48,7 @@ type AppearanceFormValues = z.infer<typeof appearanceFormSchema>;
// This can come from your database or API.
const defaultValues: Partial<AppearanceFormValues> = {
theme: "system",
language: "en",
language: Languages.English,
};
export function AppearanceForm() {
@ -188,25 +173,15 @@ export function AppearanceForm() {
<SelectValue placeholder="No preset selected" />
</SelectTrigger>
<SelectContent>
{[
{ label: "English", value: "en" },
{ label: "Polski", value: "pl" },
{ label: "Русский", value: "ru" },
{ label: "Français", value: "fr" },
{ label: "Deutsch", value: "de" },
{ label: "繁體中文", value: "zh-Hant" },
{ label: "简体中文", value: "zh-Hans" },
{ label: "Türkçe", value: "tr" },
{ label: "Қазақ", value: "tr" },
{ label: "Kazakh", value: "kz" },
{ label: "Persian", value: "fa" },
{ label: "한국어", value: "ko" },
{ label: "Português", value: "pt-br" },
].map((preset) => (
<SelectItem key={preset.label} value={preset.value}>
{preset.label}
</SelectItem>
))}
{Object.keys(Languages).map((preset) => {
const value =
Languages[preset as keyof typeof Languages];
return (
<SelectItem key={value} value={value}>
{preset}
</SelectItem>
);
})}
</SelectContent>
</Select>
</FormItem>

View File

@ -1,22 +1,22 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { Loader2 } from "lucide-react";
@ -25,118 +25,118 @@ import type React from "react";
import { useEffect, useState } from "react";
const Terminal = dynamic(
() =>
import("@/components/dashboard/docker/terminal/docker-terminal").then(
(e) => e.DockerTerminal
),
{
ssr: false,
}
() =>
import("@/components/dashboard/docker/terminal/docker-terminal").then(
(e) => e.DockerTerminal,
),
{
ssr: false,
},
);
interface Props {
appName: string;
children?: React.ReactNode;
serverId?: string;
appName: string;
children?: React.ReactNode;
serverId?: string;
}
export const DockerTerminalModal = ({ children, appName, serverId }: Props) => {
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
{
appName,
serverId,
},
{
enabled: !!appName,
}
);
const [containerId, setContainerId] = useState<string | undefined>();
const [mainDialogOpen, setMainDialogOpen] = useState(false);
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
{
appName,
serverId,
},
{
enabled: !!appName,
},
);
const [containerId, setContainerId] = useState<string | undefined>();
const [mainDialogOpen, setMainDialogOpen] = useState(false);
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
const handleMainDialogOpenChange = (open: boolean) => {
if (!open) {
setConfirmDialogOpen(true);
} else {
setMainDialogOpen(true);
}
};
const handleMainDialogOpenChange = (open: boolean) => {
if (!open) {
setConfirmDialogOpen(true);
} else {
setMainDialogOpen(true);
}
};
const handleConfirm = () => {
setConfirmDialogOpen(false);
setMainDialogOpen(false);
};
const handleConfirm = () => {
setConfirmDialogOpen(false);
setMainDialogOpen(false);
};
const handleCancel = () => {
setConfirmDialogOpen(false);
};
const handleCancel = () => {
setConfirmDialogOpen(false);
};
useEffect(() => {
if (data && data?.length > 0) {
setContainerId(data[0]?.containerId);
}
}, [data]);
useEffect(() => {
if (data && data?.length > 0) {
setContainerId(data[0]?.containerId);
}
}, [data]);
return (
<Dialog open={mainDialogOpen} onOpenChange={handleMainDialogOpenChange}>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-7xl">
<DialogHeader>
<DialogTitle>Docker Terminal</DialogTitle>
<DialogDescription>
Easy way to access to docker container
</DialogDescription>
</DialogHeader>
<Label>Select a container to view logs</Label>
<Select onValueChange={setContainerId} value={containerId}>
<SelectTrigger>
{isLoading ? (
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
<span>Loading...</span>
<Loader2 className="animate-spin size-4" />
</div>
) : (
<SelectValue placeholder="Select a container" />
)}
</SelectTrigger>
<SelectContent>
<SelectGroup>
{data?.map((container) => (
<SelectItem
key={container.containerId}
value={container.containerId}
>
{container.name} ({container.containerId}) {container.state}
</SelectItem>
))}
<SelectLabel>Containers ({data?.length})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
<Terminal
serverId={serverId || ""}
id="terminal"
containerId={containerId || "select-a-container"}
/>
<Dialog open={confirmDialogOpen} onOpenChange={setConfirmDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>
Are you sure you want to close the terminal?
</DialogTitle>
<DialogDescription>
By clicking the confirm button, the terminal will be closed.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={handleCancel}>
Cancel
</Button>
<Button onClick={handleConfirm}>Confirm</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</DialogContent>
</Dialog>
);
return (
<Dialog open={mainDialogOpen} onOpenChange={handleMainDialogOpenChange}>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-7xl">
<DialogHeader>
<DialogTitle>Docker Terminal</DialogTitle>
<DialogDescription>
Easy way to access to docker container
</DialogDescription>
</DialogHeader>
<Label>Select a container to view logs</Label>
<Select onValueChange={setContainerId} value={containerId}>
<SelectTrigger>
{isLoading ? (
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
<span>Loading...</span>
<Loader2 className="animate-spin size-4" />
</div>
) : (
<SelectValue placeholder="Select a container" />
)}
</SelectTrigger>
<SelectContent>
<SelectGroup>
{data?.map((container) => (
<SelectItem
key={container.containerId}
value={container.containerId}
>
{container.name} ({container.containerId}) {container.state}
</SelectItem>
))}
<SelectLabel>Containers ({data?.length})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
<Terminal
serverId={serverId || ""}
id="terminal"
containerId={containerId || "select-a-container"}
/>
<Dialog open={confirmDialogOpen} onOpenChange={setConfirmDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>
Are you sure you want to close the terminal?
</DialogTitle>
<DialogDescription>
By clicking the confirm button, the terminal will be closed.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={handleCancel}>
Cancel
</Button>
<Button onClick={handleConfirm}>Confirm</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</DialogContent>
</Dialog>
);
};

View File

@ -0,0 +1,16 @@
export enum Languages {
English = "en",
Polish = "pl",
Russian = "ru",
French = "fr",
German = "de",
ChineseTraditional = "zh-Hant",
ChineseSimplified = "zh-Hans",
Turkish = "tr",
Kazakh = "kz",
Persian = "fa",
Korean = "ko",
Portuguese = "pt-br",
}
export type Language = keyof typeof Languages;

View File

@ -1,23 +1,23 @@
/** @type {import('next-i18next').UserConfig} */
module.exports = {
fallbackLng: "en",
keySeparator: false,
i18n: {
defaultLocale: "en",
locales: [
"en",
"pl",
"ru",
"fr",
"de",
"tr",
"kz",
"zh-Hant",
"zh-Hans",
"fa",
"ko",
"pt-br",
],
localeDetection: false,
},
fallbackLng: "en",
keySeparator: false,
i18n: {
defaultLocale: "en",
locales: [
"en",
"pl",
"ru",
"fr",
"de",
"tr",
"kz",
"zh-Hant",
"zh-Hans",
"fa",
"ko",
"pt-br",
],
localeDetection: false,
},
};

View File

@ -1,6 +1,7 @@
import "@/styles/globals.css";
import { Toaster } from "@/components/ui/sonner";
import { Languages } from "@/lib/languages";
import { api } from "@/utils/api";
import type { NextPage } from "next";
import { appWithTranslation } from "next-i18next";
@ -71,20 +72,7 @@ export default api.withTRPC(
{
i18n: {
defaultLocale: "en",
locales: [
"en",
"pl",
"ru",
"fr",
"de",
"tr",
"kz",
"zh-Hant",
"zh-Hans",
"fa",
"ko",
"pt-br",
],
locales: Object.values(Languages),
localeDetection: false,
},
fallbackLng: "en",

View File

@ -10,142 +10,141 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { extractCommitMessage, extractHash } from "./[refreshToken]";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
req: NextApiRequest,
res: NextApiResponse,
) {
const signature = req.headers["x-hub-signature-256"];
const githubBody = req.body;
const signature = req.headers["x-hub-signature-256"];
const githubBody = req.body;
if (!githubBody?.installation?.id) {
res.status(400).json({ message: "Github Installation not found" });
return;
}
if (!githubBody?.installation?.id) {
res.status(400).json({ message: "Github Installation not found" });
return;
}
const githubResult = await db.query.github.findFirst({
where: eq(github.githubInstallationId, githubBody.installation.id),
});
const githubResult = await db.query.github.findFirst({
where: eq(github.githubInstallationId, githubBody.installation.id),
});
if (!githubResult) {
res.status(400).json({ message: "Github Installation not found" });
return;
}
if (!githubResult) {
res.status(400).json({ message: "Github Installation not found" });
return;
}
if (!githubResult.githubWebhookSecret) {
res.status(400).json({ message: "Github Webhook Secret not set" });
return;
}
const webhooks = new Webhooks({
secret: githubResult.githubWebhookSecret,
});
if (!githubResult.githubWebhookSecret) {
res.status(400).json({ message: "Github Webhook Secret not set" });
return;
}
const webhooks = new Webhooks({
secret: githubResult.githubWebhookSecret,
});
const verified = await webhooks.verify(
JSON.stringify(githubBody),
signature as string
);
const verified = await webhooks.verify(
JSON.stringify(githubBody),
signature as string,
);
if (!verified) {
res.status(401).json({ message: "Unauthorized" });
return;
}
if (!verified) {
res.status(401).json({ message: "Unauthorized" });
return;
}
if (req.headers["x-github-event"] === "ping") {
res.status(200).json({ message: "Ping received, webhook is active" });
return;
}
if (req.headers["x-github-event"] === "ping") {
res.status(200).json({ message: "Ping received, webhook is active" });
return;
}
if (req.headers["x-github-event"] !== "push") {
res.status(400).json({ message: "We only accept push events" });
return;
}
if (req.headers["x-github-event"] !== "push") {
res.status(400).json({ message: "We only accept push events" });
return;
}
try {
const branchName = githubBody?.ref?.replace("refs/heads/", "");
const repository = githubBody?.repository?.name;
const deploymentTitle = extractCommitMessage(req.headers, req.body);
const deploymentHash = extractHash(req.headers, req.body);
const owner = githubBody?.repository?.owner?.name;
try {
const branchName = githubBody?.ref?.replace("refs/heads/", "");
const repository = githubBody?.repository?.name;
const deploymentTitle = extractCommitMessage(req.headers, req.body);
const deploymentHash = extractHash(req.headers, req.body);
const owner = githubBody?.repository?.owner?.name;
const apps = await db.query.applications.findMany({
where: and(
eq(applications.sourceType, "github"),
eq(applications.autoDeploy, true),
eq(applications.branch, branchName),
eq(applications.repository, repository),
eq(applications.owner, owner),
),
});
const apps = await db.query.applications.findMany({
where: and(
eq(applications.sourceType, "github"),
eq(applications.autoDeploy, true),
eq(applications.branch, branchName),
eq(applications.repository, repository),
eq(applications.owner, owner)
),
});
for (const app of apps) {
const jobData: DeploymentJob = {
applicationId: app.applicationId as string,
titleLog: deploymentTitle,
descriptionLog: `Hash: ${deploymentHash}`,
type: "deploy",
applicationType: "application",
server: !!app.serverId,
};
for (const app of apps) {
const jobData: DeploymentJob = {
applicationId: app.applicationId as string,
titleLog: deploymentTitle,
descriptionLog: `Hash: ${deploymentHash}`,
type: "deploy",
applicationType: "application",
server: !!app.serverId,
};
if (IS_CLOUD && app.serverId) {
jobData.serverId = app.serverId;
await deploy(jobData);
return true;
}
await myQueue.add(
"deployments",
{ ...jobData },
{
removeOnComplete: true,
removeOnFail: true,
},
);
}
if (IS_CLOUD && app.serverId) {
jobData.serverId = app.serverId;
await deploy(jobData);
return true;
}
await myQueue.add(
"deployments",
{ ...jobData },
{
removeOnComplete: true,
removeOnFail: true,
}
);
}
const composeApps = await db.query.compose.findMany({
where: and(
eq(compose.sourceType, "github"),
eq(compose.autoDeploy, true),
eq(compose.branch, branchName),
eq(compose.repository, repository),
eq(compose.owner, owner),
),
});
const composeApps = await db.query.compose.findMany({
where: and(
eq(compose.sourceType, "github"),
eq(compose.autoDeploy, true),
eq(compose.branch, branchName),
eq(compose.repository, repository),
eq(compose.owner, owner)
),
});
for (const composeApp of composeApps) {
const jobData: DeploymentJob = {
composeId: composeApp.composeId as string,
titleLog: deploymentTitle,
type: "deploy",
applicationType: "compose",
descriptionLog: `Hash: ${deploymentHash}`,
server: !!composeApp.serverId,
};
for (const composeApp of composeApps) {
const jobData: DeploymentJob = {
composeId: composeApp.composeId as string,
titleLog: deploymentTitle,
type: "deploy",
applicationType: "compose",
descriptionLog: `Hash: ${deploymentHash}`,
server: !!composeApp.serverId,
};
if (IS_CLOUD && composeApp.serverId) {
jobData.serverId = composeApp.serverId;
await deploy(jobData);
return true;
}
if (IS_CLOUD && composeApp.serverId) {
jobData.serverId = composeApp.serverId;
await deploy(jobData);
return true;
}
await myQueue.add(
"deployments",
{ ...jobData },
{
removeOnComplete: true,
removeOnFail: true,
},
);
}
await myQueue.add(
"deployments",
{ ...jobData },
{
removeOnComplete: true,
removeOnFail: true,
}
);
}
const totalApps = apps.length + composeApps.length;
const emptyApps = totalApps === 0;
const totalApps = apps.length + composeApps.length;
const emptyApps = totalApps === 0;
if (emptyApps) {
res.status(200).json({ message: "No apps to deploy" });
return;
}
res.status(200).json({ message: `Deployed ${totalApps} apps` });
} catch (error) {
res.status(400).json({ message: "Error To Deploy Application", error });
}
if (emptyApps) {
res.status(200).json({ message: "No apps to deploy" });
return;
}
res.status(200).json({ message: `Deployed ${totalApps} apps` });
} catch (error) {
res.status(400).json({ message: "Error To Deploy Application", error });
}
}

View File

@ -1,44 +1,44 @@
{
"settings.common.save": "저장",
"settings.server.domain.title": "서버 도메인",
"settings.server.domain.description": "서버 애플리케이션에 도메인을 추가합니다.",
"settings.server.domain.form.domain": "도메인",
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt 이메일",
"settings.server.domain.form.certificate.label": "인증서",
"settings.server.domain.form.certificate.placeholder": "인증서 선택",
"settings.server.domain.form.certificateOptions.none": "없음",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (기본)",
"settings.common.save": "저장",
"settings.server.domain.title": "서버 도메인",
"settings.server.domain.description": "서버 애플리케이션에 도메인을 추가합니다.",
"settings.server.domain.form.domain": "도메인",
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt 이메일",
"settings.server.domain.form.certificate.label": "인증서",
"settings.server.domain.form.certificate.placeholder": "인증서 선택",
"settings.server.domain.form.certificateOptions.none": "없음",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (기본)",
"settings.server.webServer.title": "웹 서버",
"settings.server.webServer.description": "웹 서버를 재시작하거나 정리합니다.",
"settings.server.webServer.actions": "작업",
"settings.server.webServer.reload": "재시작",
"settings.server.webServer.watchLogs": "로그 보기",
"settings.server.webServer.updateServerIp": "서버 IP 갱신",
"settings.server.webServer.server.label": "서버",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "환경 변수 수정",
"settings.server.webServer.storage.label": "저장 공간",
"settings.server.webServer.storage.cleanUnusedImages": "사용하지 않는 이미지 정리",
"settings.server.webServer.storage.cleanUnusedVolumes": "사용하지 않는 볼륨 정리",
"settings.server.webServer.storage.cleanStoppedContainers": "정지된 컨테이너 정리",
"settings.server.webServer.storage.cleanDockerBuilder": "도커 빌더 & 시스템 정리",
"settings.server.webServer.storage.cleanMonitoring": "모니터링 데이터 정리",
"settings.server.webServer.storage.cleanAll": "전체 정리",
"settings.server.webServer.title": "웹 서버",
"settings.server.webServer.description": "웹 서버를 재시작하거나 정리합니다.",
"settings.server.webServer.actions": "작업",
"settings.server.webServer.reload": "재시작",
"settings.server.webServer.watchLogs": "로그 보기",
"settings.server.webServer.updateServerIp": "서버 IP 갱신",
"settings.server.webServer.server.label": "서버",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "환경 변수 수정",
"settings.server.webServer.storage.label": "저장 공간",
"settings.server.webServer.storage.cleanUnusedImages": "사용하지 않는 이미지 정리",
"settings.server.webServer.storage.cleanUnusedVolumes": "사용하지 않는 볼륨 정리",
"settings.server.webServer.storage.cleanStoppedContainers": "정지된 컨테이너 정리",
"settings.server.webServer.storage.cleanDockerBuilder": "도커 빌더 & 시스템 정리",
"settings.server.webServer.storage.cleanMonitoring": "모니터링 데이터 정리",
"settings.server.webServer.storage.cleanAll": "전체 정리",
"settings.profile.title": "계정",
"settings.profile.description": "여기에서 프로필 세부 정보를 변경하세요.",
"settings.profile.email": "이메일",
"settings.profile.password": "비밀번호",
"settings.profile.avatar": "아바타",
"settings.profile.title": "계정",
"settings.profile.description": "여기에서 프로필 세부 정보를 변경하세요.",
"settings.profile.email": "이메일",
"settings.profile.password": "비밀번호",
"settings.profile.avatar": "아바타",
"settings.appearance.title": "외관",
"settings.appearance.description": "대시보드의 테마를 사용자 설정합니다.",
"settings.appearance.theme": "테마",
"settings.appearance.themeDescription": "대시보드 테마 선택",
"settings.appearance.themes.light": "라이트",
"settings.appearance.themes.dark": "다크",
"settings.appearance.themes.system": "시스템",
"settings.appearance.language": "언어",
"settings.appearance.languageDescription": "대시보드에서 사용할 언어 선택"
"settings.appearance.title": "외관",
"settings.appearance.description": "대시보드의 테마를 사용자 설정합니다.",
"settings.appearance.theme": "테마",
"settings.appearance.themeDescription": "대시보드 테마 선택",
"settings.appearance.themes.light": "라이트",
"settings.appearance.themes.dark": "다크",
"settings.appearance.themes.system": "시스템",
"settings.appearance.language": "언어",
"settings.appearance.languageDescription": "대시보드에서 사용할 언어 선택"
}

View File

@ -1 +1 @@
{}
{}

View File

@ -1,41 +1,41 @@
{
"settings.common.save": "Сақтау",
"settings.server.domain.title": "Сервер домені",
"settings.server.domain.description": "Dokploy сервер қолданбасына домен енгізіңіз.",
"settings.server.domain.form.domain": "Домен",
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt Эл. поштасы",
"settings.server.domain.form.certificate.label": "Сертификат",
"settings.server.domain.form.certificate.placeholder": "Сертификатты таңдаңыз",
"settings.server.domain.form.certificateOptions.none": "Жоқ",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Стандартты)",
"settings.server.webServer.title": "Веб-Сервер",
"settings.server.webServer.description": "Веб-серверді қайта жүктеу немесе тазалау.",
"settings.server.webServer.actions": "Әрекеттер",
"settings.server.webServer.reload": "Қайта жүктеу",
"settings.server.webServer.watchLogs": "Журналдарды қарау",
"settings.server.webServer.updateServerIp": "Сервердің IP жаңарту",
"settings.server.webServer.server.label": "Сервер",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Env Өзгерту",
"settings.server.webServer.storage.label": "Диск кеңістігі",
"settings.server.webServer.storage.cleanUnusedImages": "Пайдаланылмаған образды тазалау",
"settings.server.webServer.storage.cleanUnusedVolumes": "Пайдаланылмаған томды тазалау",
"settings.server.webServer.storage.cleanStoppedContainers": "Тоқтатылған контейнерлерді тазалау",
"settings.server.webServer.storage.cleanDockerBuilder": "Docker Builder & Системаны тазалау",
"settings.server.webServer.storage.cleanMonitoring": "Мониторингті тазалау",
"settings.server.webServer.storage.cleanAll": "Барлығын тазалау",
"settings.profile.title": "Аккаунт",
"settings.profile.description": "Профиль мәліметтерін осы жерден өзгертіңіз.",
"settings.profile.email": "Эл. пошта",
"settings.profile.password": "Құпия сөз",
"settings.profile.avatar": "Аватар",
"settings.appearance.title": "Сыртқы түрі",
"settings.appearance.description": "Dokploy сыртқы келбетін өзгерту.",
"settings.appearance.theme": "Келбеті",
"settings.appearance.themeDescription": "Жүйе тақтасының келбетің таңдаңыз",
"settings.appearance.themes.light": "Жарық",
"settings.appearance.themes.dark": "Қараңғы",
"settings.appearance.themes.system": "Жүйелік",
"settings.appearance.language": "Тіл",
"settings.appearance.languageDescription": "Жүйе тақтасының тілің таңдаңыз"
"settings.common.save": "Сақтау",
"settings.server.domain.title": "Сервер домені",
"settings.server.domain.description": "Dokploy сервер қолданбасына домен енгізіңіз.",
"settings.server.domain.form.domain": "Домен",
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt Эл. поштасы",
"settings.server.domain.form.certificate.label": "Сертификат",
"settings.server.domain.form.certificate.placeholder": "Сертификатты таңдаңыз",
"settings.server.domain.form.certificateOptions.none": "Жоқ",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Стандартты)",
"settings.server.webServer.title": "Веб-Сервер",
"settings.server.webServer.description": "Веб-серверді қайта жүктеу немесе тазалау.",
"settings.server.webServer.actions": "Әрекеттер",
"settings.server.webServer.reload": "Қайта жүктеу",
"settings.server.webServer.watchLogs": "Журналдарды қарау",
"settings.server.webServer.updateServerIp": "Сервердің IP жаңарту",
"settings.server.webServer.server.label": "Сервер",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Env Өзгерту",
"settings.server.webServer.storage.label": "Диск кеңістігі",
"settings.server.webServer.storage.cleanUnusedImages": "Пайдаланылмаған образды тазалау",
"settings.server.webServer.storage.cleanUnusedVolumes": "Пайдаланылмаған томды тазалау",
"settings.server.webServer.storage.cleanStoppedContainers": "Тоқтатылған контейнерлерді тазалау",
"settings.server.webServer.storage.cleanDockerBuilder": "Docker Builder & Системаны тазалау",
"settings.server.webServer.storage.cleanMonitoring": "Мониторингті тазалау",
"settings.server.webServer.storage.cleanAll": "Барлығын тазалау",
"settings.profile.title": "Аккаунт",
"settings.profile.description": "Профиль мәліметтерін осы жерден өзгертіңіз.",
"settings.profile.email": "Эл. пошта",
"settings.profile.password": "Құпия сөз",
"settings.profile.avatar": "Аватар",
"settings.appearance.title": "Сыртқы түрі",
"settings.appearance.description": "Dokploy сыртқы келбетін өзгерту.",
"settings.appearance.theme": "Келбеті",
"settings.appearance.themeDescription": "Жүйе тақтасының келбетің таңдаңыз",
"settings.appearance.themes.light": "Жарық",
"settings.appearance.themes.dark": "Қараңғы",
"settings.appearance.themes.system": "Жүйелік",
"settings.appearance.language": "Тіл",
"settings.appearance.languageDescription": "Жүйе тақтасының тілің таңдаңыз"
}

View File

@ -31,8 +31,8 @@
"settings.profile.email": "Email",
"settings.profile.password": "Senha",
"settings.profile.avatar": "Avatar",
"settings.appearance.title": "Aparência",
"settings.appearance.title": "Aparencia",
"settings.appearance.description": "Personalize o tema do seu dashboard.",
"settings.appearance.theme": "Tema",
"settings.appearance.themeDescription": "Selecione um tema para o dashboard",

View File

@ -190,7 +190,7 @@ export const notificationRouter = createTRPCRouter({
await sendDiscordNotification(input, {
title: "> `🤚` - Test Notification",
description: "> Hi, From Dokploy 👋",
color: 0xf3f7f4
color: 0xf3f7f4,
});
return true;
} catch (error) {

File diff suppressed because it is too large Load Diff

View File

@ -1,93 +1,93 @@
import { Secrets } from "@/components/ui/secrets";
import {
type DomainSchema,
type Schema,
type Template,
generateBase64,
generateRandomDomain,
type DomainSchema,
type Schema,
type Template,
generateBase64,
generateRandomDomain,
} from "../utils";
export function generate(schema: Schema): Template {
const triggerDomain = generateRandomDomain(schema);
const triggerDomain = generateRandomDomain(schema);
const magicLinkSecret = generateBase64(16);
const sessionSecret = generateBase64(16);
const encryptionKey = generateBase64(32);
const providerSecret = generateBase64(32);
const coordinatorSecret = generateBase64(32);
const magicLinkSecret = generateBase64(16);
const sessionSecret = generateBase64(16);
const encryptionKey = generateBase64(32);
const providerSecret = generateBase64(32);
const coordinatorSecret = generateBase64(32);
const dbPassword = generateBase64(24);
const dbUser = "triggeruser";
const dbName = "triggerdb";
const dbPassword = generateBase64(24);
const dbUser = "triggeruser";
const dbName = "triggerdb";
const domains: DomainSchema[] = [
{
host: triggerDomain,
port: 3000,
serviceName: "webapp",
},
];
const domains: DomainSchema[] = [
{
host: triggerDomain,
port: 3000,
serviceName: "webapp",
},
];
const envs = [
`NODE_ENV=production`,
`RUNTIME_PLATFORM=docker-compose`,
`V3_ENABLED=true`,
const envs = [
"NODE_ENV=production",
"RUNTIME_PLATFORM=docker-compose",
"V3_ENABLED=true",
`# Domain configuration`,
`TRIGGER_DOMAIN=${triggerDomain}`,
`TRIGGER_PROTOCOL=http`,
"# Domain configuration",
`TRIGGER_DOMAIN=${triggerDomain}`,
"TRIGGER_PROTOCOL=http",
`# Database configuration with secure credentials`,
`POSTGRES_USER=${dbUser}`,
`POSTGRES_PASSWORD=${dbPassword}`,
`POSTGRES_DB=${dbName}`,
`DATABASE_URL=postgresql://${dbUser}:${dbPassword}@postgres:5432/${dbName}`,
"# Database configuration with secure credentials",
`POSTGRES_USER=${dbUser}`,
`POSTGRES_PASSWORD=${dbPassword}`,
`POSTGRES_DB=${dbName}`,
`DATABASE_URL=postgresql://${dbUser}:${dbPassword}@postgres:5432/${dbName}`,
`# Secrets`,
`MAGIC_LINK_SECRET=${magicLinkSecret}`,
`SESSION_SECRET=${sessionSecret}`,
`ENCRYPTION_KEY=${encryptionKey}`,
`PROVIDER_SECRET=${providerSecret}`,
`COORDINATOR_SECRET=${coordinatorSecret}`,
"# Secrets",
`MAGIC_LINK_SECRET=${magicLinkSecret}`,
`SESSION_SECRET=${sessionSecret}`,
`ENCRYPTION_KEY=${encryptionKey}`,
`PROVIDER_SECRET=${providerSecret}`,
`COORDINATOR_SECRET=${coordinatorSecret}`,
`# TRIGGER_TELEMETRY_DISABLED=1`,
`INTERNAL_OTEL_TRACE_DISABLED=1`,
`INTERNAL_OTEL_TRACE_LOGGING_ENABLED=0`,
"# TRIGGER_TELEMETRY_DISABLED=1",
"INTERNAL_OTEL_TRACE_DISABLED=1",
"INTERNAL_OTEL_TRACE_LOGGING_ENABLED=0",
`DEFAULT_ORG_EXECUTION_CONCURRENCY_LIMIT=300`,
`DEFAULT_ENV_EXECUTION_CONCURRENCY_LIMIT=100`,
"DEFAULT_ORG_EXECUTION_CONCURRENCY_LIMIT=300",
"DEFAULT_ENV_EXECUTION_CONCURRENCY_LIMIT=100",
`DIRECT_URL=\${DATABASE_URL}`,
`REDIS_HOST=redis`,
`REDIS_PORT=6379`,
`REDIS_TLS_DISABLED=true`,
"DIRECT_URL=${DATABASE_URL}",
"REDIS_HOST=redis",
"REDIS_PORT=6379",
"REDIS_TLS_DISABLED=true",
`# If this is set, emails that are not specified won't be able to log in`,
`# WHITELISTED_EMAILS="authorized@yahoo.com|authorized@gmail.com"`,
`# Accounts with these emails will become admins when signing up and get access to the admin panel`,
`# ADMIN_EMAILS="admin@example.com|another-admin@example.com"`,
"# If this is set, emails that are not specified won't be able to log in",
'# WHITELISTED_EMAILS="authorized@yahoo.com|authorized@gmail.com"',
"# Accounts with these emails will become admins when signing up and get access to the admin panel",
'# ADMIN_EMAILS="admin@example.com|another-admin@example.com"',
`# If this is set, your users will be able to log in via GitHub`,
`# AUTH_GITHUB_CLIENT_ID=`,
`# AUTH_GITHUB_CLIENT_SECRET=`,
"# If this is set, your users will be able to log in via GitHub",
"# AUTH_GITHUB_CLIENT_ID=",
"# AUTH_GITHUB_CLIENT_SECRET=",
`# E-mail settings`,
`# Ensure the FROM_EMAIL matches what you setup with Resend.com`,
`# If these are not set, emails will be printed to the console`,
`# FROM_EMAIL=`,
`# REPLY_TO_EMAIL=`,
`# RESEND_API_KEY=`,
"# E-mail settings",
"# Ensure the FROM_EMAIL matches what you setup with Resend.com",
"# If these are not set, emails will be printed to the console",
"# FROM_EMAIL=",
"# REPLY_TO_EMAIL=",
"# RESEND_API_KEY=",
`# Worker settings`,
`HTTP_SERVER_PORT=9020`,
`COORDINATOR_HOST=127.0.0.1`,
`COORDINATOR_PORT=\${HTTP_SERVER_PORT}`,
`# REGISTRY_HOST=\${DEPLOY_REGISTRY_HOST}`,
`# REGISTRY_NAMESPACE=\${DEPLOY_REGISTRY_NAMESPACE}`,
];
"# Worker settings",
"HTTP_SERVER_PORT=9020",
"COORDINATOR_HOST=127.0.0.1",
"COORDINATOR_PORT=${HTTP_SERVER_PORT}",
"# REGISTRY_HOST=${DEPLOY_REGISTRY_HOST}",
"# REGISTRY_NAMESPACE=${DEPLOY_REGISTRY_NAMESPACE}",
];
return {
envs,
domains,
};
return {
envs,
domains,
};
}

View File

@ -1,26 +1,10 @@
import type { Languages } from "@/lib/languages";
import Cookies from "js-cookie";
const SUPPORTED_LOCALES = [
"en",
"pl",
"ru",
"fr",
"de",
"tr",
"kz",
"zh-Hant",
"zh-Hans",
"fa",
"ko",
"pt-br",
] as const;
type Locale = (typeof SUPPORTED_LOCALES)[number];
export default function useLocale() {
const currentLocale = (Cookies.get("DOKPLOY_LOCALE") ?? "en") as Locale;
const currentLocale = (Cookies.get("DOKPLOY_LOCALE") ?? "en") as Languages;
const setLocale = (locale: Locale) => {
const setLocale = (locale: Languages) => {
Cookies.set("DOKPLOY_LOCALE", locale, { expires: 365 });
window.location.reload();
};

View File

@ -5,11 +5,19 @@ export function getLocale(cookies: NextApiRequestCookies) {
return locale;
}
// libs/i18n.js
import { Languages } from "@/lib/languages";
import { serverSideTranslations as originalServerSideTranslations } from "next-i18next/serverSideTranslations";
import nextI18NextConfig from "../next-i18next.config.cjs";
export const serverSideTranslations = (
locale: string,
namespaces = ["common"],
) => originalServerSideTranslations(locale, namespaces, nextI18NextConfig);
) =>
originalServerSideTranslations(locale, namespaces, {
fallbackLng: "en",
keySeparator: false,
i18n: {
defaultLocale: "en",
locales: Object.values(Languages),
localeDetection: false,
},
});

View File

@ -95,7 +95,9 @@ export const sendDatabaseBackupNotifications = async ({
},
{
name: "`❓`・Type",
value: type.replace("error", "Failed").replace("success", "Successful"),
value: type
.replace("error", "Failed")
.replace("success", "Successful"),
inline: true,
},
...(type === "error" && errorMessage

View File

@ -48,7 +48,6 @@ export const sendDockerCleanupNotifications = async (
title: "> `✅` - Docker Cleanup",
color: 0x57f287,
fields: [
{
name: "`📅`・Date",
value: date.toLocaleDateString(),