feat(ui): antd i18n

This commit is contained in:
Fu Diwei
2024-12-09 16:09:35 +08:00
parent 3b50741f19
commit fdfe54b6da
12 changed files with 138 additions and 67 deletions

49
ui/src/App.tsx Normal file
View File

@@ -0,0 +1,49 @@
import { useLayoutEffect, useState } from "react";
import { RouterProvider } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { App as AntdApp, ConfigProvider as AntdConfigProvider } from "antd";
import { type Locale } from "antd/es/locale";
import AntdLocaleEnUs from "antd/locale/en_US";
import AntdLocaleZhCN from "antd/locale/zh_CN";
import dayjs from "dayjs";
import "dayjs/locale/zh-cn";
import { localeNames } from "./i18n";
import { router } from "./router.tsx";
import { ThemeProvider } from "./components/ThemeProvider.tsx";
const App = () => {
const { i18n } = useTranslation();
const antdLocalesMap: Record<string, Locale> = {
[localeNames.ZH]: AntdLocaleZhCN,
[localeNames.EN]: AntdLocaleEnUs,
};
const [antdLocale, setAntdLocale] = useState(antdLocalesMap[i18n.language]);
const handleLanguageChanged = () => {
setAntdLocale(antdLocalesMap[i18n.language]);
dayjs.locale(i18n.language);
};
i18n.on("languageChanged", handleLanguageChanged);
useLayoutEffect(handleLanguageChanged, [i18n]);
return (
<AntdConfigProvider
locale={antdLocale}
theme={{
token: {
colorPrimary: "hsl(24.6 95% 53.1%)",
},
}}
>
<AntdApp>
<ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
<RouterProvider router={router} />
</ThemeProvider>
</AntdApp>
</AntdConfigProvider>
);
};
export default App;

View File

