diff --git a/ui/package-lock.json b/ui/package-lock.json index 12c292c6..18c5eb89 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 1c954e65..97e3db9e 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 00000000..01ec02e6 --- /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 00000000..eaed9c9a --- /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 00000000..26e64d27 --- /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 00000000..e26e6615 --- /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