This is an automated email from the ASF dual-hosted git repository.

hsluoyz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/casbin-gateway.git


The following commit(s) were added to refs/heads/master by this push:
     new cac5699  fix(web): switch language without reloading the page
cac5699 is described below

commit cac56995ead0bb7885866b08d65dbde372ff28bf
Author: Yang Luo <[email protected]>
AuthorDate: Thu Aug 20 09:36:08 2026 +0800

    fix(web): switch language without reloading the page
---
 web/src/App.tsx                              |  8 ++-
 web/src/Setting.tsx                          |  5 --
 web/src/components/DataTable.tsx             |  2 +-
 web/src/components/NodeTable.tsx             |  3 +-
 web/src/components/charts/ChartCards.tsx     |  5 +-
 web/src/components/layout/Footer.tsx         |  4 +-
 web/src/components/layout/LanguageSelect.tsx |  2 +-
 web/src/components/rules/IpRuleTable.tsx     |  2 +-
 web/src/components/rules/RowActions.tsx      |  7 +--
 web/src/locales/en/data.json                 | 25 +++++++++
 web/src/locales/zh/data.json                 | 79 ++++++++++++++++++----------
 web/src/pages/AccountPage.tsx                |  4 +-
 web/src/pages/AuthCallback.tsx               |  7 +--
 web/src/pages/CertEditPage.tsx               |  8 +--
 web/src/pages/CertListPage.tsx               | 22 ++++----
 web/src/pages/ChannelListPage.tsx            |  2 +-
 web/src/pages/NodeEditPage.tsx               |  8 +--
 web/src/pages/NodeListPage.tsx               | 14 ++---
 web/src/pages/RecordEditPage.tsx             |  8 +--
 web/src/pages/RecordListPage.tsx             | 16 +++---
 web/src/pages/RuleEditPage.tsx               | 14 ++---
 web/src/pages/RuleListPage.tsx               | 12 ++---
 web/src/pages/SigninPage.tsx                 |  4 +-
 web/src/pages/SiteEditPage.tsx               |  8 +--
 web/src/pages/SiteListPage.tsx               | 16 +++---
 25 files changed, 170 insertions(+), 115 deletions(-)

diff --git a/web/src/App.tsx b/web/src/App.tsx
index 32bacca..17d42fc 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -14,6 +14,8 @@
 
 import * as React from "react";
 import {Navigate, Route, Routes, useLocation} from "react-router-dom";
+import {useTranslation} from "react-i18next";
+import i18next from "i18next";
 
 import * as AccountBackend from "@/backend/AccountBackend";
 import * as Conf from "@/Conf";