@@ -1,25 +0,0 @@
import { useTranslation } from "react-i18next";
import { Languages } from "lucide-react";
import { Button } from "@/components/ui/button";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
export default function LocaleToggle() {
const { i18n } = useTranslation();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<Languages className="h-[1.2rem] w-[1.2rem] dark:text-white" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{Object.keys(i18n.store.data).map((key) => (
<DropdownMenuItem onClick={() => i18n.changeLanguage(key)}>{i18n.store.data[key].name as string}</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -2,14 +2,14 @@ import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import resources from "./locales";
import resources, { LOCALE_ZH_NAME, LOCALE_EN_NAME } from "./locales";
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources,
fallbackLng: "zh",
fallbackLng: LOCALE_ZH_NAME,
debug: true,
interpolation: {
escapeValue: false,
@@ -19,4 +19,9 @@ i18n
},
});
export const localeNames = {
ZH: LOCALE_ZH_NAME,
EN: LOCALE_EN_NAME,
};
export default i18n;

View File

@@ -3,12 +3,15 @@ import { Resource } from "i18next";
import zh from "./zh";
import en from "./en";
export const LOCALE_ZH_NAME = "zh" as const;
export const LOCALE_EN_NAME = "en" as const;
const resources: Resource = {
zh: {
[LOCALE_ZH_NAME]: {
name: "简体中文",
translation: zh,
},
en: {
[LOCALE_EN_NAME]: {
name: "English",
translation: en,
},

View File

@@ -1,31 +1,17 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { RouterProvider } from "react-router-dom";
import { App, ConfigProvider } from "antd";
import AntdLocaleZhCN from "antd/locale/zh_CN";
import dayjs from "dayjs";
import dayjsUtc from "dayjs/plugin/utc";
import "dayjs/locale/zh-cn";
import { router } from "./router.tsx";
import { ThemeProvider } from "./components/ThemeProvider.tsx";
import App from "./App";
import "./i18n";
import "./global.css";
// TODO: antd i18n
dayjs.extend(dayjsUtc);
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App>
<ConfigProvider
locale={AntdLocaleZhCN}
theme={{
token: {
colorPrimary: "hsl(24.6 95% 53.1%)",
},
}}
>
<ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
<RouterProvider router={router} />
</ThemeProvider>
</ConfigProvider>
</App>
<App />
</React.StrictMode>
);

View File

@@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
import { Avatar, Button, Empty, Modal, notification, Space, Table, Tooltip, Typography, type TableProps } from "antd";
import { PageHeader } from "@ant-design/pro-components";
import { Copy as CopyIcon, Pencil as PencilIcon, Plus as PlusIcon, Trash2 as Trash2Icon } from "lucide-react";
import moment from "moment";
import dayjs from "dayjs";
import { ClientResponseError } from "pocketbase";
import AccessEditDialog from "@/components/certimate/AccessEditDialog";
@@ -50,7 +50,7 @@ const AccessList = () => {
title: t("common.text.created_at"),
ellipsis: true,
render: (_, record) => {
return moment(record.created!).format("YYYY-MM-DD HH:mm:ss");
return dayjs(record.created!).format("YYYY-MM-DD HH:mm:ss");
},
},
{
@@ -58,7 +58,7 @@ const AccessList = () => {
title: t("common.text.updated_at"),
ellipsis: true,
render: (_, record) => {
return moment(record.updated!).format("YYYY-MM-DD HH:mm:ss");
return dayjs(record.updated!).format("YYYY-MM-DD HH:mm:ss");
},
},
{

View File

@@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next";
import { Button, Divider, Empty, Menu, notification, Radio, Space, Table, theme, Tooltip, Typography, type MenuProps, type TableProps } from "antd";
import { PageHeader } from "@ant-design/pro-components";
import { Eye as EyeIcon, Filter as FilterIcon } from "lucide-react";
import moment from "moment";
import dayjs from "dayjs";
import { ClientResponseError } from "pocketbase";
import CertificateDetailDrawer from "@/components/certificate/CertificateDetailDrawer";
@@ -87,8 +87,8 @@ const CertificateList = () => {
},
filterIcon: () => <FilterIcon size={14} />,
render: (_, record) => {
const total = moment(record.expireAt).diff(moment(record.created), "d") + 1;
const left = moment(record.expireAt).diff(moment(), "d");
const total = dayjs(record.expireAt).diff(dayjs(record.created), "d") + 1;
const left = dayjs(record.expireAt).diff(dayjs(), "d");
return (
<Space className="max-w-full" direction="vertical" size={4}>
{left > 0 ? (
@@ -98,7 +98,7 @@ const CertificateList = () => {
)}
<Typography.Text type="secondary">
{t("certificate.props.expiry.expiration", { date: moment(record.expireAt).format("YYYY-MM-DD") })}
{t("certificate.props.expiry.expiration", { date: dayjs(record.expireAt).format("YYYY-MM-DD") })}
</Typography.Text>
</Space>
);
@@ -132,7 +132,7 @@ const CertificateList = () => {
title: t("common.text.created_at"),
ellipsis: true,
render: (_, record) => {
return moment(record.created!).format("YYYY-MM-DD HH:mm:ss");
return dayjs(record.created!).format("YYYY-MM-DD HH:mm:ss");
},
},
{
@@ -140,7 +140,7 @@ const CertificateList = () => {
title: t("common.text.updated_at"),
ellipsis: true,
render: (_, record) => {
return moment(record.updated!).format("YYYY-MM-DD HH:mm:ss");
return dayjs(record.updated!).format("YYYY-MM-DD HH:mm:ss");
},
},
{

View File

@@ -20,7 +20,7 @@ import {
} from "antd";
import { PageHeader } from "@ant-design/pro-components";
import { Filter as FilterIcon, Pencil as PencilIcon, Plus as PlusIcon, Trash2 as Trash2Icon } from "lucide-react";
import moment from "moment";
import dayjs from "dayjs";
import { ClientResponseError } from "pocketbase";
import { Workflow as WorkflowType } from "@/domain/workflow";
@@ -153,7 +153,7 @@ const WorkflowList = () => {
title: t("common.text.created_at"),
ellipsis: true,
render: (_, record) => {
return moment(record.created!).format("YYYY-MM-DD HH:mm:ss");
return dayjs(record.created!).format("YYYY-MM-DD HH:mm:ss");
},
},
{
@@ -161,7 +161,7 @@ const WorkflowList = () => {
title: t("common.text.updated_at"),
ellipsis: true,
render: (_, record) => {
return moment(record.updated!).format("YYYY-MM-DD HH:mm:ss");
return dayjs(record.updated!).format("YYYY-MM-DD HH:mm:ss");
},
},
{

View File

@@ -1,4 +1,4 @@
import moment from "moment";
import dayjs from "dayjs";
import { type Access } from "@/domain/access";
import { getPocketBase } from "./pocketbase";
@@ -19,6 +19,6 @@ export const save = async (record: Access) => {
};
export const remove = async (record: Access) => {
record.deleted = moment.utc().format("YYYY-MM-DD HH:mm:ss");
record.deleted = dayjs.utc().format("YYYY-MM-DD HH:mm:ss");
return await getPocketBase().collection("access").update(record.id, record);
};

View File

@@ -1,5 +1,5 @@
import dayjs from "dayjs";
import { type RecordListOptions } from "pocketbase";
import moment from "moment";
import { type Certificate } from "@/domain/certificate";
import { getPocketBase } from "./pocketbase";
@@ -23,7 +23,7 @@ export const list = async (req: CertificateListReq) => {
if (req.state === "expireSoon") {
options.filter = pb.filter("expireAt<{:expiredAt}", {
expiredAt: moment().add(15, "d").toDate(),
expiredAt: dayjs().add(15, "d").toDate(),
});
} else if (req.state === "expired") {
options.filter = pb.filter("expireAt<={:expiredAt}", {