From 6921211dcd34948e6c669aec681ab4114cb702be Mon Sep 17 00:00:00 2001 From: openclaw Date: Tue, 15 Sep 2026 07:07:03 +0800 Subject: [PATCH] feat: add Chinese and English UI language switch --- handlers/settings.go | 9 +++++++++ main.go | 1 + static/js/i18n.js | 21 +++++++++++++++++++++ templates/admin/categories.html | 6 +++--- templates/admin/change_password.html | 2 +- templates/admin/dashboard.html | 20 ++++++++++---------- templates/admin/login.html | 2 +- templates/admin/services.html | 6 +++--- templates/index.html | 2 +- 9 files changed, 50 insertions(+), 19 deletions(-) create mode 100644 static/js/i18n.js diff --git a/handlers/settings.go b/handlers/settings.go index aa1cb86..b4c9124 100644 --- a/handlers/settings.go +++ b/handlers/settings.go @@ -14,6 +14,15 @@ import ( ) // GetSettings 获取设置 +func GetLanguage(c *gin.Context) { + var setting models.Setting + lang := "en" + if err := database.DB.Where("key = ?", "language").First(&setting).Error; err == nil && setting.Value != "" { + lang = setting.Value + } + c.JSON(http.StatusOK, gin.H{"language": lang}) +} + func GetSettings(c *gin.Context) { var settings []models.Setting database.DB.Find(&settings) diff --git a/main.go b/main.go index 7ab464d..c2d51a9 100644 --- a/main.go +++ b/main.go @@ -36,6 +36,7 @@ func main() { ) r.GET("/", handlers.IndexHandler) + r.GET("/api/language", handlers.GetLanguage) r.GET("/admin/login", func(c *gin.Context) { c.HTML(http.StatusOK, "login.html", nil) diff --git a/static/js/i18n.js b/static/js/i18n.js new file mode 100644 index 0000000..8a5b819 --- /dev/null +++ b/static/js/i18n.js @@ -0,0 +1,21 @@ +(function(){ +const zh={ +"OVERVIEW":"概览","SERVICES":"服务","SYSTEM":"系统","Admin sign in":"管理员登录","Overview":"概览","Service directory and system status":"服务目录与系统状态","Services":"服务","Manage service entries":"管理服务条目","Categories":"分类","Organize your directory":"整理服务目录","Settings":"设置","Site, backup and MCP":"站点、备份与 MCP","Backup":"备份","Run cloud backup now":"立即执行云端备份","System status":"系统状态","Quick actions":"快捷操作","Open navigation":"打开导航","Sign out":"退出登录","Open site":"打开网站","Theme":"主题", +"Administration":"管理后台","Username":"用户名","Password":"密码","Sign in":"登录","Current password":"当前密码","New password":"新密码","Confirm new password":"确认新密码","Update password":"更新密码","For security, the initial password must be changed before continuing.":"为了安全,继续操作前必须修改初始密码。", +"Service directory":"服务目录","Search services, tags, descriptions…":"搜索服务、标签、描述…","All":"全部","Uncategorized":"未分类","No services found":"没有找到服务","Loading…":"加载中…","System settings":"系统设置","Site title":"站点标题","WebDAV URL":"WebDAV 地址","Daily backup at 03:00":"每天 03:00 自动备份","MCP Server":"MCP 服务","Enable MCP endpoint":"启用 MCP 接口","Copy":"复制","Copied":"已复制","Regenerate":"重新生成","Cloud backups":"云端备份","No cloud backups":"暂无云端备份","Restore":"恢复","Delete":"删除","Close":"关闭","Save changes":"保存更改","Save":"保存","Cancel":"取消", +"Manage entries in your service directory":"管理服务目录中的条目","Search services...":"搜索服务…","Service":"服务","Category":"分类","Status":"状态","Order":"排序","Actions":"操作","Add service":"添加服务","Edit service":"编辑服务","Name *":"名称 *","URL *":"网址 *","Icon":"图标","No category":"无分类","Description":"描述","Tags (comma separated)":"标签(逗号分隔)","Sort order":"排序值","Health check URL":"健康检查地址","Enabled":"启用","Health check enabled":"启用健康检查", +"Add category":"添加分类","Edit category":"编辑分类","Organize services into groups":"将服务整理到不同分类","No categories yet.":"暂无分类。","Name":"名称","Save failed":"保存失败","Delete failed":"删除失败","Name is required":"名称不能为空","Name and URL are required":"名称和网址不能为空","Online":"在线","Offline":"离线","Unknown":"未知", +"POST/GET /mcp · changes apply immediately":"POST/GET /mcp · 修改立即生效","New token:":"新 Token:","Regenerate token? The old token will stop working.":"重新生成 Token?旧 Token 将立即失效。","Run cloud backup now?":"立即执行云端备份?","Current database will be backed up first.":"当前数据库会先进行备份。","Unable to load backups":"无法加载备份","English":"English","Chinese (Simplified)":"简体中文","Chinese":"中文","Language":"语言","Language preference":"语言偏好","Saved":"已保存","Backup complete":"备份完成","Done":"完成","Failed":"失败","Copy failed: ":"复制失败:","Delete ":"删除 ","Restore ":"恢复 ","?":"?" +}; +function tr(s){const v=(s||"").trim();if(/^\d+ services$/.test(v))return v.replace(/^(\d+) services$/,"$1 个服务");if(/^order \d+$/.test(v))return v.replace(/^order (\d+)$/,"排序 $1");return zh[v]||null} +function translate(root=document){ + const w=document.createTreeWalker(root,NodeFilter.SHOW_TEXT),a=[];while(w.nextNode())a.push(w.currentNode); + a.forEach(n=>{const x=tr(n.nodeValue);if(x)n.nodeValue=n.nodeValue.replace(n.nodeValue.trim(),x)}); + root.querySelectorAll?.("input[placeholder],textarea[placeholder],button[title],a[title]").forEach(e=>{for(const k of ["placeholder","title"]){if(e.hasAttribute(k)){const x=tr(e.getAttribute(k));if(x)e.setAttribute(k,x)}}}); + const x=tr(document.title);if(x)document.title=x; +} +function apply(lang){document.documentElement.lang=lang==="zh-CN"?"zh-CN":"en";if(lang==="zh-CN")translate(document)} +async function init(){try{const endpoint=location.pathname.startsWith("/admin")?"/admin/api/settings":"/api/language";const r=await fetch(endpoint,{credentials:"same-origin"});const d=await r.json();window.tonavLanguage=d.language||"en";apply(window.tonavLanguage);window.tonavTranslate=()=>translate(document);document.dispatchEvent(new CustomEvent("tonav-language",{detail:window.tonavLanguage}))}catch(e){}} +window.tonavSetLanguage=apply;window.t=function(s){return window.tonavLanguage==="zh-CN"?tr(s)||s:s};init(); +new MutationObserver(ms=>{if(window.tonavLanguage==="zh-CN")ms.forEach(m=>m.addedNodes.forEach(n=>{if(n.nodeType===1)translate(n)}))}).observe(document.documentElement,{childList:true,subtree:true}); +})(); diff --git a/templates/admin/categories.html b/templates/admin/categories.html index 8df15f9..6b7f51b 100644 --- a/templates/admin/categories.html +++ b/templates/admin/categories.html @@ -4,7 +4,7 @@ async function load(){const r=await fetch('/admin/api/categories'),d=await r.json();if(!d.data?.length){list.innerHTML='
No categories yet.
';return}list.innerHTML=d.data.map(c=>`
${escapeHTML(c.name)}order ${c.sort_order}
`).join('')} function openModal(id,name,order){editId.value=id||'';catName.value=name||'';catOrder.value=order||0;modalTitle.textContent=id?'Edit category':'Add category';modal.style.display='flex';catName.focus()} function edit(c){openModal(c.id,c.name,c.sort_order)}function closeModal(){modal.style.display='none'} -async function save(){const id=editId.value,name=catName.value.trim();if(!name)return alert('Name is required');const body={name,sort_order:+catOrder.value||0};if(id)body.id=+id;const r=await fetch(id?'/admin/api/categories/'+id:'/admin/api/categories',{method:id?'PUT':'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}),d=await r.json();if(d.success){closeModal();load()}else alert(d.message||'Save failed')} -async function del(id,name){if(!confirm('Delete '+name+'?'))return;const r=await fetch('/admin/api/categories/'+id,{method:'DELETE'}),d=await r.json();if(d.success)load();else alert(d.message||'Delete failed')} +async function save(){const id=editId.value,name=catName.value.trim();if(!name)return alert(t('Name is required'));const body={name,sort_order:+catOrder.value||0};if(id)body.id=+id;const r=await fetch(id?'/admin/api/categories/'+id:'/admin/api/categories',{method:id?'PUT':'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}),d=await r.json();if(d.success){closeModal();load()}else alert(d.message||t('Save failed'))} +async function del(id,name){if(!confirm(t('Delete ')+name+t('?')))return;const r=await fetch('/admin/api/categories/'+id,{method:'DELETE'}),d=await r.json();if(d.success)load();else alert(d.message||t('Delete failed'))} function escapeHTML(s){const d=document.createElement('div');d.textContent=s||'';return d.innerHTML}document.addEventListener('keydown',e=>{if(e.key==='Escape')closeModal()});load(); - + diff --git a/templates/admin/change_password.html b/templates/admin/change_password.html index 39d4fb2..31bea62 100644 --- a/templates/admin/change_password.html +++ b/templates/admin/change_password.html @@ -1 +1 @@ -Change password - ToNav
+Change password - ToNav
diff --git a/templates/admin/dashboard.html b/templates/admin/dashboard.html index d05b990..e507cf3 100644 --- a/templates/admin/dashboard.html +++ b/templates/admin/dashboard.html @@ -31,7 +31,7 @@