feat: Add i18n

This commit is contained in:
elvis liao 2024-09-26 17:53:44 +08:00
parent a9fdceca6f
commit 85df8eb09d
6 changed files with 341 additions and 1 deletions

141
ui/package-lock.json generated
View File

@ -27,6 +27,9 @@
"@radix-ui/react-tooltip": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.2",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"i18next": "^23.15.1",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-http-backend": "^2.6.1",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"lucide-react": "^0.417.0", "lucide-react": "^0.417.0",
"moment": "^2.30.1", "moment": "^2.30.1",
@ -34,6 +37,7 @@
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-hook-form": "^7.52.1", "react-hook-form": "^7.52.1",
"react-i18next": "^15.0.2",
"react-router-dom": "^6.25.1", "react-router-dom": "^6.25.1",
"tailwind-merge": "^2.4.0", "tailwind-merge": "^2.4.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
@ -382,6 +386,17 @@
"@babel/core": "^7.0.0-0" "@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": { "node_modules/@babel/template": {
"version": "7.24.7", "version": "7.24.7",
"resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.24.7.tgz", "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", "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" "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": { "node_modules/cross-spawn": {
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz", "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz",
@ -3697,6 +3720,52 @@
"node": ">= 0.4" "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": { "node_modules/ignore": {
"version": "5.3.1", "version": "5.3.1",
"resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.1.tgz", "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.1.tgz",
@ -4112,6 +4181,25 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true "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": { "node_modules/node-releases": {
"version": "2.0.18", "version": "2.0.18",
"resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.18.tgz", "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.18.tgz",
@ -4553,6 +4641,27 @@
"react": "^16.8.0 || ^17 || ^18 || ^19" "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": { "node_modules/react-refresh": {
"version": "0.14.2", "version": "0.14.2",
"resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.14.2.tgz", "resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.14.2.tgz",
@ -4692,6 +4801,11 @@
"node": ">=8.10.0" "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": { "node_modules/resolve": {
"version": "1.22.8", "version": "1.22.8",
"resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.8.tgz", "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.8.tgz",
@ -5140,6 +5254,11 @@
"node": ">=8.0" "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": { "node_modules/ts-api-utils": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz", "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": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",

View File

@ -40,7 +40,11 @@
"tailwind-merge": "^2.4.0", "tailwind-merge": "^2.4.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"vaul": "^0.9.1", "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": { "devDependencies": {
"@types/node": "^22.0.0", "@types/node": "^22.0.0",

View File

@ -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 (
<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>
);
}

33
ui/src/i18n/index.ts Normal file
View File

@ -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;

View File

@ -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"
}

118
ui/src/i18n/locales/zh.json Normal file
View File

@ -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>部署历史</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": "部署"
}
}