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

55
ui/package-lock.json generated
View File

@ -26,6 +26,7 @@
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-tooltip": "^1.1.2",
"@tanstack/react-table": "^8.20.5",
"ahooks": "^3.8.4",
"antd": "^5.22.2",
"antd-zod": "^6.0.0",
"class-variance-authority": "^0.7.0",
@ -38,7 +39,6 @@
"immer": "^10.1.1",
"jszip": "^3.10.1",
"lucide-react": "^0.417.0",
"moment": "^2.30.1",
"nanoid": "^5.0.7",
"pocketbase": "^0.21.4",
"react": "^18.3.1",
@ -3607,6 +3607,28 @@
"object-assign": "4.x"
}
},
"node_modules/ahooks": {
"version": "3.8.4",
"resolved": "https://registry.npmmirror.com/ahooks/-/ahooks-3.8.4.tgz",
"integrity": "sha512-39wDEw2ZHvypaT14EpMMk4AzosHWt0z9bulY0BeDsvc9PqJEV+Kjh/4TZfftSsotBMq52iYIOFPd3PR56e0ZJg==",
"dependencies": {
"@babel/runtime": "^7.21.0",
"dayjs": "^1.9.1",
"intersection-observer": "^0.12.0",
"js-cookie": "^3.0.5",
"lodash": "^4.17.21",
"react-fast-compare": "^3.2.2",
"resize-observer-polyfill": "^1.5.1",
"screenfull": "^5.0.0",
"tslib": "^2.4.1"
},
"engines": {
"node": ">=8.0.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz",
@ -5345,6 +5367,11 @@
"resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/intersection-observer": {
"version": "0.12.2",
"resolved": "https://registry.npmmirror.com/intersection-observer/-/intersection-observer-0.12.2.tgz",
"integrity": "sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg=="
},
"node_modules/invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmmirror.com/invariant/-/invariant-2.2.4.tgz",
@ -5454,6 +5481,14 @@
"jiti": "bin/jiti.js"
}
},
"node_modules/js-cookie": {
"version": "3.0.5",
"resolved": "https://registry.npmmirror.com/js-cookie/-/js-cookie-3.0.5.tgz",
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
"engines": {
"node": ">=14"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
@ -5699,6 +5734,8 @@
"version": "2.30.1",
"resolved": "https://registry.npmmirror.com/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
"optional": true,
"peer": true,
"engines": {
"node": "*"
}
@ -6842,6 +6879,11 @@
"react": "^18.3.1"
}
},
"node_modules/react-fast-compare": {
"version": "3.2.2",
"resolved": "https://registry.npmmirror.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="
},
"node_modules/react-hook-form": {
"version": "7.52.1",
"resolved": "https://registry.npmmirror.com/react-hook-form/-/react-hook-form-7.52.1.tgz",
@ -7173,6 +7215,17 @@
"loose-envify": "^1.1.0"
}
},
"node_modules/screenfull": {
"version": "5.2.0",
"resolved": "https://registry.npmmirror.com/screenfull/-/screenfull-5.2.0.tgz",
"integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==",
"engines": {
"node": ">=0.10.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/scroll-into-view-if-needed": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz",

View File

@ -28,6 +28,7 @@
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-tooltip": "^1.1.2",
"@tanstack/react-table": "^8.20.5",
"ahooks": "^3.8.4",
"antd": "^5.22.2",
"antd-zod": "^6.0.0",
"class-variance-authority": "^0.7.0",
@ -40,7 +41,6 @@
"immer": "^10.1.1",
"jszip": "^3.10.1",
"lucide-react": "^0.417.0",
"moment": "^2.30.1",
"nanoid": "^5.0.7",
"pocketbase": "^0.21.4",
"react": "^18.3.1",

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}", {