@@ -55,6 +57,8 @@ Setting.initCasdoorSdk(Conf.AuthConfig);
 const collapsedKey = "sidebarCollapsed";
 
 export default function App() {
+  // Pages call i18next.t() directly, so re-render the whole tree on a 
language change.
+  useTranslation();
   // undefined while the account request is in flight, null when signed out.
   const [account, setAccount] = React.useState<Account | null | 
undefined>(undefined);
   const [collapsed, setCollapsed] = React.useState(
@@ -104,10 +108,10 @@ export default function App() {
     AccountBackend.signout().then(res => {
       if (res.status === "ok") {
         setAccount(null);
-        Setting.showMessage("success", "Successfully signed out, redirected to 
homepage");
+        Setting.showMessage("success", i18next.t("general:Signed out 
successfully"));
         Setting.goToLink("/");
       } else {
-        Setting.showMessage("error", `Signout failed: ${res.msg}`);
+        Setting.showMessage("error", `${i18next.t("general:Failed to sign 
out")}: ${res.msg}`);
       }
     });
   };
diff --git a/web/src/Setting.tsx b/web/src/Setting.tsx
index 643eff4..1cf46fd 100644
--- a/web/src/Setting.tsx
+++ b/web/src/Setting.tsx
@@ -198,11 +198,6 @@ export function setLanguage(language: string) {
   i18next.changeLanguage(language);
 }
 
-export function changeLanguage(language: string) {
-  setLanguage(language);
-  window.location.reload();
-}
-
 export function isResponseDenied(data: {msg?: string}) {
   return data.msg === "Unauthorized operation" || data.msg === "未授权的操作";
 }
diff --git a/web/src/components/DataTable.tsx b/web/src/components/DataTable.tsx
index 76a13ed..2759933 100644
--- a/web/src/components/DataTable.tsx
+++ b/web/src/components/DataTable.tsx
@@ -209,7 +209,7 @@ export function DataTable<T>({
           {pageRows.length === 0 ? (
             <TableRow className="hover:bg-transparent">
               <TableCell colSpan={columnCount} className="h-24 text-center 
text-muted-foreground">
-                {loading ? <Spinner className="mx-auto" /> : (emptyText ?? "No 
data")}
+                {loading ? <Spinner className="mx-auto" /> : (emptyText ?? 
i18next.t("general:No data"))}
               </TableCell>
             </TableRow>
           ) : (
diff --git a/web/src/components/NodeTable.tsx b/web/src/components/NodeTable.tsx
index ac2567d..08df2b7 100644
--- a/web/src/components/NodeTable.tsx
+++ b/web/src/components/NodeTable.tsx
@@ -14,6 +14,7 @@
 
 import * as React from "react";
 import {CircleCheck, CircleMinus, RefreshCw} from "lucide-react";
+import i18next from "i18next";
 
 import * as Setting from "@/Setting";
 import {DataTable, type Column} from "@/components/DataTable";
@@ -107,7 +108,7 @@ export function NodeTable({
       render: (text: string, _record, index) => (
         <Select value={text || undefined} onValueChange={value => 
updateField(index, "name", value)}>
           <SelectTrigger>
-            <SelectValue placeholder="Node" />
+            <SelectValue placeholder={i18next.t("site:Node")} />
           </SelectTrigger>
           <SelectContent>
             {availableNodes(text).map(node => (
diff --git a/web/src/components/charts/ChartCards.tsx 
b/web/src/components/charts/ChartCards.tsx
index 04e5107..b0d62c6 100644
--- a/web/src/components/charts/ChartCards.tsx
+++ b/web/src/components/charts/ChartCards.tsx
@@ -80,6 +80,7 @@ export function PieChartCard({
   title: string;
   data: {name: string; value: number}[] | null;
 }) {
+  const other = i18next.t("general:Other");
   // Rank the slices, keep the five that get their own color, and sum the tail
   // into a single neutral "Other" slice so no color is ever reused.
   const {slices, config} = React.useMemo(() => {
@@ -92,7 +93,7 @@ export function PieChartCard({
 
     if (tail.length > 0) {
       head.push({
-        name: i18next.t("general:Other"),
+        name: other,
         value: tail.reduce((sum, item) => sum + item.value, 0),
         fill: OTHER_COLOR,
       });
@@ -104,7 +105,7 @@ export function PieChartCard({
     });
 
     return {slices: head, config: chartConfig};
-  }, [data]);
+  }, [data, other]);
 
   return (
     <Card className="h-full">
diff --git a/web/src/components/layout/Footer.tsx 
b/web/src/components/layout/Footer.tsx
index 18bcab3..f79434b 100644
--- a/web/src/components/layout/Footer.tsx
+++ b/web/src/components/layout/Footer.tsx
@@ -12,13 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import i18next from "i18next";
+
 import * as Setting from "@/Setting";
 
 export function Footer() {
   return (
     <footer className="mt-auto border-t py-4">
       <div className="flex items-center justify-center gap-2 text-sm 
text-muted-foreground">
-        Powered by
+        {i18next.t("general:Powered by")}
         <a
           target="_blank"
           rel="noreferrer"
diff --git a/web/src/components/layout/LanguageSelect.tsx 
b/web/src/components/layout/LanguageSelect.tsx
index e2a45ea..c79c2a1 100644
--- a/web/src/components/layout/LanguageSelect.tsx
+++ b/web/src/components/layout/LanguageSelect.tsx
@@ -33,7 +33,7 @@ export function LanguageSelect() {
       </DropdownMenuTrigger>
       <DropdownMenuContent align="end">
         {Setting.Countries.map(country => (
-          <DropdownMenuItem key={country.key} onClick={() => 
Setting.changeLanguage(country.key)}>
+          <DropdownMenuItem key={country.key} onClick={() => 
Setting.setLanguage(country.key)}>
             <img
               
src={`${Setting.StaticBaseUrl}/flag-icons/${country.country}.svg`}
               alt={country.alt}
diff --git a/web/src/components/rules/IpRuleTable.tsx 
b/web/src/components/rules/IpRuleTable.tsx
index 45d7ebc..9a66f75 100644
--- a/web/src/components/rules/IpRuleTable.tsx
+++ b/web/src/components/rules/IpRuleTable.tsx
@@ -85,7 +85,7 @@ export function IpRuleTable({title, table, onUpdateTable}: 
ExpressionTableProps)
       // split apart for editing and joined back on every change.
       render: (text: string, _record, index) => (
         <TagsInput
-          placeholder="Input IP Addresses"
+          placeholder={i18next.t("general:Input IP Addresses")}
           value={text ? text.split(",") : []}
           onChange={value => updateField(index, "value", value.map(item => 
item.trim()).join(","))}
         />
diff --git a/web/src/components/rules/RowActions.tsx 
b/web/src/components/rules/RowActions.tsx
index 5ac684e..377ed55 100644
--- a/web/src/components/rules/RowActions.tsx
+++ b/web/src/components/rules/RowActions.tsx
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 import {ArrowDown, ArrowUp, Trash2} from "lucide-react";
+import i18next from "i18next";
 
 import {Button} from "@/components/ui/button";
 import {Tooltip} from "@/components/ui/tooltip";
@@ -33,17 +34,17 @@ export function RowActions({
 }) {
   return (
     <div className="flex items-center gap-1">
-      <Tooltip title="Up">
+      <Tooltip title={i18next.t("general:Up")}>
         <Button variant="outline" size="icon-sm" disabled={index === 0} 
onClick={onUp}>
           <ArrowUp />
         </Button>
       </Tooltip>
-      <Tooltip title="Down">
+      <Tooltip title={i18next.t("general:Down")}>
         <Button variant="outline" size="icon-sm" disabled={index === length - 
1} onClick={onDown}>
           <ArrowDown />
         </Button>
       </Tooltip>
-      <Tooltip title="Delete">
+      <Tooltip title={i18next.t("general:Delete")}>
         <Button variant="outline" size="icon-sm" onClick={onDelete}>
           <Trash2 />
         </Button>
diff --git a/web/src/locales/en/data.json b/web/src/locales/en/data.json
index 42de594..c449adc 100644
--- a/web/src/locales/en/data.json
+++ b/web/src/locales/en/data.json
@@ -92,10 +92,12 @@
     "Copy private key": "Copy private key",
     "Crypto algorithm": "Crypto algorithm",
     "Domain expire": "Domain expire",
+    "Domain expire refreshed successfully": "Domain expire refreshed 
successfully",
     "Download certificate": "Download certificate",
     "Download private key": "Download private key",
     "Edit Cert": "Edit Cert",
     "Expire time": "Expire time",
+    "Failed to refresh domain expire": "Failed to refresh domain expire",
     "Private key": "Private key",
     "Private key copied to clipboard successfully": "Private key copied to 
clipboard successfully",
     "Provider": "Provider",
@@ -134,6 +136,7 @@
   "general": {
     "Action": "Action",
     "Add": "Add",
+    "Added successfully": "Added successfully",
     "Advanced": "Advanced",
     "At Any Time": "At Any Time",
     "Back Home": "Back Home",
@@ -147,9 +150,16 @@
     "CreatedTime": "CreatedTime",
     "Dashboard": "Dashboard",
     "Delete": "Delete",
+    "Deleted successfully": "Deleted successfully",
     "Display name": "Display name",
+    "Down": "Down",
     "Edit": "Edit",
     "Edit Record": "Edit Record",
+    "Failed to add": "Failed to add",
+    "Failed to delete": "Failed to delete",
+    "Failed to get data": "Failed to get data",
+    "Failed to save": "Failed to save",
+    "Failed to sign out": "Failed to sign out",
     "Gateway Analytics": "Gateway Analytics",
     "Go to writable demo site?": "Go to writable demo site?",
     "Half A Hour": "Half A Hour",
@@ -157,23 +167,33 @@
     "Host": "Host",
     "ID": "ID",
     "IP Address": "IP Address",
+    "Input IP Addresses": "Input IP Addresses",
+    "Logged in successfully": "Logged in successfully",
+    "Login Error": "Login Error",
     "Method": "Method",
     "Name": "Name",
     "No Upgrade": "No Upgrade",
+    "No data": "No data",
     "Nodes": "Nodes",
     "OK": "OK",
     "Other": "Other",
     "Owner": "Owner",
     "Password": "Password",
     "Path": "Path",
+    "Powered by": "Powered by",
     "Records": "Records",
     "Refresh": "Refresh",
+    "Request Count Over Time": "Request Count Over Time",
     "Restore": "Restore",
     "Rules": "Rules",
     "Save": "Save",
     "Self": "Self",
+    "Signed out successfully": "Signed out successfully",
+    "Signing in...": "Signing in...",
     "Sites": "Sites",
     "Sorry, you do not have permission to access this page or logged in status 
invalid.": "Sorry, you do not have permission to access this page or logged in 
status invalid.",
+    "Successfully saved": "Successfully saved",
+    "Sure to delete {name} ?": "Sure to delete {name} ?",
     "Tag": "Tag",
     "This is a read-only demo site!": "This is a read-only demo site!",
     "Toggle sidebar": "Toggle sidebar",
@@ -181,6 +201,7 @@
     "Top 10 User-Agents": "Top 10 User-Agents",
     "Total Request Count": "Total Request Count",
     "Unique IP Count": "Unique IP Count",
+    "Up": "Up",
     "Update time": "Update time",
     "Upgrade mode": "Upgrade mode",
     "User-Agent": "User-Agent",
@@ -200,13 +221,17 @@
     "Expressions": "Expressions",
     "IP List": "IP List",
     "IP Rate Limiting": "IP Rate Limiting",
+    "IPs": "IPs",
     "Logic": "Logic",
     "Operator": "Operator",
     "Rate": "Rate",
     "Reason": "Reason",
     "Rule": "Rule",
+    "Seclang": "Seclang",
     "Status code": "Status code",
     "Type": "Type",
+    "URL Paths": "URL Paths",
+    "User-Agents": "User-Agents",
     "Value": "Value",
     "Verbose mode": "Verbose mode",
     "and": "and",
diff --git a/web/src/locales/zh/data.json b/web/src/locales/zh/data.json
index a31db89..b961f07 100644
--- a/web/src/locales/zh/data.json
+++ b/web/src/locales/zh/data.json
@@ -84,7 +84,7 @@
   },
   "cert": {
     "Access key": "访问密钥",
-    "Access secret": "访问密钥",
+    "Access secret": "访问密钥 Secret",
     "Account": "账户",
     "Certificate": "证书",
     "Certificate copied to clipboard successfully": "证书成功复制到剪贴板",
@@ -92,10 +92,12 @@
     "Copy private key": "复制私钥",
     "Crypto algorithm": "加密算法",
     "Domain expire": "域名到期",
+    "Domain expire refreshed successfully": "域名到期时间刷新成功",
     "Download certificate": "下载证书",
     "Download private key": "下载私钥",
     "Edit Cert": "编辑证书",
     "Expire time": "到期时间",
+    "Failed to refresh domain expire": "刷新域名到期时间失败",
     "Private key": "私钥",
     "Private key copied to clipboard successfully": "私钥成功复制到剪贴板",
     "Provider": "提供者",
@@ -134,12 +136,13 @@
   "general": {
     "Action": "操作",
     "Add": "添加",
+    "Added successfully": "添加成功",
     "Advanced": "高级",
-    "At Any Time": "At Any Time",
+    "At Any Time": "任意时间",
     "Back Home": "返回首页",
     "Cancel": "取消",
     "Certs": "证书",
-    "Client IP": "Client IP",
+    "Client IP": "客户端 IP",
     "Client ip": "客户端IP",
     "Count": "计数",
     "Create time": "创建时间",
@@ -147,33 +150,50 @@
     "CreatedTime": "创建时间",
     "Dashboard": "数据看板",
     "Delete": "删除",
+    "Deleted successfully": "删除成功",
     "Display name": "显示名称",
+    "Down": "下移",
     "Edit": "编辑",
     "Edit Record": "编辑日志",
+    "Failed to add": "添加失败",
+    "Failed to delete": "删除失败",
+    "Failed to get data": "获取数据失败",
+    "Failed to save": "保存失败",
+    "Failed to sign out": "退出登录失败",
     "Gateway Analytics": "网关统计",
     "Go to writable demo site?": "跳转至可写演示站点?",
-    "Half A Hour": "Half A Hour",
+    "Half A Hour": "半小时",
     "Home": "首页",
     "Host": "主机",
     "ID": "ID",
     "IP Address": "IP地址",
+    "Input IP Addresses": "请输入 IP 地址",
+    "Logged in successfully": "登录成功",
+    "Login Error": "登录失败",
     "Method": "方法",
     "Name": "名称",
-    "No Upgrade": "No Upgrade",
-    "Nodes": "Nodes",
+    "No Upgrade": "不升级",
+    "No data": "暂无数据",
+    "Nodes": "节点",
     "OK": "确定",
     "Other": "其他",
     "Owner": "所有者",
     "Password": "密码",
     "Path": "路径",
+    "Powered by": "技术支持",
     "Records": "日志",
-    "Refresh": "Refresh",
+    "Refresh": "刷新",
+    "Request Count Over Time": "请求数趋势",
     "Restore": "复原",
     "Rules": "规则",
     "Save": "保存",
     "Self": "自己",
+    "Signed out successfully": "已退出登录",
+    "Signing in...": "正在登录...",
     "Sites": "站点",
     "Sorry, you do not have permission to access this page or logged in status 
invalid.": "抱歉,您无权访问该页面或登录状态无效。",
+    "Successfully saved": "保存成功",
+    "Sure to delete {name} ?": "确定删除 {name} ?",
     "Tag": "标签",
     "This is a read-only demo site!": "这是一个只读演示站点!",
     "Toggle sidebar": "折叠/展开侧边栏",
@@ -181,58 +201,63 @@
     "Top 10 User-Agents": "前10名 User-Agent",
     "Total Request Count": "总请求数",
     "Unique IP Count": "独立IP统计",
-    "Update time": "创建时间",
-    "Upgrade mode": "Upgrade mode",
+    "Up": "上移",
+    "Update time": "更新时间",
+    "Upgrade mode": "升级模式",
     "User-Agent": "User-Agent",
-    "{total} in total": "{total} in total"
+    "{total} in total": "共 {total} 条"
   },
   "node": {
-    "Edit Node": "Edit Node"
+    "Edit Node": "编辑节点"
   },
   "rule": {
     "Allow": "允许",
     "Block": "阻止",
     "Block Duration": "阻断时间",
-    "Captcha": "Captcha",
-    "Compound": "Compound",
+    "Captcha": "验证码",
+    "Compound": "组合",
     "Edit Rule": "编辑规则",
     "Expression": "表达式",
     "Expressions": "表达式",
     "IP List": "IP 列表",
     "IP Rate Limiting": "IP 速率限制",
-    "Logic": "Logic",
+    "IPs": "IP 列表",
+    "Logic": "逻辑",
     "Operator": "操作符",
     "Rate": "速率",
     "Reason": "原因",
-    "Rule": "Rule",
+    "Rule": "规则",
+    "Seclang": "Seclang",
     "Status code": "状态码",
     "Type": "类型",
+    "URL Paths": "URL 路径",
+    "User-Agents": "User-Agent 列表",
     "Value": "值",
     "Verbose mode": "详细模式",
-    "and": "and",
-    "begin": "begin",
+    "and": "且",
+    "begin": "以…开头",
     "contains": "包含",
     "does not contain": "不包含",
     "does not equal": "不等于",
     "equals": "等于",
     "is abroad": "在国外",
-    "is in": "包含",
-    "is not in": "不包含于",
-    "or": "or",
+    "is in": "属于",
+    "is not in": "不属于",
+    "or": "或",
     "regex match": "正则表达式匹配"
   },
   "site": {
-    "Alert interval": "Alert interval",
-    "Alert providers": "Alert providers",
-    "Alert try times": "Alert try times",
+    "Alert interval": "告警间隔",
+    "Alert providers": "告警提供商",
+    "Alert try times": "告警重试次数",
     "Casdoor app": "Casdoor应用",
     "Challenges": "挑战",
-    "Disable verbose": "Disable verbose",
+    "Disable verbose": "关闭详细日志",
     "Domain": "域名",
     "Edit Site": "编辑站点",
-    "Enable alert": "Enable alert",
+    "Enable alert": "启用告警",
     "Host": "主机",
-    "Hosts": "Hosts",
+    "Hosts": "主机列表",
     "Mode": "模式",
     "Need redirect": "需要重定向",
     "Node": "节点",
@@ -240,7 +265,7 @@
     "Other domains": "其他域名",
     "Port": "端口",
     "Public IP": "公网IP",
-    "Rules": "Rules",
+    "Rules": "规则",
     "SSL cert": "SSL证书",
     "Status": "状态",
     "The reverse proxy is not enabled": "反向代理未启用",
diff --git a/web/src/pages/AccountPage.tsx b/web/src/pages/AccountPage.tsx
index 6829dda..6b50ac8 100644
--- a/web/src/pages/AccountPage.tsx
+++ b/web/src/pages/AccountPage.tsx
@@ -46,7 +46,7 @@ export default function AccountPage({account}: {account: 
Account}) {
     AccountBackend.updateAccount({displayName: displayName, avatar: avatar})
       .then(res => {
         if (res.status === "ok") {
-          Setting.showMessage("success", "Successfully saved");
+          Setting.showMessage("success", i18next.t("general:Successfully 
saved"));
           window.location.reload();
         } else {
           Setting.showMessage("error", res.msg);
@@ -64,7 +64,7 @@ export default function AccountPage({account}: {account: 
Account}) {
     })
       .then(res => {
         if (res.status === "ok") {
-          Setting.showMessage("success", "Successfully saved");
+          Setting.showMessage("success", i18next.t("general:Successfully 
saved"));
           setPasswordOpen(false);
           setCurrentPassword("");
           setNewPassword("");
diff --git a/web/src/pages/AuthCallback.tsx b/web/src/pages/AuthCallback.tsx
index 8021ca7..c895def 100644
--- a/web/src/pages/AuthCallback.tsx
+++ b/web/src/pages/AuthCallback.tsx
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 import * as React from "react";
+import i18next from "i18next";
 
 import * as Setting from "@/Setting";
 import {Result} from "@/components/Result";
@@ -25,7 +26,7 @@ export default function AuthCallback() {
   React.useEffect(() => {
     Setting.signin().then(res => {
       if (res.status === "ok") {
-        Setting.showMessage("success", "Logged in successfully");
+        Setting.showMessage("success", i18next.t("general:Logged in 
successfully"));
         Setting.goToLink(sessionStorage.getItem("from") || "/");
       } else {
         setMessage(res.msg);
@@ -34,8 +35,8 @@ export default function AuthCallback() {
   }, []);
 
   if (message === null) {
-    return <PageSpinner tip="Signing in..." />;
+    return <PageSpinner tip={i18next.t("general:Signing in...")} />;
   }
 
-  return <Result status="error" title="Login Error" subTitle={message} />;
+  return <Result status="error" title={i18next.t("general:Login Error")} 
subTitle={message} />;
 }
diff --git a/web/src/pages/CertEditPage.tsx b/web/src/pages/CertEditPage.tsx
index 5e24fca..4c02512 100644
--- a/web/src/pages/CertEditPage.tsx
+++ b/web/src/pages/CertEditPage.tsx
@@ -45,7 +45,7 @@ export default function CertEditPage() {
       if (res.status === "ok") {
         setCert(res.data);
       } else {
-        Setting.showMessage("error", `Failed to get cert: ${res.msg}`);
+        Setting.showMessage("error", `${i18next.t("general:Failed to get 
data")}: ${res.msg}`);
       }
     });
   }, [certName, owner]);
@@ -62,13 +62,13 @@ export default function CertEditPage() {
     CertBackend.updateCert(cert.owner, certName, Setting.deepCopy(cert))
       .then(res => {
         if (res.status === "error") {
-          Setting.showMessage("error", `Failed to save: ${res.msg}`);
+          Setting.showMessage("error", `${i18next.t("general:Failed to 
save")}: ${res.msg}`);
         } else {
-          Setting.showMessage("success", "Successfully saved");
+          Setting.showMessage("success", i18next.t("general:Successfully 
saved"));
           navigate(`/certs/${cert.owner}/${cert.name}`);
         }
       })
-      .catch(error => Setting.showMessage("error", `failed to save: 
${error}`));
+      .catch(error => Setting.showMessage("error", 
`${i18next.t("general:Failed to save")}: ${error}`));
   };
 
   if (cert === null) {
diff --git a/web/src/pages/CertListPage.tsx b/web/src/pages/CertListPage.tsx
index 6e29695..2cc49f9 100644
--- a/web/src/pages/CertListPage.tsx
+++ b/web/src/pages/CertListPage.tsx
@@ -63,7 +63,7 @@ export default function CertListPage({account}: {account: 
Account}) {
           setPage(nextPage);
           setPageSize(nextPageSize);
         } else {
-          Setting.showMessage("error", `Failed to get certs: ${res.msg}`);
+          Setting.showMessage("error", `${i18next.t("general:Failed to get 
data")}: ${res.msg}`);
         }
       });
     },
@@ -80,39 +80,39 @@ export default function CertListPage({account}: {account: 
Account}) {
     CertBackend.addCert(newCert(account.name))
       .then(res => {
         if (res.status === "error") {
-          Setting.showMessage("error", `Failed to add: ${res.msg}`);
+          Setting.showMessage("error", `${i18next.t("general:Failed to add")}: 
${res.msg}`);
         } else {
-          Setting.showMessage("success", "Cert added successfully");
+          Setting.showMessage("success", i18next.t("general:Added 
successfully"));
           fetchCerts();
         }
       })
-      .catch(error => Setting.showMessage("error", `Cert failed to add: 
${error}`));
+      .catch(error => Setting.showMessage("error", 
`${i18next.t("general:Failed to add")}: ${error}`));
   };
 
   const deleteCert = (cert: Cert) => {
     CertBackend.deleteCert(cert)
       .then(res => {
         if (res.status === "error") {
-          Setting.showMessage("error", `Failed to delete: ${res.msg}`);
+          Setting.showMessage("error", `${i18next.t("general:Failed to 
delete")}: ${res.msg}`);
         } else {
-          Setting.showMessage("success", "Cert deleted successfully");
+          Setting.showMessage("success", i18next.t("general:Deleted 
successfully"));
           fetchCerts();
         }
       })
-      .catch(error => Setting.showMessage("error", `Cert failed to delete: 
${error}`));
+      .catch(error => Setting.showMessage("error", 
`${i18next.t("general:Failed to delete")}: ${error}`));
   };
 
   const refreshCert = (cert: Cert) => {
     CertBackend.refreshDomainExpire(cert.owner, cert.name)
       .then(res => {
         if (res.status === "error") {
-          Setting.showMessage("error", `Failed to refresh domain expire: 
${res.msg}`);
+          Setting.showMessage("error", `${i18next.t("cert:Failed to refresh 
domain expire")}: ${res.msg}`);
         } else {
-          Setting.showMessage("success", "Domain expire refresh successfully");
+          Setting.showMessage("success", i18next.t("cert:Domain expire 
refreshed successfully"));
           fetchCerts();
         }
       })
-      .catch(error => Setting.showMessage("error", `Domain expire failed to 
refresh: ${error}`));
+      .catch(error => Setting.showMessage("error", `${i18next.t("cert:Failed 
to refresh domain expire")}: ${error}`));
   };
 
   const copyButton = (text: string, prefix: RegExp, message: string) => (
@@ -224,7 +224,7 @@ export default function CertListPage({account}: {account: 
Account}) {
             {i18next.t("general:Edit")}
           </Button>
           <ConfirmButton
-            title={`Sure to delete cert: ${record.name} ?`}
+            title={i18next.t("general:Sure to delete {name} 
?").replace("{name}", record.name)}
             onConfirm={() => deleteCert(record)}
           >
             <Button size="sm" variant="destructive">
diff --git a/web/src/pages/ChannelListPage.tsx 
b/web/src/pages/ChannelListPage.tsx
index c6e9506..c19762a 100644
--- a/web/src/pages/ChannelListPage.tsx
+++ b/web/src/pages/ChannelListPage.tsx
@@ -221,7 +221,7 @@ export default function ChannelListPage({account}: 
{account: Account}) {
             {i18next.t("general:Edit")}
           </Button>
           <ConfirmButton
-            title={`${i18next.t("general:Delete")} channel "${record.name}"?`}
+            title={i18next.t("general:Sure to delete {name} 
?").replace("{name}", record.name)}
             onConfirm={() => deleteChannel(record)}
           >
             <Button size="sm" variant="destructive">
diff --git a/web/src/pages/NodeEditPage.tsx b/web/src/pages/NodeEditPage.tsx
index b3e8d54..34b25a0 100644
--- a/web/src/pages/NodeEditPage.tsx
+++ b/web/src/pages/NodeEditPage.tsx
@@ -44,7 +44,7 @@ export default function NodeEditPage() {
       if (res.status === "ok") {
         setNode(res.data);
       } else {
-        Setting.showMessage("error", `Failed to get node: ${res.msg}`);
+        Setting.showMessage("error", `${i18next.t("general:Failed to get 
data")}: ${res.msg}`);
       }
     });
   }, [nodeName, owner]);
@@ -61,13 +61,13 @@ export default function NodeEditPage() {
     NodeBackend.updateNode(node.owner, nodeName, Setting.deepCopy(node))
       .then(res => {
         if (res.status === "error") {
-          Setting.showMessage("error", `Failed to save: ${res.msg}`);
+          Setting.showMessage("error", `${i18next.t("general:Failed to 
save")}: ${res.msg}`);
         } else {
-          Setting.showMessage("success", "Successfully saved");
+          Setting.showMessage("success", i18next.t("general:Successfully 
saved"));
           navigate(`/nodes/${node.owner}/${node.name}`);
         }
       })
-      .catch(error => Setting.showMessage("error", `Failed to connect to 
server: ${error}`));
+      .catch(error => Setting.showMessage("error", 
`${i18next.t("general:Failed to save")}: ${error}`));
   };
 
   if (node === null) {
diff --git a/web/src/pages/NodeListPage.tsx b/web/src/pages/NodeListPage.tsx
index a8b1862..f67d687 100644
--- a/web/src/pages/NodeListPage.tsx
+++ b/web/src/pages/NodeListPage.tsx
@@ -65,26 +65,26 @@ export default function NodeListPage({account}: {account: 
Account}) {
     NodeBackend.addNode(newNode(account.name))
       .then(res => {
         if (res.status === "error") {
-          Setting.showMessage("error", `Failed to add: ${res.msg}`);
+          Setting.showMessage("error", `${i18next.t("general:Failed to add")}: 
${res.msg}`);
         } else {
-          Setting.showMessage("success", "Node added successfully");
+          Setting.showMessage("success", i18next.t("general:Added 
successfully"));
           fetchNodes();
         }
       })
-      .catch(error => Setting.showMessage("error", `Node failed to add: 
${error}`));
+      .catch(error => Setting.showMessage("error", 
`${i18next.t("general:Failed to add")}: ${error}`));
   };
 
   const deleteNode = (node: Node) => {
     NodeBackend.deleteNode(node)
       .then(res => {
         if (res.status === "error") {
-          Setting.showMessage("error", `Failed to delete: ${res.msg}`);
+          Setting.showMessage("error", `${i18next.t("general:Failed to 
delete")}: ${res.msg}`);
         } else {
-          Setting.showMessage("success", "Node deleted successfully");
+          Setting.showMessage("success", i18next.t("general:Deleted 
successfully"));
           fetchNodes();
         }
       })
-      .catch(error => Setting.showMessage("error", `Node failed to delete: 
${error}`));
+      .catch(error => Setting.showMessage("error", 
`${i18next.t("general:Failed to delete")}: ${error}`));
   };
 
   if (!authorized) {
@@ -156,7 +156,7 @@ export default function NodeListPage({account}: {account: 
Account}) {
             {i18next.t("general:Edit")}
           </Button>
           <ConfirmButton
-            title={`Sure to delete node: ${record.name} ?`}
+            title={i18next.t("general:Sure to delete {name} 
?").replace("{name}", record.name)}
             onConfirm={() => deleteNode(record)}
           >
             <Button size="sm" variant="destructive">
diff --git a/web/src/pages/RecordEditPage.tsx b/web/src/pages/RecordEditPage.tsx
index 7f59170..7456d31 100644
--- a/web/src/pages/RecordEditPage.tsx
+++ b/web/src/pages/RecordEditPage.tsx
@@ -35,7 +35,7 @@ export default function RecordEditPage() {
       if (res.status === "ok") {
         setRecord(res.data);
       } else {
-        Setting.showMessage("error", `Failed to get record: ${res.msg}`);
+        Setting.showMessage("error", `${i18next.t("general:Failed to get 
data")}: ${res.msg}`);
       }
     });
   }, [id, owner]);
@@ -52,13 +52,13 @@ export default function RecordEditPage() {
     RecordBackend.updateRecord(record.owner, id, Setting.deepCopy(record))
       .then(res => {
         if (res.status === "error") {
-          Setting.showMessage("error", `Failed to save: ${res.msg}`);
+          Setting.showMessage("error", `${i18next.t("general:Failed to 
save")}: ${res.msg}`);
         } else {
-          Setting.showMessage("success", "Successfully saved");
+          Setting.showMessage("success", i18next.t("general:Successfully 
saved"));
           navigate(`/records/${record.owner}/${record.id}`);
         }
       })
-      .catch(error => Setting.showMessage("error", `failed to save: 
${error}`));
+      .catch(error => Setting.showMessage("error", 
`${i18next.t("general:Failed to save")}: ${error}`));
   };
 
   if (record === null) {
diff --git a/web/src/pages/RecordListPage.tsx b/web/src/pages/RecordListPage.tsx
index 94f7b44..d04f426 100644
--- a/web/src/pages/RecordListPage.tsx
+++ b/web/src/pages/RecordListPage.tsx
@@ -42,7 +42,7 @@ export default function RecordListPage({account}: {account: 
Account}) {
           setPage(nextPage);
           setPageSize(nextPageSize);
         } else {
-          Setting.showMessage("error", `Failed to get records: ${res.msg}`);
+          Setting.showMessage("error", `${i18next.t("general:Failed to get 
data")}: ${res.msg}`);
         }
       });
     },
@@ -67,26 +67,26 @@ export default function RecordListPage({account}: {account: 
Account}) {
     })
       .then(res => {
         if (res.status === "error") {
-          Setting.showMessage("error", `Failed to add: ${res.msg}`);
+          Setting.showMessage("error", `${i18next.t("general:Failed to add")}: 
${res.msg}`);
         } else {
-          Setting.showMessage("success", "Record added successfully");
+          Setting.showMessage("success", i18next.t("general:Added 
successfully"));
           fetchRecords();
         }
       })
-      .catch(error => Setting.showMessage("error", `Record failed to add: 
${error}`));
+      .catch(error => Setting.showMessage("error", 
`${i18next.t("general:Failed to add")}: ${error}`));
   };
 
   const deleteRecord = (record: GatewayRecord) => {
     RecordBackend.deleteRecord(record)
       .then(res => {
         if (res.status === "error") {
-          Setting.showMessage("error", `Failed to delete: ${res.msg}`);
+          Setting.showMessage("error", `${i18next.t("general:Failed to 
delete")}: ${res.msg}`);
         } else {
-          Setting.showMessage("success", "Record deleted successfully");
+          Setting.showMessage("success", i18next.t("general:Deleted 
successfully"));
           fetchRecords();
         }
       })
-      .catch(error => Setting.showMessage("error", `Record failed to delete: 
${error}`));
+      .catch(error => Setting.showMessage("error", 
`${i18next.t("general:Failed to delete")}: ${error}`));
   };
 
   const columns: Column<GatewayRecord>[] = [
@@ -160,7 +160,7 @@ export default function RecordListPage({account}: {account: 
Account}) {
           <Button size="sm" onClick={() => 
navigate(`/records/${record.owner}/${record.id}`)}>
             {i18next.t("general:Edit")}
           </Button>
-          <ConfirmButton title="Sure to delete record?" onConfirm={() => 
deleteRecord(record)}>
+          <ConfirmButton title={i18next.t("general:Sure to delete {name} 
?").replace("{name}", String(record.id))} onConfirm={() => 
deleteRecord(record)}>
             <Button size="sm" variant="destructive">
               {i18next.t("general:Delete")}
             </Button>
diff --git a/web/src/pages/RuleEditPage.tsx b/web/src/pages/RuleEditPage.tsx
index 788d3fb..e6b9f36 100644
--- a/web/src/pages/RuleEditPage.tsx
+++ b/web/src/pages/RuleEditPage.tsx
@@ -49,7 +49,7 @@ export default function RuleEditPage() {
       if (res.status === "ok") {
         setRule(res.data);
       } else {
-        Setting.showMessage("error", `Failed to get rule: ${res.msg}`);
+        Setting.showMessage("error", `${i18next.t("general:Failed to get 
data")}: ${res.msg}`);
       }
     });
   }, [owner, ruleName]);
@@ -80,10 +80,10 @@ export default function RuleEditPage() {
 
     RuleBackend.updateRule(owner, ruleName, Setting.deepCopy(rule)).then(res 
=> {
       if (res.status !== "error") {
-        Setting.showMessage("success", "Rule updated successfully");
+        Setting.showMessage("success", i18next.t("general:Successfully 
saved"));
         navigate(`/rules/${rule.owner}/${rule.name}`);
       } else {
-        Setting.showMessage("error", `Rule failed to update: ${res.msg}`);
+        Setting.showMessage("error", `${i18next.t("general:Failed to save")}: 
${res.msg}`);
         getRule();
       }
     });
@@ -98,18 +98,18 @@ export default function RuleEditPage() {
   const renderExpressions = () => {
     switch (rule.type) {
     case "WAF":
-      return <WafRuleTable title="Seclang" table={rule.expressions} 
onUpdateTable={setExpressions} />;
+      return <WafRuleTable title={i18next.t("rule:Seclang")} 
table={rule.expressions} onUpdateTable={setExpressions} />;
     case "IP":
-      return <IpRuleTable title="IPs" table={rule.expressions} 
onUpdateTable={setExpressions} />;
+      return <IpRuleTable title={i18next.t("rule:IPs")} 
table={rule.expressions} onUpdateTable={setExpressions} />;
     case "User-Agent":
       return (
-        <UaRuleTable title="User-Agents" table={rule.expressions} 
onUpdateTable={setExpressions} />
+        <UaRuleTable title={i18next.t("rule:User-Agents")} 
table={rule.expressions} onUpdateTable={setExpressions} />
       );
     case "URL Path":
       return (
         <UaRuleTable
           kind="urlPath"
-          title="URL Paths"
+          title={i18next.t("rule:URL Paths")}
           table={rule.expressions}
           onUpdateTable={setExpressions}
         />
diff --git a/web/src/pages/RuleListPage.tsx b/web/src/pages/RuleListPage.tsx
index 144eacb..2dc28f2 100644
--- a/web/src/pages/RuleListPage.tsx
+++ b/web/src/pages/RuleListPage.tsx
@@ -59,7 +59,7 @@ export default function RuleListPage({account}: {account: 
Account}) {
           setPage(nextPage);
           setPageSize(nextPageSize);
         } else {
-          Setting.showMessage("error", `Failed to get rules: ${res.msg}`);
+          Setting.showMessage("error", `${i18next.t("general:Failed to get 
data")}: ${res.msg}`);
         }
       });
     },
@@ -74,9 +74,9 @@ export default function RuleListPage({account}: {account: 
Account}) {
   const addRule = () => {
     RuleBackend.addRule(newRule(account.name)).then(res => {
       if (res.status === "error") {
-        Setting.showMessage("error", `Failed to add: ${res.msg}`);
+        Setting.showMessage("error", `${i18next.t("general:Failed to add")}: 
${res.msg}`);
       } else {
-        Setting.showMessage("success", "Rule added successfully");
+        Setting.showMessage("success", i18next.t("general:Added 
successfully"));
         fetchRules();
       }
     });
@@ -85,9 +85,9 @@ export default function RuleListPage({account}: {account: 
Account}) {
   const deleteRule = (rule: Rule) => {
     RuleBackend.deleteRule(rule).then(res => {
       if (res.status === "error") {
-        Setting.showMessage("error", `Failed to delete: ${res.msg}`);
+        Setting.showMessage("error", `${i18next.t("general:Failed to 
delete")}: ${res.msg}`);
       } else {
-        Setting.showMessage("success", "Deleted successfully");
+        Setting.showMessage("success", i18next.t("general:Deleted 
successfully"));
         fetchRules();
       }
     });
@@ -182,7 +182,7 @@ export default function RuleListPage({account}: {account: 
Account}) {
             {i18next.t("general:Edit")}
           </Button>
           <ConfirmButton
-            title={`Sure to delete rule: ${record.name} ?`}
+            title={i18next.t("general:Sure to delete {name} 
?").replace("{name}", record.name)}
             onConfirm={() => deleteRule(record)}
           >
             <Button size="sm" variant="destructive">
diff --git a/web/src/pages/SigninPage.tsx b/web/src/pages/SigninPage.tsx
index 65062a8..a35069f 100644
--- a/web/src/pages/SigninPage.tsx
+++ b/web/src/pages/SigninPage.tsx
@@ -76,14 +76,14 @@ export default function SigninPage() {
   };
 
   if (loading) {
-    return <PageSpinner tip="Signing in..." />;
+    return <PageSpinner tip={i18next.t("general:Signing in...")} />;
   }
 
   if (!showSignin) {
     return (
       <Result
         status="warning"
-        title="Login Error"
+        title={i18next.t("general:Login Error")}
         subTitle={errorMessage || i18next.t("account:Sign in is unavailable")}
       />
     );
diff --git a/web/src/pages/SiteEditPage.tsx b/web/src/pages/SiteEditPage.tsx
index dcac8a6..e12725d 100644
--- a/web/src/pages/SiteEditPage.tsx
+++ b/web/src/pages/SiteEditPage.tsx
@@ -62,7 +62,7 @@ export default function SiteEditPage({account}: {account: 
Account}) {
       if (res.status === "ok") {
         setSite(res.data);
       } else {
-        Setting.showMessage("error", `Failed to get site: ${res.msg}`);
+        Setting.showMessage("error", `${i18next.t("general:Failed to get 
data")}: ${res.msg}`);
       }
     });
   }, [owner, siteName]);
@@ -113,13 +113,13 @@ export default function SiteEditPage({account}: {account: 
Account}) {
     SiteBackend.updateSite(site.owner, siteName, Setting.deepCopy(site))
       .then(res => {
         if (res.status === "error") {
-          Setting.showMessage("error", `Failed to save: ${res.msg}`);
+          Setting.showMessage("error", `${i18next.t("general:Failed to 
save")}: ${res.msg}`);
         } else {
-          Setting.showMessage("success", "Successfully saved");
+          Setting.showMessage("success", i18next.t("general:Successfully 
saved"));
           navigate(`/sites/${site.owner}/${site.name}`);
         }
       })
-      .catch(error => Setting.showMessage("error", `failed to save: 
${error}`));
+      .catch(error => Setting.showMessage("error", 
`${i18next.t("general:Failed to save")}: ${error}`));
   };
 
   if (site === null) {
diff --git a/web/src/pages/SiteListPage.tsx b/web/src/pages/SiteListPage.tsx
index 30e77e4..b20b84b 100644
--- a/web/src/pages/SiteListPage.tsx
+++ b/web/src/pages/SiteListPage.tsx
@@ -72,7 +72,7 @@ export default function SiteListPage({account}: {account: 
Account}) {
       if (res.status === "ok") {
         setData(res.data ?? []);
       } else {
-        Setting.showMessage("error", `Failed to get sites: ${res.msg}`);
+        Setting.showMessage("error", `${i18next.t("general:Failed to get 
data")}: ${res.msg}`);
       }
     });
   }, [account.name]);
@@ -94,26 +94,26 @@ export default function SiteListPage({account}: {account: 
Account}) {
     SiteBackend.addSite(site)
       .then(res => {
         if (res.status === "error") {
-          Setting.showMessage("error", `Failed to add: ${res.msg}`);
+          Setting.showMessage("error", `${i18next.t("general:Failed to add")}: 
${res.msg}`);
         } else {
-          Setting.showMessage("success", "Site added successfully");
+          Setting.showMessage("success", i18next.t("general:Added 
successfully"));
           fetchSites();
         }
       })
-      .catch(error => Setting.showMessage("error", `Site failed to add: 
${error}`));
+      .catch(error => Setting.showMessage("error", 
`${i18next.t("general:Failed to add")}: ${error}`));
   };
 
   const deleteSite = (site: Site) => {
     SiteBackend.deleteSite(site)
       .then(res => {
         if (res.status === "error") {
-          Setting.showMessage("error", `Failed to delete: ${res.msg}`);
+          Setting.showMessage("error", `${i18next.t("general:Failed to 
delete")}: ${res.msg}`);
         } else {
-          Setting.showMessage("success", "Site deleted successfully");
+          Setting.showMessage("success", i18next.t("general:Deleted 
successfully"));
           fetchSites();
         }
       })
-      .catch(error => Setting.showMessage("error", `Site failed to delete: 
${error}`));
+      .catch(error => Setting.showMessage("error", 
`${i18next.t("general:Failed to delete")}: ${error}`));
   };
 
   const columns: Column<Site>[] = [
@@ -290,7 +290,7 @@ export default function SiteListPage({account}: {account: 
Account}) {
             {i18next.t("general:Edit")}
           </Button>
           <ConfirmButton
-            title={`Sure to delete site: ${record.name} ?`}
+            title={i18next.t("general:Sure to delete {name} 
?").replace("{name}", record.name)}
             onConfirm={() => deleteSite(record)}
           >
             <Button size="sm" variant="destructive">


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to