From 85df8eb09dfc317a48acdf4b2a8c43714435e5f2 Mon Sep 17 00:00:00 2001 From: elvis liao <1219585136@qq.com> Date: Thu, 26 Sep 2024 17:53:44 +0800 Subject: [PATCH 1/8] feat: Add i18n --- ui/package-lock.json | 141 +++++++++++++++++++++++++++++ ui/package.json | 6 +- ui/src/components/LocaleToggle.tsx | 33 +++++++ ui/src/i18n/index.ts | 33 +++++++ ui/src/i18n/locales/en.json | 11 +++ ui/src/i18n/locales/zh.json | 118 ++++++++++++++++++++++++ 6 files changed, 341 insertions(+), 1 deletion(-) create mode 100644 ui/src/components/LocaleToggle.tsx create mode 100644 ui/src/i18n/index.ts create mode 100644 ui/src/i18n/locales/en.json create mode 100644 ui/src/i18n/locales/zh.json diff --git a/ui/package-lock.json b/ui/package-lock.json index 12c292c..18c5eb8 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -27,6 +27,9 @@ "@radix-ui/react-tooltip": "^1.1.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "i18next": "^23.15.1", + "i18next-browser-languagedetector": "^8.0.0", + "i18next-http-backend": "^2.6.1", "jszip": "^3.10.1", "lucide-react": "^0.417.0", "moment": "^2.30.1", @@ -34,6 +37,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-hook-form": "^7.52.1", + "react-i18next": "^15.0.2", "react-router-dom": "^6.25.1", "tailwind-merge": "^2.4.0", "tailwindcss-animate": "^1.0.7", @@ -382,6 +386,17 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.25.6", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.25.6.tgz", + "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.24.7", "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.24.7.tgz", @@ -2959,6 +2974,14 @@ "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, + "node_modules/cross-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -3697,6 +3720,52 @@ "node": ">= 0.4" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/i18next": { + "version": "23.15.1", + "resolved": "https://registry.npmmirror.com/i18next/-/i18next-23.15.1.tgz", + "integrity": "sha512-wB4abZ3uK7EWodYisHl/asf8UYEhrI/vj/8aoSsrj/ZDxj4/UXPOa1KvFt1Fq5hkUHquNqwFlDprmjZ8iySgYA==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.0.tgz", + "integrity": "sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-http-backend": { + "version": "2.6.1", + "resolved": "https://registry.npmmirror.com/i18next-http-backend/-/i18next-http-backend-2.6.1.tgz", + "integrity": "sha512-rCilMAnlEQNeKOZY1+x8wLM5IpYOj10guGvEpeC59tNjj6MMreLIjIW8D1RclhD3ifLwn6d/Y9HEM1RUE6DSog==", + "dependencies": { + "cross-fetch": "4.0.0" + } + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.1.tgz", @@ -4112,6 +4181,25 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-releases": { "version": "2.0.18", "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.18.tgz", @@ -4553,6 +4641,27 @@ "react": "^16.8.0 || ^17 || ^18 || ^19" } }, + "node_modules/react-i18next": { + "version": "15.0.2", + "resolved": "https://registry.npmmirror.com/react-i18next/-/react-i18next-15.0.2.tgz", + "integrity": "sha512-z0W3/RES9Idv3MmJUcf0mDNeeMOUXe+xoL0kPfQPbDoZHmni/XsIoq5zgT2MCFUiau283GuBUK578uD/mkAbLQ==", + "dependencies": { + "@babel/runtime": "^7.25.0", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.2.3", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.14.2.tgz", @@ -4692,6 +4801,11 @@ "node": ">=8.10.0" } }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.8.tgz", @@ -5140,6 +5254,11 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz", @@ -5357,6 +5476,28 @@ } } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", diff --git a/ui/package.json b/ui/package.json index 1c954e6..97e3db9 100644 --- a/ui/package.json +++ b/ui/package.json @@ -40,7 +40,11 @@ "tailwind-merge": "^2.4.0", "tailwindcss-animate": "^1.0.7", "vaul": "^0.9.1", - "zod": "^3.23.8" + "zod": "^3.23.8", + "i18next": "^23.15.1", + "i18next-browser-languagedetector": "^8.0.0", + "i18next-http-backend": "^2.6.1", + "react-i18next": "^15.0.2" }, "devDependencies": { "@types/node": "^22.0.0", diff --git a/ui/src/components/LocaleToggle.tsx b/ui/src/components/LocaleToggle.tsx new file mode 100644 index 0000000..01ec02e --- /dev/null +++ b/ui/src/components/LocaleToggle.tsx @@ -0,0 +1,33 @@ +import { Languages } from "lucide-react"; + +import { useTranslation } from "react-i18next"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +export default function LocaleToggle() { + const { i18n } = useTranslation() + + return ( + + + + + + {Object.keys(i18n.store.data).map(key => ( + i18n.changeLanguage(key)}> + {i18n.store.data[key].name as string} + + ))} + + + ); +} diff --git a/ui/src/i18n/index.ts b/ui/src/i18n/index.ts new file mode 100644 index 0000000..eaed9c9 --- /dev/null +++ b/ui/src/i18n/index.ts @@ -0,0 +1,33 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; + +import zh from './locales/zh.json' +import en from './locales/en.json' + + +i18n + .use(LanguageDetector) + .use(initReactI18next) + .init({ + resources: { + zh: { + name: '简体中文', + translation: zh + }, + en: { + name: 'English', + translation: en + } + }, + fallbackLng: 'zh', + debug: true, + interpolation: { + escapeValue: false, + }, + backend: { + loadPath: '/locales/{{lng}}.json', + } + }); + +export default i18n; diff --git a/ui/src/i18n/locales/en.json b/ui/src/i18n/locales/en.json new file mode 100644 index 0000000..26e64d2 --- /dev/null +++ b/ui/src/i18n/locales/en.json @@ -0,0 +1,11 @@ +{ + "username": "username", + "password": "password", + "login.submit": "login.submit", + "login.username.no.empty.message": "login.username.no.empty.message", + "login.password.length.message": "login.password.length.message", + "menu.dashboard": "menu.dashboard", + "menu.domain.management": "menu.domain.management", + "menu.auth.management": "menu.auth.management", + "menu.deploy.log": "menu.deploy.log" +} \ No newline at end of file diff --git a/ui/src/i18n/locales/zh.json b/ui/src/i18n/locales/zh.json new file mode 100644 index 0000000..e26e661 --- /dev/null +++ b/ui/src/i18n/locales/zh.json @@ -0,0 +1,118 @@ +{ + "username": "用户名", + "password": "密码", + "email": "邮箱", + "logout": "退出登录", + "setting": "设置", + "account": "账户", + "template": "模版", + "save": "保存", + "no.data": "暂无数据", + "status": "状态", + "operation": "操作", + "enable": "启用", + "disable": "禁用", + "deploy": "部署", + "download": "下载", + "delete": "删除", + "cancel": "取消", + "confirm": "确认", + "edit": "编辑", + "operation.succeed": "操作成功", + "save.succeed": "保存成功", + "save.failed": "保存失败", + "login.submit": "登录", + "login.username.no.empty.message": "请输入正确的邮箱地址", + "login.password.length.message": "密码至少10个字符", + "menu": { + "auth.management": "授权管理" + }, + "theme": { + "light": "浅色", + "dark": "暗黑", + "system": "系统" + }, + "dashboard": "控制面板", + "dashboard.all": "所有", + "dashboard.near.expired": "即将过期", + "dashboard.enabled": "启用中", + "dashboard.not.enabled": "未启用", + "dashboard.unit": "个", + "deployment.log": { + "name": "部署历史", + "empty": "你暂未创建任何部署,请先添加域名进行部署吧!", + "status": "状态", + "stage": "阶段", + "last.execution.time": "最近执行时间", + "detail.button.text": "日志", + "detail": "部署详情" + }, + "pagination": { + "next": "下一页", + "prev": "上一页" + }, + "domain": "域名", + "domain.add": "新增域名", + "domain.delete": "删除域名", + "domain.management": { + "name": "域名列表", + "start.deploy.succeed.tips": "已发起部署,请稍后查看部署日志。", + "execution.failed": "执行失败", + "execution.failed.tips": "执行失败,请在 <1>部署历史 查看详情。", + "empty": "请添加域名开始部署证书吧。", + "expiry.date": "有效期限", + "expiry.date1": "有效期 {{date}} 天", + "expiry.date2": "{{date}} 到期", + "last.execution.time": "最近执行时间", + "last.execution.status": "最近执行状态", + "last.execution.stage": "最近执行阶段", + "enable": "是否启用", + "start.deploying": "立即部署", + "forced.deployment": "强行部署", + "delete.confirm": "确定要删除域名吗?", + "edit": { + "title": "编辑域名", + "domain.verify.tips": "域名", + "dns.verify.tips": "请选择DNS服务商授权配置", + "target.type.verify.tips": "请选择部署服务类型" + } + }, + "setting.notify.menu": "消息推送", + "setting.submit": "确认修改", + "setting.account.email": { + "valid.message": "请输入正确的邮箱地址", + "placeholder": "请输入邮箱", + "change.succeed": "修改账户邮箱成功", + "change.failed": "修改账户邮箱失败" + }, + "setting.account.log.back.in": "请重新登录", + "setting.password": { + "length.message": "密码至少10个字符", + "not.match": "两次密码不一致", + "change.succeed": "修改密码成功", + "change.failed": "修改密码失败", + "current.password": "当前密码", + "new.password": "新密码", + "confirm.password": "确认密码" + }, + "setting.notify": { + "template": { + "save.succeed": "通知模板保存成功", + "variables.tips": { + "title": "可选的变量, COUNT:即将过期张数", + "content": "可选的变量, COUNT:即将过期张数,DOMAINS:域名列表" + } + }, + "config": { + "enable": "是否启用", + "save.succeed": "配置保存成功", + "save.failed": "配置保存失败", + "save.failed.url.not.valid": "Url格式不正确" + } + }, + "deploy.progress": { + "check": "检查", + "apply": "获取", + "deploy": "部署" + } +} \ No newline at end of file From 0abb030889ce36cbea7e68aaf884fab5fda6f506 Mon Sep 17 00:00:00 2001 From: elvis liao <1219585136@qq.com> Date: Thu, 26 Sep 2024 17:57:30 +0800 Subject: [PATCH 2/8] wip: i18n chinese --- ui/src/components/ThemeToggle.tsx | 9 ++- .../components/certimate/DeployProgress.tsx | 76 ++++++++++++++----- ui/src/components/notify/DingTalk.tsx | 14 ++-- ui/src/components/notify/NotifyTemplate.tsx | 12 +-- ui/src/components/notify/Telegram.tsx | 14 ++-- ui/src/components/notify/Webhook.tsx | 18 +++-- ui/src/components/ui/form.tsx | 5 +- ui/src/components/ui/pagination.tsx | 53 +++++++------ ui/src/pages/DashboardLayout.tsx | 26 ++++--- ui/src/pages/SettingLayout.tsx | 10 ++- ui/src/pages/dashboard/Dashboard.tsx | 60 ++++++++++----- ui/src/pages/domains/Edit.tsx | 8 +- ui/src/pages/domains/Home.tsx | 73 +++++++++--------- ui/src/pages/history/History.tsx | 26 ++++--- ui/src/pages/login/Login.tsx | 20 +++-- ui/src/pages/setting/Account.tsx | 18 +++-- ui/src/pages/setting/Notify.tsx | 5 +- ui/src/pages/setting/Password.tsx | 26 ++++--- 18 files changed, 289 insertions(+), 184 deletions(-) diff --git a/ui/src/components/ThemeToggle.tsx b/ui/src/components/ThemeToggle.tsx index 4ec3d9c..a772948 100644 --- a/ui/src/components/ThemeToggle.tsx +++ b/ui/src/components/ThemeToggle.tsx @@ -1,4 +1,5 @@ import { Moon, Sun } from "lucide-react"; +import { useTranslation } from 'react-i18next' import { Button } from "@/components/ui/button"; import { @@ -11,6 +12,8 @@ import { useTheme } from "./ThemeProvider"; export function ThemeToggle() { const { setTheme } = useTheme(); + const { t } = useTranslation(); + return ( @@ -23,13 +26,13 @@ export function ThemeToggle() { setTheme("light")}> - 浅色 + {t('theme.light')} setTheme("dark")}> - 暗黑 + {t('theme.dark')} setTheme("system")}> - 系统 + {t('theme.system')} diff --git a/ui/src/components/certimate/DeployProgress.tsx b/ui/src/components/certimate/DeployProgress.tsx index 863da79..b96dc2b 100644 --- a/ui/src/components/certimate/DeployProgress.tsx +++ b/ui/src/components/certimate/DeployProgress.tsx @@ -1,3 +1,5 @@ +import { useTranslation } from "react-i18next"; + import { Separator } from "../ui/separator"; type DeployProgressProps = { @@ -6,26 +8,40 @@ type DeployProgressProps = { }; const DeployProgress = ({ phase, phaseSuccess }: DeployProgressProps) => { + const { t } = useTranslation(); + let rs = <> ; if (phase === "check") { if (phaseSuccess) { rs = (
-
检查
+
+ {t('deploy.progress.check')} +
-
获取
+
+ {t('deploy.progress.apply')} +
-
部署
+
+ {t('deploy.progress.deploy')} +
); } else { rs = (
-
检查
+
+ {t('deploy.progress.check')} +
-
获取
+
+ {t('deploy.progress.apply')} +
-
部署
+
+ {t('deploy.progress.deploy')} +
); } @@ -35,21 +51,33 @@ const DeployProgress = ({ phase, phaseSuccess }: DeployProgressProps) => { if (phaseSuccess) { rs = (
-
检查
+
+ {t('deploy.progress.check')} +
-
获取
+
+ {t('deploy.progress.apply')} +
-
部署
+
+ {t('deploy.progress.deploy')} +
); } else { rs = (
-
检查
+
+ {t('deploy.progress.check')} +
-
获取
+
+ {t('deploy.progress.apply')} +
-
部署
+
+ {t('deploy.progress.deploy')} +
); } @@ -59,21 +87,33 @@ const DeployProgress = ({ phase, phaseSuccess }: DeployProgressProps) => { if (phaseSuccess) { rs = (
-
检查
+
+ {t('deploy.progress.check')} +
-
获取
+
+ {t('deploy.progress.apply')} +
-
部署
+
+ {t('deploy.progress.deploy')} +
); } else { rs = (
-
检查
+
+ {t('deploy.progress.check')} +
-
获取
+
+ {t('deploy.progress.apply')} +
-
部署
+
+ {t('deploy.progress.deploy')} +
); } diff --git a/ui/src/components/notify/DingTalk.tsx b/ui/src/components/notify/DingTalk.tsx index 2e2a58f..37b3f32 100644 --- a/ui/src/components/notify/DingTalk.tsx +++ b/ui/src/components/notify/DingTalk.tsx @@ -8,6 +8,7 @@ import { useEffect, useState } from "react"; import { update } from "@/repository/settings"; import { getErrMessage } from "@/lib/error"; import { useToast } from "../ui/use-toast"; +import { useTranslation } from 'react-i18next' type DingTalkSetting = { id: string; @@ -17,6 +18,7 @@ type DingTalkSetting = { const DingTalk = () => { const { config, setChannels } = useNotify(); + const { t } = useTranslation(); const [dingtalk, setDingtalk] = useState({ id: config.id ?? "", @@ -70,15 +72,15 @@ const DingTalk = () => { setChannels(resp); toast({ - title: "保存成功", - description: "配置保存成功", + title: t('save.succeed'), + description: t('setting.notify.config.save.succeed'), }); } catch (e) { const msg = getErrMessage(e); toast({ - title: "保存失败", - description: "配置保存失败:" + msg, + title: t('save.failed'), + description: `${t('setting.notify.config.save.failed')}: ${msg}`, variant: "destructive", }); } @@ -127,7 +129,7 @@ const DingTalk = () => { }); }} /> - +
@@ -136,7 +138,7 @@ const DingTalk = () => { handleSaveClick(); }} > - 保存 + {t('save')}
diff --git a/ui/src/components/notify/NotifyTemplate.tsx b/ui/src/components/notify/NotifyTemplate.tsx index 2a761b0..2412ee0 100644 --- a/ui/src/components/notify/NotifyTemplate.tsx +++ b/ui/src/components/notify/NotifyTemplate.tsx @@ -9,6 +9,7 @@ import { } from "@/domain/settings"; import { getSetting, update } from "@/repository/settings"; import { useToast } from "../ui/use-toast"; +import { useTranslation } from 'react-i18next' const NotifyTemplate = () => { const [id, setId] = useState(""); @@ -17,6 +18,7 @@ const NotifyTemplate = () => { ]); const { toast } = useToast(); + const { t } = useTranslation(); useEffect(() => { const featchData = async () => { @@ -66,8 +68,8 @@ const NotifyTemplate = () => { } toast({ - title: "保存成功", - description: "通知模板保存成功", + title: t('save.succeed'), + description: t('setting.notify.template.save.succeed'), }); }; @@ -81,7 +83,7 @@ const NotifyTemplate = () => { />
- 可选的变量, COUNT:即将过期张数 + {t('setting.notify.template.variables.tips.title')}
- 可选的变量, COUNT:即将过期张数,DOMAINS:域名列表 + {t('setting.notify.template.variables.tips.content')}
- +
); diff --git a/ui/src/components/notify/Telegram.tsx b/ui/src/components/notify/Telegram.tsx index 5091946..339a24d 100644 --- a/ui/src/components/notify/Telegram.tsx +++ b/ui/src/components/notify/Telegram.tsx @@ -8,6 +8,7 @@ import { useEffect, useState } from "react"; import { update } from "@/repository/settings"; import { getErrMessage } from "@/lib/error"; import { useToast } from "../ui/use-toast"; +import { useTranslation } from "react-i18next"; type TelegramSetting = { id: string; @@ -17,6 +18,7 @@ type TelegramSetting = { const Telegram = () => { const { config, setChannels } = useNotify(); + const { t } = useTranslation(); const [telegram, setTelegram] = useState({ id: config.id ?? "", @@ -70,15 +72,15 @@ const Telegram = () => { setChannels(resp); toast({ - title: "保存成功", - description: "配置保存成功", + title: t('save.succeed'), + description: t('setting.notify.config.save.succeed'), }); } catch (e) { const msg = getErrMessage(e); toast({ - title: "保存失败", - description: "配置保存失败:" + msg, + title: t('save.failed'), + description: `${t('setting.notify.config.save.failed')}: ${msg}`, variant: "destructive", }); } @@ -128,7 +130,7 @@ const Telegram = () => { }); }} /> - +
@@ -137,7 +139,7 @@ const Telegram = () => { handleSaveClick(); }} > - 保存 + {t('save')}
diff --git a/ui/src/components/notify/Webhook.tsx b/ui/src/components/notify/Webhook.tsx index f2844a7..af8468c 100644 --- a/ui/src/components/notify/Webhook.tsx +++ b/ui/src/components/notify/Webhook.tsx @@ -9,6 +9,7 @@ import { update } from "@/repository/settings"; import { getErrMessage } from "@/lib/error"; import { useToast } from "../ui/use-toast"; import { isValidURL } from "@/lib/url"; +import { useTranslation } from 'react-i18next' type WebhookSetting = { id: string; @@ -18,6 +19,7 @@ type WebhookSetting = { const Webhook = () => { const { config, setChannels } = useNotify(); + const { t } = useTranslation(); const [webhook, setWebhook] = useState({ id: config.id ?? "", @@ -59,8 +61,8 @@ const Webhook = () => { webhook.data.url = webhook.data.url.trim(); if (!isValidURL(webhook.data.url)) { toast({ - title: "保存失败", - description: "Url格式不正确", + title: t('save.failed'), + description: t('setting.notify.config.save.failed.url.not.valid'), variant: "destructive", }); return; @@ -79,15 +81,15 @@ const Webhook = () => { setChannels(resp); toast({ - title: "保存成功", - description: "配置保存成功", + title: t('save.succeed'), + description: t('setting.notify.config.save.succeed'), }); } catch (e) { const msg = getErrMessage(e); toast({ - title: "保存失败", - description: "配置保存失败:" + msg, + title: t('save.failed'), + description: `${t('setting.notify.config.save.failed')}: ${msg}`, variant: "destructive", }); } @@ -123,7 +125,7 @@ const Webhook = () => { }); }} /> - +
@@ -132,7 +134,7 @@ const Webhook = () => { handleSaveClick(); }} > - 保存 + {t('save')}
diff --git a/ui/src/components/ui/form.tsx b/ui/src/components/ui/form.tsx index 4603f8b..d91f6b8 100644 --- a/ui/src/components/ui/form.tsx +++ b/ui/src/components/ui/form.tsx @@ -12,6 +12,7 @@ import { import { cn } from "@/lib/utils" import { Label } from "@/components/ui/label" +import { useTranslation } from "react-i18next" const Form = FormProvider @@ -145,7 +146,9 @@ const FormMessage = React.forwardRef< React.HTMLAttributes >(({ className, children, ...props }, ref) => { const { error, formMessageId } = useFormField() - const body = error ? String(error?.message) : children + const { t } = useTranslation() + + const body = error ? t(String(error?.message)) : children if (!body) { return null diff --git a/ui/src/components/ui/pagination.tsx b/ui/src/components/ui/pagination.tsx index d6b1fcf..bbaa801 100644 --- a/ui/src/components/ui/pagination.tsx +++ b/ui/src/components/ui/pagination.tsx @@ -3,6 +3,7 @@ import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"; import { cn } from "@/lib/utils"; import { ButtonProps, buttonVariants } from "@/components/ui/button"; +import { useTranslation } from "react-i18next"; const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => ( @@ -138,7 +141,7 @@ export default function Dashboard() { )} > - 控制面板 + {t('dashboard')} - 域名列表 + {t('domain.management.name')} - 授权管理 + {t('menu.auth.management')} - 部署历史 + {t('deployment.log.name')}
+ @@ -182,16 +200,16 @@ const Dashboard = () => { ) : ( <>
-
域名
+
{t('domain')}
-
状态
-
阶段
-
最近执行时间
+
{t('deployment.log.status')}
+
{t('deployment.log.stage')}
+
{t('deployment.log.last.execution.time')}
-
操作
+
{t('operation')}
- 部署历史 + {t('deployment.log.name')}
{deployments?.map((deployment) => ( @@ -218,14 +236,14 @@ const Dashboard = () => { {deployment.expand.domain?.domain}-{deployment.id} - 部署详情 + {t('deployment.log.detail')}
diff --git a/ui/src/pages/domains/Edit.tsx b/ui/src/pages/domains/Edit.tsx index af03ecc..3a4c423 100644 --- a/ui/src/pages/domains/Edit.tsx +++ b/ui/src/pages/domains/Edit.tsx @@ -38,6 +38,7 @@ import EmailsEdit from "@/components/certimate/EmailsEdit"; import { Textarea } from "@/components/ui/textarea"; import { cn } from "@/lib/utils"; import { EmailsSetting } from "@/domain/settings"; +import { useTranslation } from "react-i18next"; const Edit = () => { const { @@ -47,6 +48,7 @@ const Edit = () => { const [domain, setDomain] = useState(); const location = useLocation(); + const { t } = useTranslation(); const [tab, setTab] = useState<"base" | "advance">("base"); @@ -69,15 +71,15 @@ const Edit = () => { const formSchema = z.object({ id: z.string().optional(), domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { - message: "请输入正确的域名", + message: t('domain.management.edit.domain.verify.tips'), }), email: z.string().email().optional(), access: z.string().regex(/^[a-zA-Z0-9]+$/, { - message: "请选择DNS服务商授权配置", + message: t('domain.management.edit.dns.verify.tips'), }), targetAccess: z.string().optional(), targetType: z.string().regex(/^[a-zA-Z0-9-]+$/, { - message: "请选择部署服务类型", + message: t('domain.management.edit.target.type.verify.tips'), }), variables: z.string().optional(), group: z.string().optional(), diff --git a/ui/src/pages/domains/Home.tsx b/ui/src/pages/domains/Home.tsx index 162eaf6..5150a17 100644 --- a/ui/src/pages/domains/Home.tsx +++ b/ui/src/pages/domains/Home.tsx @@ -35,11 +35,13 @@ import { TooltipContent, TooltipProvider } from "@radix-ui/react-tooltip"; import { Earth } from "lucide-react"; import { useEffect, useState } from "react"; import { Link, useLocation, useNavigate } from "react-router-dom"; +import { useTranslation, Trans } from "react-i18next"; const Home = () => { const toast = useToast(); const navigate = useNavigate(); + const { t } = useTranslation() const location = useLocation(); const query = new URLSearchParams(location.search); @@ -127,23 +129,22 @@ const Home = () => { await save(domain); toast.toast({ - title: "操作成功", - description: "已发起部署,请稍后查看部署日志。", + title: t('operation.succeed'), + description: t('domain.management.start.deploy.succeed.tips'), }); } catch (e) { toast.toast({ - title: "执行失败", + title: t('domain.management.execution.failed'), description: ( - <> - 执行失败,请查看 + // 这里的 text 只是占位作用,实际文案在 src/i18n/locales/[lang].json + + text1 - 部署日志 - - 查看详情。 - + >text2 + text3 + ), variant: "destructive", }); @@ -175,8 +176,10 @@ const Home = () => {
-
域名列表
- +
{t('domain.management.name')}
+
{!domains.length ? ( @@ -187,26 +190,26 @@ const Home = () => {
- 请添加域名开始部署证书吧。 + {t('domain.management.empty')}
) : ( <>
-
域名
-
有效期限
-
最近执行状态
-
最近执行阶段
-
最近执行时间
-
是否启用
-
操作
+
{t('domain')}
+
{t('domain.management.expiry.date')}
+
{t('domain.management.last.execution.status')}
+
{t('domain.management.last.execution.stage')}
+
{t('domain.management.last.execution.time')}
+
{t('domain.management.enable')}
+
{t('operation')}
- 域名 + {t('domain')}
{domains.map((domain) => ( @@ -221,8 +224,8 @@ const Home = () => {
{domain.expiredAt ? ( <> -
有效期90天
-
{getDate(domain.expiredAt)}到期
+
{t('domain.management.expiry.date1', { date: 90 })}
+
{t('domain.management.expiry.date2', { date: getDate(domain.expiredAt) })}
) : ( "---" @@ -266,7 +269,7 @@ const Home = () => {
- {domain.enabled ? "禁用" : "启用"} + {domain.enabled ? t('disable') : t('enable')}
@@ -278,7 +281,7 @@ const Home = () => { className="p-0" onClick={() => handleHistoryClick(domain.id)} > - 部署历史 + {t('deployment.log.name')} @@ -287,7 +290,7 @@ const Home = () => { className="p-0" onClick={() => handleRightNowClick(domain)} > - 立即部署 + {t('domain.management.start.deploying')} @@ -304,7 +307,7 @@ const Home = () => { className="p-0" onClick={() => handleForceClick(domain)} > - 强行部署 + {t('domain.management.forced.deployment')} @@ -315,7 +318,7 @@ const Home = () => { className="p-0" onClick={() => handleDownloadClick(domain)} > - 下载 + {t('download')} @@ -325,24 +328,24 @@ const Home = () => { - 删除域名 + {t('domain.delete')} - 确定要删除域名吗? + {t('domain.management.delete.confirm')} - 取消 + {t('cancel')} { handleDeleteClick(domain.id); }} > - 确认 + {t('confirm')} @@ -354,7 +357,7 @@ const Home = () => { className="p-0" onClick={() => handleEditClick(domain.id)} > - 编辑 + {t('edit')} )} diff --git a/ui/src/pages/history/History.tsx b/ui/src/pages/history/History.tsx index 3db4db4..94dc756 100644 --- a/ui/src/pages/history/History.tsx +++ b/ui/src/pages/history/History.tsx @@ -17,11 +17,13 @@ import { list } from "@/repository/deployment"; import { Smile } from "lucide-react"; import { useEffect, useState } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; +import { useTranslation } from "react-i18next"; const History = () => { const navigate = useNavigate(); const [deployments, setDeployments] = useState(); const [searchParams] = useSearchParams(); + const { t } = useTranslation(); const domain = searchParams.get("domain"); useEffect(() => { @@ -38,11 +40,11 @@ const History = () => { return ( -
部署历史
+
{t('deployment.log.name')}
{!deployments?.length ? ( <> - 暂无数据 + {t('no.data')}
@@ -50,7 +52,7 @@ const History = () => {
{" "} - 你暂未创建任何部署,请先添加域名进行部署吧! + {t('deployment.log.empty')}
@@ -59,7 +61,7 @@ const History = () => { navigate("/"); }} > - 添加域名 + {t('domain.add')}
@@ -68,16 +70,16 @@ const History = () => { ) : ( <>
-
域名
+
{t('domain')}
-
状态
-
阶段
-
最近执行时间
+
{t('deployment.log.status')}
+
{t('deployment.log.stage')}
+
{t('deployment.log.last.execution.time')}
-
操作
+
{t('operation')}
- 部署历史 + {t('deployment.log.name')}
{deployments?.map((deployment) => ( @@ -104,14 +106,14 @@ const History = () => { {deployment.expand.domain?.domain}-{deployment.id} - 部署详情 + {t('deployment.log.detail')}
diff --git a/ui/src/pages/login/Login.tsx b/ui/src/pages/login/Login.tsx index d023a9b..9df8aa3 100644 --- a/ui/src/pages/login/Login.tsx +++ b/ui/src/pages/login/Login.tsx @@ -1,3 +1,8 @@ +import { useForm } from "react-hook-form"; +import { useNavigate } from "react-router-dom"; +import { z } from "zod"; +import { useTranslation } from 'react-i18next' + import { Button } from "@/components/ui/button"; import { Form, @@ -11,20 +16,19 @@ import { Input } from "@/components/ui/input"; import { getErrMessage } from "@/lib/error"; import { getPb } from "@/repository/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useForm } from "react-hook-form"; -import { useNavigate } from "react-router-dom"; -import { z } from "zod"; const formSchema = z.object({ username: z.string().email({ - message: "请输入正确的邮箱地址", + message: "login.username.no.empty.message", }), password: z.string().min(10, { - message: "密码至少10个字符", + message: "login.password.length.message", }), }); const Login = () => { + const { t } = useTranslation() + const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { @@ -61,7 +65,7 @@ const Login = () => { name="username" render={({ field }) => ( - 用户名 + {t('username')} @@ -76,7 +80,7 @@ const Login = () => { name="password" render={({ field }) => ( - 密码 + {t('password')} @@ -86,7 +90,7 @@ const Login = () => { )} />
- +
diff --git a/ui/src/pages/setting/Account.tsx b/ui/src/pages/setting/Account.tsx index 65e6e62..c839f70 100644 --- a/ui/src/pages/setting/Account.tsx +++ b/ui/src/pages/setting/Account.tsx @@ -15,16 +15,18 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useState } from "react"; import { useForm } from "react-hook-form"; import { useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; import { z } from "zod"; const formSchema = z.object({ - email: z.string().email("请输入正确的邮箱"), + email: z.string().email("setting.account.email.valid.message"), }); const Account = () => { const { toast } = useToast(); const navigate = useNavigate(); + const { t } = useTranslation(); const [changed, setChanged] = useState(false); @@ -43,8 +45,8 @@ const Account = () => { getPb().authStore.clear(); toast({ - title: "修改账户邮箱功", - description: "请重新登录", + title: t("setting.account.email.change.succeed"), + description: t("setting.account.log.back.in"), }); setTimeout(() => { navigate("/login"); @@ -52,7 +54,7 @@ const Account = () => { } catch (e) { const message = getErrMessage(e); toast({ - title: "修改账户邮箱失败", + title: t("setting.account.email.change.failed"), description: message, variant: "destructive", }); @@ -72,10 +74,10 @@ const Account = () => { name="email" render={({ field }) => ( - 邮箱 + {t('email')} { @@ -92,10 +94,10 @@ const Account = () => {
{changed ? ( - + ) : ( )}
diff --git a/ui/src/pages/setting/Notify.tsx b/ui/src/pages/setting/Notify.tsx index ecdec18..a727611 100644 --- a/ui/src/pages/setting/Notify.tsx +++ b/ui/src/pages/setting/Notify.tsx @@ -9,15 +9,18 @@ import { AccordionTrigger, } from "@/components/ui/accordion"; import { NotifyProvider } from "@/providers/notify"; +import { useTranslation } from "react-i18next"; const Notify = () => { + const { t } = useTranslation(); + return ( <>
- 模板 + {t('template')} diff --git a/ui/src/pages/setting/Password.tsx b/ui/src/pages/setting/Password.tsx index 8733a5c..c72b15c 100644 --- a/ui/src/pages/setting/Password.tsx +++ b/ui/src/pages/setting/Password.tsx @@ -14,29 +14,31 @@ import { getPb } from "@/repository/api"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import { useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; import { z } from "zod"; const formSchema = z .object({ oldPassword: z.string().min(10, { - message: "密码至少10个字符", + message: "setting.password.length.message", }), newPassword: z.string().min(10, { - message: "密码至少10个字符", + message: "setting.password.length.message", }), confirmPassword: z.string().min(10, { - message: "密码至少10个字符", + message: "setting.password.length.message", }), }) .refine((data) => data.newPassword === data.confirmPassword, { - message: "两次密码不一致", + message: "setting.password.not.match", path: ["confirmPassword"], }); const Password = () => { const { toast } = useToast(); const navigate = useNavigate(); + const { t } = useTranslation(); const form = useForm>({ resolver: zodResolver(formSchema), @@ -66,8 +68,8 @@ const Password = () => { getPb().authStore.clear(); toast({ - title: "修改密码成功", - description: "请重新登录", + title: t('setting.password.change.succeed'), + description: t("setting.account.log.back.in"), }); setTimeout(() => { navigate("/login"); @@ -75,7 +77,7 @@ const Password = () => { } catch (e) { const message = getErrMessage(e); toast({ - title: "修改密码失败", + title: t('setting.password.change.failed'), description: message, variant: "destructive", }); @@ -95,9 +97,9 @@ const Password = () => { name="oldPassword" render={({ field }) => ( - 当前密码 + {t('setting.password.current.password')} - + @@ -110,7 +112,7 @@ const Password = () => { name="newPassword" render={({ field }) => ( - 新密码 + {t('setting.password.new.password')} { name="confirmPassword" render={({ field }) => ( - 确认密码 + {t('setting.password.confirm.password')} { )} />
- +
From b3f1e1e444f18a93d68107112fb4f694d213ddd5 Mon Sep 17 00:00:00 2001 From: Elvis Liao Date: Thu, 26 Sep 2024 19:47:50 +0800 Subject: [PATCH 3/8] chore: import i18n --- ui/src/main.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/src/main.tsx b/ui/src/main.tsx index 92acf52..6039f41 100644 --- a/ui/src/main.tsx +++ b/ui/src/main.tsx @@ -4,6 +4,7 @@ import "./global.css"; import { RouterProvider } from "react-router-dom"; import { router } from "./router.tsx"; import { ThemeProvider } from "./components/ThemeProvider.tsx"; +import "@/i18n"; ReactDOM.createRoot(document.getElementById("root")!).render( From e820e5599bc289e910ed85a2a6f34708ea145bbf Mon Sep 17 00:00:00 2001 From: Elvis Liao Date: Fri, 27 Sep 2024 00:42:40 +0800 Subject: [PATCH 4/8] wip: i18n - Change locales json to one-dimensional format --- ui/src/components/certimate/EmailsEdit.tsx | 14 +- ui/src/components/certimate/Version.tsx | 5 +- ui/src/i18n/locales/en.json | 103 +++++++++++- ui/src/i18n/locales/zh.json | 172 +++++++++++---------- ui/src/pages/access/Access.tsx | 35 +++-- ui/src/pages/domains/Edit.tsx | 72 ++++----- 6 files changed, 259 insertions(+), 142 deletions(-) diff --git a/ui/src/components/certimate/EmailsEdit.tsx b/ui/src/components/certimate/EmailsEdit.tsx index d1d2fc9..6da10ea 100644 --- a/ui/src/components/certimate/EmailsEdit.tsx +++ b/ui/src/components/certimate/EmailsEdit.tsx @@ -10,6 +10,7 @@ import { import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; import { Form, FormControl, @@ -39,9 +40,10 @@ const EmailsEdit = ({ className, trigger }: EmailsEditProps) => { } = useConfig(); const [open, setOpen] = useState(false); + const { t } = useTranslation(); const formSchema = z.object({ - email: z.string().email(), + email: z.string().email("email.valid.message"), }); const form = useForm>({ @@ -54,7 +56,7 @@ const EmailsEdit = ({ className, trigger }: EmailsEditProps) => { const onSubmit = async (data: z.infer) => { if ((emails.content as EmailsSetting).emails.includes(data.email)) { form.setError("email", { - message: "邮箱已存在", + message: "email.already.exist", }); return; } @@ -100,7 +102,7 @@ const EmailsEdit = ({ className, trigger }: EmailsEditProps) => { - 添加邮箱 + {t('email.add')}
@@ -118,9 +120,9 @@ const EmailsEdit = ({ className, trigger }: EmailsEditProps) => { name="email" render={({ field }) => ( - 邮箱 + {t('email')} - + @@ -129,7 +131,7 @@ const EmailsEdit = ({ className, trigger }: EmailsEditProps) => { />
- +
diff --git a/ui/src/components/certimate/Version.tsx b/ui/src/components/certimate/Version.tsx index 47bfa7c..997ab11 100644 --- a/ui/src/components/certimate/Version.tsx +++ b/ui/src/components/certimate/Version.tsx @@ -1,9 +1,12 @@ import { BookOpen } from "lucide-react"; +import { useTranslation } from "react-i18next"; import { Separator } from "../ui/separator"; import { version } from "@/domain/version"; const Version = () => { + const { t } = useTranslation() + return (
@@ -14,7 +17,7 @@ const Version = () => { className="flex items-center" > -
文档
+
{t('document')}
部署历史 查看详情。", - "empty": "请添加域名开始部署证书吧。", - "expiry.date": "有效期限", - "expiry.date1": "有效期 {{date}} 天", - "expiry.date2": "{{date}} 到期", - "last.execution.time": "最近执行时间", - "last.execution.status": "最近执行状态", - "last.execution.stage": "最近执行阶段", - "enable": "是否启用", - "start.deploying": "立即部署", - "forced.deployment": "强行部署", - "delete.confirm": "确定要删除域名吗?", - "edit": { - "title": "编辑域名", - "domain.verify.tips": "域名", - "dns.verify.tips": "请选择DNS服务商授权配置", - "target.type.verify.tips": "请选择部署服务类型" - } - }, + "domain.not.empty.verify.message": "请输入域名", + "domain.management.name": "域名列表", + "domain.management.start.deploy.succeed.tips": "已发起部署,请稍后查看部署日志。", + "domain.management.execution.failed": "执行失败", + "domain.management.execution.failed.tips": "执行失败,请在 <1>部署历史 查看详情。", + "domain.management.empty": "请添加域名开始部署证书吧。", + "domain.management.expiry.date": "有效期限", + "domain.management.expiry.date1": "有效期 {{date}} 天", + "domain.management.expiry.date2": "{{date}} 到期", + "domain.management.last.execution.time": "最近执行时间", + "domain.management.last.execution.status": "最近执行状态", + "domain.management.last.execution.stage": "最近执行阶段", + "domain.management.enable": "是否启用", + "domain.management.start.deploying": "立即部署", + "domain.management.forced.deployment": "强行部署", + "domain.management.delete.confirm": "确定要删除域名吗?", + "domain.management.edit.title": "编辑域名", + "domain.management.edit.dns.access.label": "DNS 服务商授权配置", + "domain.management.edit.dns.access.not.empty.message": "请选择DNS服务商授权配置", + "domain.management.edit.access.label": "服务商授权配置", + "domain.management.edit.access.not.empty.message": "请选择授权配置", + "domain.management.edit.target.type": "部署服务类型", + "domain.management.edit.target.type.not.empty.message": "请选择部署服务类型", + "domain.management.edit.succeed.tips": "域名编辑成功", + "domain.management.edit.target.access": "部署服务商授权配置", + "domain.management.edit.target.access.content.label": "服务商授权配置", + "domain.management.edit.target.access.not.empty.message": "请选择授权配置", + "domain.management.edit.target.access.verify.msg": "部署授权和部署授权组至少选一个", + "domain.management.edit.group.label": "部署配置组(用于将一个域名证书部署到多个 ssh 主机)", + "domain.management.edit.group.not.empty.message": "请选择分组", + "domain.management.edit.email.not.empty.message": "请选择邮箱", + "domain.management.edit.email.description": "(申请证书需要提供邮箱)", + "domain.management.edit.variables.placeholder": "可在SSH部署中使用,形如:\nkey=val;\nkey2=val2;", + "domain.management.edit.dns.placeholder": "自定义域名服务器,多个用分号隔开,如:\n8.8.8.8;\n8.8.4.4;", + "domain.management.add.succeed.tips": "域名添加成功", + "email.add": "添加邮箱", + "email.list": "邮箱列表", + "email.valid.message": "请输入正确的邮箱地址", + "email.already.exist": "邮箱已存在", + "email.not.empty.message": "请输入邮箱", "setting.notify.menu": "消息推送", "setting.submit": "确认修改", - "setting.account.email": { - "valid.message": "请输入正确的邮箱地址", - "placeholder": "请输入邮箱", - "change.succeed": "修改账户邮箱成功", - "change.failed": "修改账户邮箱失败" - }, + "setting.account.email.valid.message": "请输入正确的邮箱地址", + "setting.account.email.placeholder": "请输入邮箱", + "setting.account.email.change.succeed": "修改账户邮箱成功", + "setting.account.email.change.failed": "修改账户邮箱失败", "setting.account.log.back.in": "请重新登录", - "setting.password": { - "length.message": "密码至少10个字符", - "not.match": "两次密码不一致", - "change.succeed": "修改密码成功", - "change.failed": "修改密码失败", - "current.password": "当前密码", - "new.password": "新密码", - "confirm.password": "确认密码" - }, - "setting.notify": { - "template": { - "save.succeed": "通知模板保存成功", - "variables.tips": { - "title": "可选的变量, COUNT:即将过期张数", - "content": "可选的变量, COUNT:即将过期张数,DOMAINS:域名列表" - } - }, - "config": { - "enable": "是否启用", - "save.succeed": "配置保存成功", - "save.failed": "配置保存失败", - "save.failed.url.not.valid": "Url格式不正确" - } - }, - "deploy.progress": { - "check": "检查", - "apply": "获取", - "deploy": "部署" - } + "setting.password.length.message": "密码至少10个字符", + "setting.password.not.match": "两次密码不一致", + "setting.password.change.succeed": "修改密码成功", + "setting.password.change.failed": "修改密码失败", + "setting.password.current.password": "当前密码", + "setting.password.new.password": "新密码", + "setting.password.confirm.password": "确认密码", + "setting.notify.template.save.succeed": "通知模板保存成功", + "setting.notify.template.variables.tips.title": "可选的变量, COUNT:即将过期张数", + "setting.notify.template.variables.tips.content": "可选的变量, COUNT:即将过期张数,DOMAINS:域名列表", + "setting.notify.config.enable": "是否启用", + "setting.notify.config.save.succeed": "配置保存成功", + "setting.notify.config.save.failed": "配置保存失败", + "setting.notify.config.save.failed.url.not.valid": "Url格式不正确", + "deploy.progress.check": "检查", + "deploy.progress.apply": "获取", + "deploy.progress.deploy": "部署", + "access.management": "授权管理", + "access.add": "添加授权", + "access.list": "授权列表", + "access.type": "服务商", + "access.empty": "请添加授权开始部署证书吧。", + "access.group.management": "授权组管理", + "access.group.add": "添加授权组" } \ No newline at end of file diff --git a/ui/src/pages/access/Access.tsx b/ui/src/pages/access/Access.tsx index cbeb267..3f955f2 100644 --- a/ui/src/pages/access/Access.tsx +++ b/ui/src/pages/access/Access.tsx @@ -9,6 +9,7 @@ import { Access as AccessType, accessTypeMap } from "@/domain/access"; import { convertZulu2Beijing } from "@/lib/time"; import { useConfig } from "@/providers/config"; import { remove } from "@/repository/access"; +import { t } from "i18next"; import { Key } from "lucide-react"; import { useLocation, useNavigate } from "react-router-dom"; @@ -46,11 +47,11 @@ const Access = () => { return (
-
授权管理
+
{t('access.management')}
{tab != "access_group" ? ( - 添加授权} op="add" /> + {t('access.add')}} op="add" /> ) : ( - 添加授权组} /> + {t('access.group.add')}} /> )}
@@ -66,7 +67,7 @@ const Access = () => { handleTabItemClick("access"); }} > - 授权管理 + {t('access.management')} { handleTabItemClick("access_group"); }} > - 授权组管理 + {t('access.group.management')} @@ -85,10 +86,10 @@ const Access = () => {
- 请添加授权开始部署证书吧。 + {t('access.empty')}
添加授权} + trigger={} op="add" className="mt-3" /> @@ -96,15 +97,15 @@ const Access = () => { ) : ( <>
-
名称
-
服务商
+
{t('name')}
+
{t('access.type')}
-
创建时间
-
更新时间
-
操作
+
{t('create.time')}
+
{t('update.time')}
+
{t('operation')}
- 授权列表 + {t('access.list')}
{accesses .filter((item) => { @@ -128,18 +129,18 @@ const Access = () => {
- 创建于{" "} + {t('created.in')}{" "} {access.created && convertZulu2Beijing(access.created)}
- 更新于{" "} + {t('updated.in')}{" "} {access.updated && convertZulu2Beijing(access.updated)}
- 编辑 + {t('edit')} } op="edit" @@ -153,7 +154,7 @@ const Access = () => { handleDelete(access); }} > - 删除 + {t('delete')}
diff --git a/ui/src/pages/domains/Edit.tsx b/ui/src/pages/domains/Edit.tsx index 3a4c423..771b96b 100644 --- a/ui/src/pages/domains/Edit.tsx +++ b/ui/src/pages/domains/Edit.tsx @@ -71,15 +71,15 @@ const Edit = () => { const formSchema = z.object({ id: z.string().optional(), domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { - message: t('domain.management.edit.domain.verify.tips'), + message: 'domain.not.empty.verify.message', }), - email: z.string().email().optional(), + email: z.string().email('email.valid.message').optional(), access: z.string().regex(/^[a-zA-Z0-9]+$/, { - message: t('domain.management.edit.dns.verify.tips'), + message: 'domain.management.edit.dns.access.not.empty.message', }), targetAccess: z.string().optional(), targetType: z.string().regex(/^[a-zA-Z0-9-]+$/, { - message: t('domain.management.edit.target.type.verify.tips'), + message: 'domain.management.edit.target.type.not.empty.message', }), variables: z.string().optional(), group: z.string().optional(), @@ -140,11 +140,11 @@ const Edit = () => { if (group == "" && targetAccess == "") { form.setError("group", { type: "manual", - message: "部署授权和部署授权组至少选一个", + message: 'domain.management.edit.target.access.verify.msg', }); form.setError("targetAccess", { type: "manual", - message: "部署授权和部署授权组至少选一个", + message: 'domain.management.edit.target.access.verify.msg', }); return; } @@ -164,13 +164,13 @@ const Edit = () => { try { await save(req); - let description = "域名编辑成功"; + let description = t('domain.management.edit.succeed.tips'); if (req.id == "") { - description = "域名添加成功"; + description = t('domain.management.add.succeed.tips'); } toast({ - title: "成功", + title: t('succeed'), description, }); navigate("/domains"); @@ -195,7 +195,7 @@ const Edit = () => {
- {domain?.id ? "编辑" : "新增"}域名 + {domain?.id ? t('domain.edit') : t('domain.add')}
@@ -208,7 +208,7 @@ const Edit = () => { setTab("base"); }} > - 基础设置 + {t('basic.setting')}
{ setTab("advance"); }} > - 高级设置 + {t('advanced.setting')}
@@ -234,9 +234,9 @@ const Edit = () => { name="domain" render={({ field }) => (
} /> @@ -268,11 +268,11 @@ const Edit = () => { }} > - + - 邮箱列表 + {t('email.list')} {(emails.content as EmailsSetting).emails.map( (item) => ( @@ -295,12 +295,12 @@ const Edit = () => { render={({ field }) => (
} op="add" @@ -315,11 +315,11 @@ const Edit = () => { }} > - + - 服务商授权配置 + {t('domain.management.edit.access.label')} {accesses .filter((item) => item.usage != "deploy") .map((item) => ( @@ -351,7 +351,7 @@ const Edit = () => { name="targetType" render={({ field }) => (