feat(i18n): add i18n support for sidebar

This commit is contained in:
JiPai
2025-03-09 16:53:19 +08:00
parent 3a0dbc26d1
commit 7903ddba89
17 changed files with 229 additions and 78 deletions

View File

@@ -86,6 +86,7 @@ import { Logo } from "../shared/logo";
import { Button } from "../ui/button";
import { UpdateServerButton } from "./update-server";
import { UserNav } from "./user-nav";
import { useTranslation } from "next-i18next";
// The types of the queries we are going to use
type AuthQueryOutput = inferRouterOutputs<AppRouter>["user"]["get"];
@@ -93,6 +94,7 @@ type AuthQueryOutput = inferRouterOutputs<AppRouter>["user"]["get"];
type SingleNavItem = {
isSingle?: true;
title: string;
titleKey: string;
url: string;
icon?: LucideIcon;
isEnabled?: (opts: {
@@ -110,6 +112,7 @@ type NavItem =
| {
isSingle: false;
title: string;
titleKey: string;
icon: LucideIcon;
items: SingleNavItem[];
isEnabled?: (opts: {
@@ -122,6 +125,7 @@ type NavItem =
// Represents an external link item (used for the help section)
type ExternalLink = {
name: string;
nameKey: string;
url: string;
icon: React.ComponentType<{ className?: string }>;
isEnabled?: (opts: {
@@ -147,12 +151,14 @@ const MENU: Menu = {
{
isSingle: true,
title: "Projects",
titleKey: "common.side.projects",
url: "/dashboard/projects",
icon: Folder,
},
{
isSingle: true,
title: "Monitoring",
titleKey: "common.side.monitoring",
url: "/dashboard/monitoring",
icon: BarChartHorizontalBigIcon,
// Only enabled in non-cloud environments
@@ -161,6 +167,7 @@ const MENU: Menu = {
{
isSingle: true,
title: "Traefik File System",
titleKey: "common.side.traefik",
url: "/dashboard/traefik",
icon: GalleryVerticalEnd,
// Only enabled for admins and users with access to Traefik files in non-cloud environments
@@ -173,6 +180,7 @@ const MENU: Menu = {
{
isSingle: true,
title: "Docker",
titleKey: "common.side.docker",
url: "/dashboard/docker",
icon: BlocksIcon,
// Only enabled for admins and users with access to Docker in non-cloud environments
@@ -182,6 +190,7 @@ const MENU: Menu = {
{
isSingle: true,
title: "Swarm",
titleKey: "common.side.swarm",
url: "/dashboard/swarm",
icon: PieChart,
// Only enabled for admins and users with access to Docker in non-cloud environments
@@ -191,6 +200,7 @@ const MENU: Menu = {
{
isSingle: true,
title: "Requests",
titleKey: "common.side.requests",
url: "/dashboard/requests",
icon: Forward,
// Only enabled for admins and users with access to Docker in non-cloud environments
@@ -259,6 +269,7 @@ const MENU: Menu = {
{
isSingle: true,
title: "Web Server",
titleKey: "common.side.web-server",
url: "/dashboard/settings/server",
icon: Activity,
// Only enabled for admins in non-cloud environments
@@ -267,12 +278,14 @@ const MENU: Menu = {
{
isSingle: true,
title: "Profile",
titleKey: "common.side.profile",
url: "/dashboard/settings/profile",
icon: User,
},
{
isSingle: true,
title: "Remote Servers",
titleKey: "common.side.remote-servers",
url: "/dashboard/settings/servers",
icon: Server,
// Only enabled for admins
@@ -281,6 +294,7 @@ const MENU: Menu = {
{
isSingle: true,
title: "Users",
titleKey: "common.side.users",
icon: Users,
url: "/dashboard/settings/users",
// Only enabled for admins
@@ -289,6 +303,7 @@ const MENU: Menu = {
{
isSingle: true,
title: "SSH Keys",
titleKey: "common.side.ssh-keys",
icon: KeyRound,
url: "/dashboard/settings/ssh-keys",
// Only enabled for admins and users with access to SSH keys
@@ -297,6 +312,7 @@ const MENU: Menu = {
},
{
title: "AI",
titleKey: "common.side.ai",
icon: BotIcon,
url: "/dashboard/settings/ai",
isSingle: true,
@@ -305,6 +321,7 @@ const MENU: Menu = {
{
isSingle: true,
title: "Git",
titleKey: "common.side.git",
url: "/dashboard/settings/git-providers",
icon: GitBranch,
// Only enabled for admins and users with access to Git providers
@@ -314,6 +331,7 @@ const MENU: Menu = {
{
isSingle: true,
title: "Registry",
titleKey: "common.side.registry",
url: "/dashboard/settings/registry",
icon: Package,
// Only enabled for admins
@@ -322,6 +340,7 @@ const MENU: Menu = {
{
isSingle: true,
title: "S3 Destinations",
titleKey: "common.side.s3-destinations",
url: "/dashboard/settings/destinations",
icon: Database,
// Only enabled for admins
@@ -331,6 +350,7 @@ const MENU: Menu = {
{
isSingle: true,
title: "Certificates",
titleKey: "common.side.certificates",
url: "/dashboard/settings/certificates",
icon: ShieldCheck,
// Only enabled for admins
@@ -339,6 +359,7 @@ const MENU: Menu = {
{
isSingle: true,
title: "Cluster",
titleKey: "common.side.cluster",
url: "/dashboard/settings/cluster",
icon: Boxes,
// Only enabled for admins in non-cloud environments
@@ -347,6 +368,7 @@ const MENU: Menu = {
{
isSingle: true,
title: "Notifications",
titleKey: "common.side.notifications",
url: "/dashboard/settings/notifications",
icon: Bell,
// Only enabled for admins
@@ -355,6 +377,7 @@ const MENU: Menu = {
{
isSingle: true,
title: "Billing",
titleKey: "common.side.billing",
url: "/dashboard/settings/billing",
icon: CreditCard,
// Only enabled for admins in cloud environments
@@ -365,16 +388,19 @@ const MENU: Menu = {
help: [
{
name: "Documentation",
nameKey: "common.side.documentation",
url: "https://docs.dokploy.com/docs/core",
icon: BookIcon,
},
{
name: "Support",
nameKey: "common.side.support",
url: "https://discord.gg/2tBnJ3jDJc",
icon: CircleHelp,
},
{
name: "Sponsor",
nameKey: "common.side.sponsor",
url: "https://opencollective.com/dokploy",
icon: ({ className }) => (
<HeartIcon
@@ -493,6 +519,7 @@ function LogoWrapper() {
}
function SidebarLogo() {
const { t } = useTranslation("common");
const { state } = useSidebar();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: user } = api.user.get.useQuery();
@@ -689,7 +716,9 @@ function SidebarLogo() {
side={"right"}
className="w-80"
>
<DropdownMenuLabel>Pending Invitations</DropdownMenuLabel>
<DropdownMenuLabel>
{t("common.side.invitations.pending-invitations")}
</DropdownMenuLabel>
<div className="flex flex-col gap-2">
{invitations && invitations.length > 0 ? (
invitations.map((invitation) => (
@@ -702,16 +731,23 @@ function SidebarLogo() {
{invitation?.organization?.name}
</div>
<div className="text-xs text-muted-foreground">
Expires:{" "}
{new Date(invitation.expiresAt).toLocaleString()}
{t("common.side.invitations.expires", {
expireDate: new Date(
invitation.expiresAt,
).toLocaleString(),
})}
</div>
<div className="text-xs text-muted-foreground">
Role: {invitation.role}
{t("common.side.invitations.role", {
role: invitation.role,
})}
</div>
</DropdownMenuItem>
<DialogAction
title="Accept Invitation"
description="Are you sure you want to accept this invitation?"
title={t("common.side.invitations.accept-invitation")}
description={t(
"common.side.invitations.confirm-accept-invitation",
)}
type="default"
onClick={async () => {
const { error } =
@@ -721,24 +757,31 @@ function SidebarLogo() {
if (error) {
toast.error(
error.message || "Error accepting invitation",
error.message ||
t(
"common.side.invitations.error-accepting-invitation",
),
);
} else {
toast.success("Invitation accepted successfully");
toast.success(
t(
"common.side.invitations.invitation-accepted",
),
);
await refetchInvitations();
await refetch();
}
}}
>
<Button size="sm" variant="secondary">
Accept Invitation
{t("common.side.invitations.accept-invitation")}
</Button>
</DialogAction>
</div>
))
) : (
<DropdownMenuItem disabled>
No pending invitations
{t("common.side.invitations.no-pending-invitations")}
</DropdownMenuItem>
)}
</div>
@@ -752,6 +795,8 @@ function SidebarLogo() {
}
export default function Page({ children }: Props) {
const { t } = useTranslation("common");
const [defaultOpen, setDefaultOpen] = useState<boolean | undefined>(
undefined,
);
@@ -818,7 +863,7 @@ export default function Page({ children }: Props) {
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Home</SidebarGroupLabel>
<SidebarGroupLabel>{t("common.side.home")}</SidebarGroupLabel>
<SidebarMenu>
{filteredHome.map((item) => {
const isSingle = item.isSingle !== false;
@@ -851,7 +896,7 @@ export default function Page({ children }: Props) {
className={cn(isActive && "text-primary")}
/>
)}
<span>{item.title}</span>
<span>{t(item.titleKey)}</span>
</Link>
</SidebarMenuButton>
) : (
@@ -907,7 +952,7 @@ export default function Page({ children }: Props) {
</SidebarMenu>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Settings</SidebarGroupLabel>
<SidebarGroupLabel>{t("common.side.settings")}</SidebarGroupLabel>
<SidebarMenu className="gap-2">
{filteredSettings.map((item) => {
const isSingle = item.isSingle !== false;
@@ -940,7 +985,7 @@ export default function Page({ children }: Props) {
className={cn(isActive && "text-primary")}
/>
)}
<span>{item.title}</span>
<span>{t(item.titleKey)}</span>
</Link>
</SidebarMenuButton>
) : (
@@ -996,7 +1041,7 @@ export default function Page({ children }: Props) {
</SidebarMenu>
</SidebarGroup>
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
<SidebarGroupLabel>Extra</SidebarGroupLabel>
<SidebarGroupLabel>{t("common.side.extra")}</SidebarGroupLabel>
<SidebarMenu>
{help.map((item: ExternalLink) => (
<SidebarMenuItem key={item.name}>
@@ -1010,7 +1055,7 @@ export default function Page({ children }: Props) {
<span className="mr-2">
<item.icon className="h-4 w-4" />
</span>
<span>{item.name}</span>
<span>{t(item.nameKey)}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>

View File

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

View File

@@ -2,6 +2,7 @@ import { ShowBilling } from "@/components/dashboard/settings/billing/show-billin
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { appRouter } from "@/server/api/root";
import { getLocale, serverSideTranslations } from "@/utils/i18n";
import { IS_CLOUD } from "@dokploy/server/constants";
import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server";
@@ -30,6 +31,7 @@ export async function getServerSideProps(
};
}
const { req, res } = ctx;
const locale = getLocale(req.cookies);
const { user, session } = await validateRequest(req);
if (!user || user.role === "member") {
return {
@@ -59,6 +61,7 @@ export async function getServerSideProps(
return {
props: {
trpcState: helpers.dehydrate(),
...(await serverSideTranslations(locale, ["common", "settings"])),
},
};
}

View File

@@ -2,6 +2,7 @@ import { ShowCertificates } from "@/components/dashboard/settings/certificates/s
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { appRouter } from "@/server/api/root";
import { getLocale, serverSideTranslations } from "@/utils/i18n";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
@@ -24,6 +25,7 @@ export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { req, res } = ctx;
const locale = await getLocale(req.cookies);
const { user, session } = await validateRequest(req);
if (!user || user.role === "member") {
return {
@@ -51,6 +53,7 @@ export async function getServerSideProps(
return {
props: {
trpcState: helpers.dehydrate(),
...(await serverSideTranslations(locale, ["common", "settings"])),
},
};
}

View File

@@ -2,6 +2,7 @@ import { ShowNodes } from "@/components/dashboard/settings/cluster/nodes/show-no
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { appRouter } from "@/server/api/root";
import { getLocale, serverSideTranslations } from "@/utils/i18n";
import { IS_CLOUD, validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
@@ -25,6 +26,7 @@ export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { req, res } = ctx;
const locale = await getLocale(req.cookies);
if (IS_CLOUD) {
return {
redirect: {
@@ -58,6 +60,7 @@ export async function getServerSideProps(
return {
props: {
trpcState: helpers.dehydrate(),
...(await serverSideTranslations(locale, ["common", "settings"])),
},
};
}

View File

@@ -2,6 +2,7 @@ import { ShowDestinations } from "@/components/dashboard/settings/destination/sh
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { appRouter } from "@/server/api/root";
import { getLocale, serverSideTranslations } from "@/utils/i18n";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
@@ -25,6 +26,7 @@ export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { req, res } = ctx;
const locale = await getLocale(req.cookies);
const { user, session } = await validateRequest(req);
if (!user || user.role === "member") {
return {
@@ -52,6 +54,7 @@ export async function getServerSideProps(
return {
props: {
trpcState: helpers.dehydrate(),
...(await serverSideTranslations(locale, ["common", "settings"])),
},
};
}

View File

@@ -2,6 +2,7 @@ import { ShowGitProviders } from "@/components/dashboard/settings/git/show-git-p
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { appRouter } from "@/server/api/root";
import { getLocale, serverSideTranslations } from "@/utils/i18n";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
@@ -9,68 +10,72 @@ import type { ReactElement } from "react";
import superjson from "superjson";
const Page = () => {
return (
<div className="flex flex-col gap-4 w-full">
<ShowGitProviders />
</div>
);
return (
<div className="flex flex-col gap-4 w-full">
<ShowGitProviders />
</div>
);
};
export default Page;
Page.getLayout = (page: ReactElement) => {
return <DashboardLayout metaName="Git Providers">{page}</DashboardLayout>;
return <DashboardLayout metaName="Git Providers">{page}</DashboardLayout>;
};
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
ctx: GetServerSidePropsContext<{ serviceId: string }>
) {
const { user, session } = await validateRequest(ctx.req);
if (!user) {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
const { req, res } = ctx;
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
req: req as any,
res: res as any,
db: null as any,
session: session as any,
user: user as any,
},
transformer: superjson,
});
await helpers.user.get.prefetch();
try {
await helpers.project.all.prefetch();
await helpers.settings.isCloud.prefetch();
if (user.role === "member") {
const userR = await helpers.user.one.fetch({
userId: user.id,
});
const { user, session } = await validateRequest(ctx.req);
if (!user) {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
const { req, res } = ctx;
const locale = await getLocale(req.cookies);
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
req: req as any,
res: res as any,
db: null as any,
session: session as any,
user: user as any,
},
transformer: superjson,
});
await helpers.user.get.prefetch();
try {
await helpers.project.all.prefetch();
await helpers.settings.isCloud.prefetch();
if (user.role === "member") {
const userR = await helpers.user.one.fetch({
userId: user.id,
});
if (!userR?.canAccessToGitProviders) {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
}
return {
props: {
trpcState: helpers.dehydrate(),
},
};
} catch (_error) {
return {
props: {},
};
}
if (!userR?.canAccessToGitProviders) {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
}
return {
props: {
trpcState: helpers.dehydrate(),
...(await serverSideTranslations(locale, ["common", "settings"])),
},
};
} catch (_error) {
return {
props: {
...(await serverSideTranslations(locale, ["common", "settings"])),
},
};
}
}

View File

@@ -21,6 +21,7 @@ import {
import { Switch } from "@/components/ui/switch";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { getLocale, serverSideTranslations } from "@/utils/i18n";
import { validateRequest } from "@dokploy/server";
import { zodResolver } from "@hookform/resolvers/zod";
import { createServerSideHelpers } from "@trpc/react-query/server";
@@ -180,6 +181,7 @@ export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { req, res } = ctx;
const locale = await getLocale(req.cookies);
const { user, session } = await validateRequest(ctx.req);
if (!user) {
return {
@@ -214,6 +216,7 @@ export async function getServerSideProps(
return {
props: {
trpcState: helpers.dehydrate(),
...(await serverSideTranslations(locale, ["common", "settings"])),
},
};
}

View File

@@ -2,6 +2,7 @@ import { ShowNotifications } from "@/components/dashboard/settings/notifications
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { appRouter } from "@/server/api/root";
import { getLocale, serverSideTranslations } from "@/utils/i18n";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
@@ -25,6 +26,7 @@ export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { req, res } = ctx;
const locale = await getLocale(req.cookies);
const { user, session } = await validateRequest(req);
if (!user || user.role === "member") {
return {
@@ -52,6 +54,7 @@ export async function getServerSideProps(
return {
props: {
trpcState: helpers.dehydrate(),
...(await serverSideTranslations(locale, ["common", "settings"])),
},
};
}

View File

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

View File

@@ -2,6 +2,7 @@ import { ShowRegistry } from "@/components/dashboard/settings/cluster/registry/s
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { appRouter } from "@/server/api/root";
import { serverSideTranslations, getLocale } from "@/utils/i18n";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
@@ -25,6 +26,7 @@ export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { req, res } = ctx;
const locale = await getLocale(req.cookies);
const { user, session } = await validateRequest(req);
if (!user || user.role === "member") {
return {
@@ -51,6 +53,7 @@ export async function getServerSideProps(
return {
props: {
trpcState: helpers.dehydrate(),
...(await serverSideTranslations(locale, ["common", "settings"])),
},
};
}

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ import { ShowDestinations } from "@/components/dashboard/settings/ssh-keys/show-
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { appRouter } from "@/server/api/root";
import { getLocale, serverSideTranslations } from "@/utils/i18n";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
@@ -34,6 +35,7 @@ export async function getServerSideProps(
};
}
const { req, res } = ctx;
const locale = await getLocale(req.cookies);
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
@@ -67,11 +69,14 @@ export async function getServerSideProps(
return {
props: {
trpcState: helpers.dehydrate(),
...(await serverSideTranslations(locale, ["common", "settings"])),
},
};
} catch (_error) {
return {
props: {},
props: {
...(await serverSideTranslations(locale, ["common", "settings"])),
},
};
}
}

View File

@@ -3,6 +3,7 @@ import { ShowUsers } from "@/components/dashboard/settings/users/show-users";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { appRouter } from "@/server/api/root";
import { getLocale, serverSideTranslations } from "@/utils/i18n";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
@@ -27,6 +28,7 @@ export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { req, res } = ctx;
const locale = await getLocale(req.cookies);
const { user, session } = await validateRequest(req);
if (!user || user.role === "member") {
@@ -55,6 +57,7 @@ export async function getServerSideProps(
return {
props: {
trpcState: helpers.dehydrate(),
...(await serverSideTranslations(locale, ["common", "settings"])),
},
};
}

View File

@@ -1 +1,37 @@
{}
{
"common.side.home": "Home",
"common.side.settings": "Settings",
"common.side.extra": "Extra",
"common.side.projects": "Projects",
"common.side.monitoring": "Monitoring",
"common.side.traefik": "Traefik File System",
"common.side.docker": "Docker",
"common.side.swarm": "Swarm",
"common.side.requests": "Requests",
"common.side.web-server": "Web Server",
"common.side.profile": "Profile",
"common.side.remote-servers": "Remote Servers",
"common.side.users": "Users",
"common.side.ssh-keys": "SSH Keys",
"common.side.ai": "AI",
"common.side.git": "Git",
"common.side.registry": "Registry",
"common.side.s3-destinations": "S3 Destinations",
"common.side.certificates": "Certificates",
"common.side.cluster": "Cluster",
"common.side.notifications": "Notifications",
"common.side.billing": "Billing",
"common.side.documentation": "Documentation",
"common.side.support": "Support",
"common.side.sponsor": "Sponsor",
"common.side.invitations.pending-invitations": "Pending Invitations",
"common.side.invitations.no-pending-invitations": "No pending invitations",
"common.side.invitations.accept-invitation": "Accept Invitation",
"common.side.invitations.confirm-accept-invitation": "Are you sure you want to accept this invitation?",
"common.side.invitations.error-accepting-invitation": "Error accepting invitation",
"common.side.invitations.invitation-accepted": "Invitation accepted successfully",
"common.side.invitations.expires": "Expires: {{expireDate}}",
"common.side.invitations.role": "Role: {{role}}"
}

View File

@@ -1 +1,37 @@
{}
{
"common.side.home": "主页",
"common.side.settings": "设置",
"common.side.extra": "其他",
"common.side.projects": "项目",
"common.side.monitoring": "监控",
"common.side.traefik": "Traefik",
"common.side.docker": "Docker",
"common.side.swarm": "Swarm",
"common.side.requests": "请求",
"common.side.web-server": "本地配置",
"common.side.profile": "个人资料",
"common.side.remote-servers": "远程服务器",
"common.side.users": "用户",
"common.side.ssh-keys": "SSH 密钥",
"common.side.ai": "AI",
"common.side.git": "Git 集成",
"common.side.registry": "注册表",
"common.side.s3-destinations": "S3 存储",
"common.side.certificates": "证书",
"common.side.cluster": "集群",
"common.side.notifications": "通知",
"common.side.billing": "账单",
"common.side.documentation": "文档",
"common.side.support": "支持",
"common.side.sponsor": "赞助",
"common.side.invitations.pending-invitations": "待处理邀请",
"common.side.invitations.no-pending-invitations": "没有待处理的邀请",
"common.side.invitations.accept-invitation": "接受邀请",
"common.side.invitations.confirm-accept-invitation": "您确定要接受此邀请吗?",
"common.side.invitations.error-accepting-invitation": "接受邀请时出错",
"common.side.invitations.invitation-accepted": "邀请已成功接受",
"common.side.invitations.expires": "有效期:{{expireDate}}",
"common.side.invitations.role": "角色:{{role}}"
}