From 05bf52d04cf1cc006c5a5e3c0545d0c062d945a0 Mon Sep 17 00:00:00 2001 From: GoEdgeLab Date: Wed, 28 Jun 2023 09:12:13 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=A4=9A=E8=AF=AD=E8=A8=80?= =?UTF-8?q?=E6=9C=80=E5=9F=BA=E7=A1=80=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- build/build-messages.sh | 3 + build/rpc.json | 2 +- cmd/langs/main.go | 276 ++++++++++++++++++ go.mod | 4 +- go.sum | 4 + pkg/langs/codes/codes.go | 74 +++++ pkg/langs/lang.go | 32 ++ pkg/langs/manager.go | 105 +++++++ pkg/langs/manager_test.go | 43 +++ pkg/langs/message.go | 5 + pkg/langs/messages/messages_zh_cn.go | 75 +++++ pkg/langs/protos/zh-cn/admin_api_node.json | 2 + pkg/langs/protos/zh-cn/admin_cluster.json | 3 + pkg/langs/protos/zh-cn/admin_common.json | 3 + pkg/langs/protos/zh-cn/admin_dashboard.json | 7 + pkg/langs/protos/zh-cn/admin_login.json | 3 + pkg/langs/protos/zh-cn/admin_menu.json | 70 +++++ pkg/langs/protos/zh-cn/admin_node.json | 3 + pkg/langs/protos/zh-cn/admin_server.json | 3 + .../protos/zh-cn/admin_server_origin.json | 3 + pkg/langs/protos/zh-cn/admin_setings.json | 3 + pkg/langs/protos/zh-cn/admin_waf.json | 3 + pkg/langs/utils.go | 74 +++++ pkg/langs/utils_test.go | 36 +++ pkg/rpc/pb/model_admin_list.pb.go | 17 +- pkg/rpc/protos/models/model_admin_list.proto | 3 +- 27 files changed, 850 insertions(+), 9 deletions(-) create mode 100755 build/build-messages.sh create mode 100644 cmd/langs/main.go create mode 100644 pkg/langs/codes/codes.go create mode 100644 pkg/langs/lang.go create mode 100644 pkg/langs/manager.go create mode 100644 pkg/langs/manager_test.go create mode 100644 pkg/langs/message.go create mode 100644 pkg/langs/messages/messages_zh_cn.go create mode 100644 pkg/langs/protos/zh-cn/admin_api_node.json create mode 100644 pkg/langs/protos/zh-cn/admin_cluster.json create mode 100644 pkg/langs/protos/zh-cn/admin_common.json create mode 100644 pkg/langs/protos/zh-cn/admin_dashboard.json create mode 100644 pkg/langs/protos/zh-cn/admin_login.json create mode 100644 pkg/langs/protos/zh-cn/admin_menu.json create mode 100644 pkg/langs/protos/zh-cn/admin_node.json create mode 100644 pkg/langs/protos/zh-cn/admin_server.json create mode 100644 pkg/langs/protos/zh-cn/admin_server_origin.json create mode 100644 pkg/langs/protos/zh-cn/admin_setings.json create mode 100644 pkg/langs/protos/zh-cn/admin_waf.json create mode 100644 pkg/langs/utils.go create mode 100644 pkg/langs/utils_test.go diff --git a/.gitignore b/.gitignore index 923a2c5..2c868b1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *_plus.go -*_plus_test.go \ No newline at end of file +*_plus_test.go +*_plus.json \ No newline at end of file diff --git a/build/build-messages.sh b/build/build-messages.sh new file mode 100755 index 0000000..b658e33 --- /dev/null +++ b/build/build-messages.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +go run ../cmd/langs/main.go generate \ No newline at end of file diff --git a/build/rpc.json b/build/rpc.json index 64154c7..86758da 100644 --- a/build/rpc.json +++ b/build/rpc.json @@ -13537,7 +13537,7 @@ }, { "name": "AdminModuleList", - "code": "message AdminModuleList {\n\tint64 adminId = 1;\n\tbool isSuper = 2;\n\trepeated AdminModule Modules = 3;\n\tstring fullname = 4;\n\tstring theme = 5;\n}", + "code": "message AdminModuleList {\n\tint64 adminId = 1;\n\tbool isSuper = 2;\n\trepeated AdminModule Modules = 3;\n\tstring fullname = 4;\n\tstring theme = 5; // 风格模板\n\tstring lang = 6; // 界面语言\n}", "doc": "" }, { diff --git a/cmd/langs/main.go b/cmd/langs/main.go new file mode 100644 index 0000000..cde6b0d --- /dev/null +++ b/cmd/langs/main.go @@ -0,0 +1,276 @@ +// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn . + +package main + +import ( + "encoding/json" + "fmt" + "github.com/TeaOSLab/EdgeCommon/pkg/langs" + "github.com/iwind/TeaGo/Tea" + _ "github.com/iwind/TeaGo/bootstrap" + "github.com/iwind/TeaGo/lists" + "github.com/iwind/TeaGo/types" + "go/format" + "os" + "path/filepath" + "regexp" + "sort" + "strconv" + "strings" +) + +func main() { + var args = os.Args + if len(args) >= 2 { + switch args[1] { + case "generate": + runGenerate() + } + } else { + fmt.Println("Usage: langs [generate]") + } +} + +func runGenerate() { + var rootDir = filepath.Clean(Tea.Root + "/../pkg/langs/protos") + dir, err := os.Open(rootDir) + if err != nil { + fmt.Println("[ERROR]read dir failed: " + err.Error()) + return + } + defer func() { + _ = dir.Close() + }() + + files, err := dir.Readdir(0) + if err != nil { + fmt.Println("[ERROR]read dir failed: " + err.Error()) + return + } + + var dirRegexp = regexp.MustCompile(`^[a-z]+-[a-z]+$`) + var jsonFileNameRegexp = regexp.MustCompile(`^([a-zA-Z0-9]+)(_([a-zA-Z0-9]+))+\.json$`) + var messageCodeRegexp = regexp.MustCompile(`^[a-zA-Z0-9_]+$`) + + var messageCodes = []string{} + var messageMaps = map[string]map[string]string{} // lang => { K:V } + var defaultLang = langs.DefaultManager().DefaultLang() + + for _, file := range files { + var dirName = file.Name() + + if !file.IsDir() || !dirRegexp.MatchString(dirName) { + continue + } + var langCode = dirName + var isBaseLang = langCode == defaultLang + + func() { + jsonFiles, err := filepath.Glob(rootDir + "/" + dirName + "/*.json") + if err != nil { + fmt.Println("[ERROR]list json files failed: " + err.Error()) + return + } + + for _, jsonFile := range jsonFiles { + var jsonFileName = filepath.Base(jsonFile) + if len(jsonFileName) == 0 || !jsonFileNameRegexp.MatchString(jsonFileName) { + continue + } + var module = strings.TrimSuffix(jsonFileName, ".json") + + data, err := os.ReadFile(jsonFile) + if err != nil { + fmt.Println("[ERROR]read json file '" + jsonFile + "' failed: " + err.Error()) + return + } + + var m = map[string]string{} // code => value + err = json.Unmarshal(data, &m) + if err != nil { + fmt.Println("[ERROR]decode json file '" + jsonFile + "' failed: " + err.Error()) + return + } + + var newM = map[string]string{} + for code, value := range m { + if !messageCodeRegexp.MatchString(code) { + fmt.Println("[ERROR]invalid message code '" + code + "'") + return + } + + var fullCode = module + "_" + code + if isBaseLang { + messageCodes = append(messageCodes, fullCode) + } + newM[fullCode] = value + } + + finalMap, ok := messageMaps[langCode] + if !ok { + finalMap = map[string]string{} + messageMaps[langCode] = finalMap + } + for code, value := range newM { + finalMap[code] = value + } + } + }() + } + + // check message codes + fmt.Println("checking message codes ...") + var defaultMessageMap = map[string]string{} + for langCode, messageMap := range messageMaps { + if langCode == defaultLang { // only check lang not equal to 'en-us' + defaultMessageMap = messageMap + continue + } + + for messageCode := range messageMap { + if !lists.ContainsString(messageCodes, messageCode) { + fmt.Println("[ERROR]message code '" + messageCode + "' in lang '" + langCode + "' not exist in default lang file ('" + defaultLang + "')") + return + } + } + } + fmt.Println("found '" + types.String(len(messageCodes)) + "' message codes") + + // generate codes/codes.go + sort.Strings(messageCodes) + var codesSource = ` +// generated by run 'langs generate' + +package codes + +import( + "github.com/TeaOSLab/EdgeCommon/pkg/langs" +) + +const ( + ` + + for _, messageCode := range messageCodes { + // add comment to message code + comment, _, _ := strings.Cut(defaultMessageMap[messageCode], "\n") + + codesSource += upperWords(messageCode) + " langs.MessageCode = " + strconv.Quote(messageCode) + " // " + comment + "\n" + } + + codesSource += ` +) +` + + formattedCodesSource, err := format.Source([]byte(codesSource)) + if err != nil { + fmt.Println("[ERROR]format 'codes.go' failed: " + err.Error()) + return + } + + fmt.Println("generating 'codes/codes.go' ...") + err = os.WriteFile(Tea.Root+"/../pkg/langs/codes/codes.go", formattedCodesSource, 0666) + if err != nil { + fmt.Println("[ERROR]write to 'codes.go' failed: " + err.Error()) + return + } + + // generate messages_LANG.go + for langCode, messageMap := range messageMaps { + var langFile = strings.ReplaceAll("messages/messages_"+langCode+".go", "-", "_") + + fmt.Println("generating '" + langFile + "' ...") + var source = ` +// generated by run 'langs generate' + +package messages + +import( + "github.com/TeaOSLab/EdgeCommon/pkg/langs" +) + +func init() { + langs.Load("` + langCode + `", map[string]string{ + ` + + for code, value := range messageMap { + source += strconv.Quote(code) + ": " + strconv.Quote(value) + ",\n" + } + + source += ` + }) +} +` + + formattedSource, err := format.Source([]byte(source)) + if err != nil { + fmt.Println("[ERROR]format '" + langFile + "' failed: " + err.Error()) + return + } + + err = os.WriteFile(Tea.Root+"/../pkg/langs/"+langFile, formattedSource, 0666) + if err != nil { + fmt.Println("[ERROR]write file '" + langFile + "' failed: " + err.Error()) + return + } + } + + // generate language javascript files for EdgeAdmin + for lang, messageMap := range messageMaps { + if lang != defaultLang { + // TODO merge messageMap with default message map + } + + for _, component := range []string{"EdgeAdmin", "EdgeUser"} { + fmt.Println("generating '" + lang + ".js' for " + component + " ...") + var targetFile = filepath.Clean(Tea.Root + "/../../" + component + "/web/public/js/langs/" + lang + ".js") + var targetDir = filepath.Dir(targetFile) + dirStat, _ := os.Stat(targetDir) + if dirStat != nil { + messageMapJSON, err := json.Marshal(messageMap) + if err != nil { + fmt.Println("[ERROR]marshal message map failed: " + err.Error()) + return + } + err = os.WriteFile(targetFile, []byte(`// generated by 'langs generate' +window.LANG_MESSAGES = `+string(messageMapJSON)+";\n"), 0666) + if err != nil { + fmt.Println("[ERROR]write file '" + targetFile + "' failed: " + err.Error()) + return + } + } + } + } + + fmt.Println("success") +} + +func upperWords(s string) string { + var words = strings.Split(s, "_") + for index, word := range words { + words[index] = upperWord(word) + } + return strings.Join(words, "") +} + +func upperWord(word string) string { + var l = len(word) + if l == 0 { + return "" + } + + if l == 1 { + return strings.ToUpper(word) + } + + // special words + switch word { + case "api", "http", "https", "tcp", "udp", "ip", "dns", "ns", "waf", "acme", "ssh", "toa": + return strings.ToUpper(word) + case "ipv6": + return "IPv6" + case "ddos": + return "DDoS" + } + + return strings.ToUpper(word[:1]) + word[1:] +} diff --git a/go.mod b/go.mod index 684ef54..7c4bb3d 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,11 @@ go 1.18 require ( github.com/golang/protobuf v1.5.2 - github.com/iwind/TeaGo v0.0.0-20220304043459-0dd944a5b475 + github.com/iwind/TeaGo v0.0.0-20230623080147-cd1e53b4915f golang.org/x/net v0.8.0 google.golang.org/grpc v1.45.0 google.golang.org/protobuf v1.27.1 - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b + gopkg.in/yaml.v3 v3.0.1 ) require ( diff --git a/go.sum b/go.sum index a821788..efb0c0c 100644 --- a/go.sum +++ b/go.sum @@ -59,6 +59,8 @@ github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFb github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/iwind/TeaGo v0.0.0-20220304043459-0dd944a5b475 h1:EseyfFaQOjWanGiby9KMw7PjDBMg/95tLDgIw/ns0Cw= github.com/iwind/TeaGo v0.0.0-20220304043459-0dd944a5b475/go.mod h1:HRHK0zoC/og3c9/hKosD9yYVMTnnzm3PgXUdhRYHaLc= +github.com/iwind/TeaGo v0.0.0-20230623080147-cd1e53b4915f h1:xo6XmXLtveKcwcZAXV6VMxkWNzy/2dStfHEnyowsGAE= +github.com/iwind/TeaGo v0.0.0-20230623080147-cd1e53b4915f/go.mod h1:fi/Pq+/5m2HZoseM+39dMF57ANXRt6w4PkGu3NXPc5s= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -201,5 +203,7 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/pkg/langs/codes/codes.go b/pkg/langs/codes/codes.go new file mode 100644 index 0000000..5a2fd17 --- /dev/null +++ b/pkg/langs/codes/codes.go @@ -0,0 +1,74 @@ +// generated by run 'langs generate' + +package codes + +import ( + "github.com/TeaOSLab/EdgeCommon/pkg/langs" +) + +const ( + AdminDashboardDNS langs.MessageCode = "admin_dashboard_dns" // DNS + AdminDashboardEvents langs.MessageCode = "admin_dashboard_events" // 事件 + AdminDashboardOverview langs.MessageCode = "admin_dashboard_overview" // 概况 + AdminDashboardUser langs.MessageCode = "admin_dashboard_user" // 用户 + AdminDashboardWAF langs.MessageCode = "admin_dashboard_waf" // WAF + AdminMenuAdminRecipients langs.MessageCode = "admin_menu_admin_recipients" // 通知媒介 + AdminMenuAdmins langs.MessageCode = "admin_menu_admins" // 系统用户 + AdminMenuDashboard langs.MessageCode = "admin_menu_dashboard" // 数据看板 + AdminMenuDNS langs.MessageCode = "admin_menu_dns" // 域名解析 + AdminMenuDNSClusters langs.MessageCode = "admin_menu_dns_clusters" // 集群列表 + AdminMenuDNSIssues langs.MessageCode = "admin_menu_dns_issues" // 问题修复 + AdminMenuDNSProviders langs.MessageCode = "admin_menu_dns_providers" // DNS服务商 + AdminMenuFinance langs.MessageCode = "admin_menu_finance" // 财务管理 + AdminMenuFinanceAccounts langs.MessageCode = "admin_menu_finance_accounts" // 用户账户 + AdminMenuFinanceBills langs.MessageCode = "admin_menu_finance_bills" // 费用账单 + AdminMenuFinanceFee langs.MessageCode = "admin_menu_finance_fee" // 计费设置 + AdminMenuFinanceIncome langs.MessageCode = "admin_menu_finance_income" // 统计报表 + AdminMenuFinanceLogs langs.MessageCode = "admin_menu_finance_logs" // 收支明细 + AdminMenuFinanceOrders langs.MessageCode = "admin_menu_finance_orders" // 订单管理 + AdminMenuFinancePackages langs.MessageCode = "admin_menu_finance_packages" // 流量包 + AdminMenuLogs langs.MessageCode = "admin_menu_logs" // 日志审计 + AdminMenuNodeAntiDDoSProducts langs.MessageCode = "admin_menu_node_anti_ddos_products" // 高防IP + AdminMenuNodeClusters langs.MessageCode = "admin_menu_node_clusters" // 集群列表 + AdminMenuNodeDistributedMonitors langs.MessageCode = "admin_menu_node_distributed_monitors" // 区域监控 + AdminMenuNodeIPList langs.MessageCode = "admin_menu_node_ip_list" // 节点IP + AdminMenuNodeLogs langs.MessageCode = "admin_menu_node_logs" // 节点日志 + AdminMenuNodeRegions langs.MessageCode = "admin_menu_node_regions" // 区域设置 + AdminMenuNodeSSHGrants langs.MessageCode = "admin_menu_node_ssh_grants" // 节点SSH + AdminMenuNodes langs.MessageCode = "admin_menu_nodes" // 边缘节点 + AdminMenuNS langs.MessageCode = "admin_menu_ns" // 智能DNS + AdminMenuNSAccessLogs langs.MessageCode = "admin_menu_ns_access_logs" // 访问日志 + AdminMenuNSClusters langs.MessageCode = "admin_menu_ns_clusters" // 集群管理 + AdminMenuNSDomainBatchOperations langs.MessageCode = "admin_menu_ns_domain_batch_operations" // 批量操作 + AdminMenuNSDomainGroups langs.MessageCode = "admin_menu_ns_domain_groups" // 域名分组 + AdminMenuNSDomains langs.MessageCode = "admin_menu_ns_domains" // 域名管理 + AdminMenuNSNodeLogs langs.MessageCode = "admin_menu_ns_node_logs" // 运行日志 + AdminMenuNSPlans langs.MessageCode = "admin_menu_ns_plans" // 套餐设置 + AdminMenuNSResolveTest langs.MessageCode = "admin_menu_ns_resolve_test" // 解析测试 + AdminMenuNSRoutes langs.MessageCode = "admin_menu_ns_routes" // 线路管理 + AdminMenuNSSettings langs.MessageCode = "admin_menu_ns_settings" // 全局配置 + AdminMenuNSUserPlans langs.MessageCode = "admin_menu_ns_user_plans" // 用户套餐 + AdminMenuPlanUserPlans langs.MessageCode = "admin_menu_plan_user_plans" // 已购套餐 + AdminMenuPlans langs.MessageCode = "admin_menu_plans" // 套餐管理 + AdminMenuServerAccessLogPolicies langs.MessageCode = "admin_menu_server_access_log_policies" // 日志策略 + AdminMenuServerAccessLogs langs.MessageCode = "admin_menu_server_access_logs" // 访问日志 + AdminMenuServerCachePolicies langs.MessageCode = "admin_menu_server_cache_policies" // 缓存策略 + AdminMenuServerCerts langs.MessageCode = "admin_menu_server_certs" // 证书管理 + AdminMenuServerGlobalSettings langs.MessageCode = "admin_menu_server_global_settings" // 通用设置 + AdminMenuServerGroups langs.MessageCode = "admin_menu_server_groups" // 网站分组 + AdminMenuServerIPLists langs.MessageCode = "admin_menu_server_ip_lists" // IP名单 + AdminMenuServerMetrics langs.MessageCode = "admin_menu_server_metrics" // 统计指标 + AdminMenuServerPurgeFetchCaches langs.MessageCode = "admin_menu_server_purge_fetch_caches" // 刷新预热 + AdminMenuServerScripts langs.MessageCode = "admin_menu_server_scripts" // 脚本库 + AdminMenuServerTrafficStats langs.MessageCode = "admin_menu_server_traffic_stats" // 用量统计 + AdminMenuServerWAFPolicies langs.MessageCode = "admin_menu_server_waf_policies" // WAF策略 + AdminMenuServers langs.MessageCode = "admin_menu_servers" // 网站列表 + AdminMenuSettingAdvancedSettings langs.MessageCode = "admin_menu_setting_advanced_settings" // 高级设置 + AdminMenuSettingAuthority langs.MessageCode = "admin_menu_setting_authority" // 商业版本 + AdminMenuSettingBasicSettings langs.MessageCode = "admin_menu_setting_basic_settings" // 基础设置 + AdminMenuSettings langs.MessageCode = "admin_menu_settings" // 系统设置 + AdminMenuTicketCategory langs.MessageCode = "admin_menu_ticket_category" // 分类 + AdminMenuTickets langs.MessageCode = "admin_menu_tickets" // 工单系统 + AdminMenuUsers langs.MessageCode = "admin_menu_users" // 用户设置 + +) diff --git a/pkg/langs/lang.go b/pkg/langs/lang.go new file mode 100644 index 0000000..c5ef4bb --- /dev/null +++ b/pkg/langs/lang.go @@ -0,0 +1,32 @@ +// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn . + +package langs + +type Lang struct { + code string + + messageMap map[string]string // message code => message text +} + +func NewLang(code string) *Lang { + return &Lang{ + code: code, + messageMap: map[string]string{}, + } +} + +func (this *Lang) Set(messageCode string, messageText string) { + this.messageMap[messageCode] = messageText +} + +// Get 读取单条消息 +// get single message with message code +func (this *Lang) Get(messageCode string) string { + return this.messageMap[messageCode] +} + +// GetAll 读取所有消息 +// get all messages +func (this *Lang) GetAll() map[string]string { + return this.messageMap +} diff --git a/pkg/langs/manager.go b/pkg/langs/manager.go new file mode 100644 index 0000000..56f8ac8 --- /dev/null +++ b/pkg/langs/manager.go @@ -0,0 +1,105 @@ +// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn . + +package langs + +import ( + "fmt" + "strings" +) + +var defaultManager = NewManager() + +type Manager struct { + langMap map[string]*Lang // lang code => *Lang, lang code must be in lowercase + defaultLangCode string +} + +func NewManager() *Manager { + return &Manager{ + langMap: map[string]*Lang{}, + defaultLangCode: "zh-cn", + } +} + +func DefaultManager() *Manager { + return defaultManager +} + +func (this *Manager) AddLang(code string) *Lang { + var lang = NewLang(code) + this.langMap[code] = lang + return lang +} + +func (this *Manager) HasLang(code string) bool { + _, ok := this.langMap[code] + return ok +} + +func (this *Manager) GetLang(code string) (lang *Lang, ok bool) { + lang, ok = this.langMap[code] + return +} + +func (this *Manager) MatchLang(code string) (matchedCode string) { + // lookup exact match + code = strings.ToLower(code) + _, ok := this.langMap[code] + if ok { + return code + } + + // lookup language family, such as en-us, en + if strings.Contains(code, "-") { + code, _, _ = strings.Cut(code, "-") + } + for rawCode := range this.langMap { + if strings.HasPrefix(rawCode, code+"-") { // en-us vs en + return rawCode + } + } + + return this.DefaultLang() +} + +func (this *Manager) SetDefaultLang(code string) { + this.defaultLangCode = code +} + +func (this *Manager) DefaultLang() string { + if len(this.defaultLangCode) > 0 { + return this.defaultLangCode + } + return "zh-cn" +} + +// GetMessage +// message: name: %s, age: %d, salary: %.2f +func (this *Manager) GetMessage(langCode string, messageCode string, args ...any) string { + var lang = this.langMap[langCode] + if lang == nil && len(this.defaultLangCode) > 0 { + lang = this.langMap[this.defaultLangCode] + } + if lang == nil { + return "" + } + + var message = lang.Get(messageCode) + if len(message) == 0 { + // try to get message from default lang + if lang.code != this.defaultLangCode { + var defaultLang = this.langMap[this.defaultLangCode] + if defaultLang != nil { + return defaultLang.Get(messageCode) + } + } + + return "" + } + + if len(args) == 0 { + return message + } + + return fmt.Sprintf(message, args...) +} diff --git a/pkg/langs/manager_test.go b/pkg/langs/manager_test.go new file mode 100644 index 0000000..9b0f1cd --- /dev/null +++ b/pkg/langs/manager_test.go @@ -0,0 +1,43 @@ +// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn . + +package langs_test + +import ( + "github.com/TeaOSLab/EdgeCommon/pkg/langs" + "testing" +) + +func TestManager_GetMessage(t *testing.T) { + var manager = langs.NewManager() + var lang = manager.AddLang("en-US") + lang.Set("user_description", "user: %s, age: %d") + t.Log(manager.GetMessage("en-US", "user_description", "Lily", 23)) + t.Log(manager.GetMessage("zh-CN", "user_description", "Lucy", 23)) // use 'en-US' as fallback language +} + +func TestManager_GetMessage2(t *testing.T) { + var manager = langs.NewManager() + manager.SetDefaultLang("zh-CN") + var lang = manager.AddLang("en-US") + lang.Set("user_description", "user: %s, age: %d") + t.Log(manager.GetMessage("en-US", "user_description", "Lily", 23)) + t.Log(manager.GetMessage("zh-CN", "user_description", "Lucy", 23)) // should be empty +} + +func TestManager_MatchLang(t *testing.T) { + var manager = langs.NewManager() + manager.AddLang("en-us") + manager.AddLang("en") + manager.AddLang("zh-cn") + manager.AddLang("zh-hk") + //manager.AddLang("zh-tw") + + for _, code := range []string{ + "en", + "en-us", + "zh-cn", + "zh-tw", + } { + t.Log(code, "=>", manager.MatchLang(code)) + } +} diff --git a/pkg/langs/message.go b/pkg/langs/message.go new file mode 100644 index 0000000..b464188 --- /dev/null +++ b/pkg/langs/message.go @@ -0,0 +1,5 @@ +// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn . + +package langs + +type MessageCode = string diff --git a/pkg/langs/messages/messages_zh_cn.go b/pkg/langs/messages/messages_zh_cn.go new file mode 100644 index 0000000..2a8cbc9 --- /dev/null +++ b/pkg/langs/messages/messages_zh_cn.go @@ -0,0 +1,75 @@ +// generated by run 'langs generate' + +package messages + +import ( + "github.com/TeaOSLab/EdgeCommon/pkg/langs" +) + +func init() { + langs.Load("zh-cn", map[string]string{ + "admin_menu_finance_fee": "计费设置", + "admin_menu_server_traffic_stats": "用量统计", + "admin_menu_ns_node_logs": "运行日志", + "admin_menu_servers": "网站列表", + "admin_menu_finance_logs": "收支明细", + "admin_menu_plan_user_plans": "已购套餐", + "admin_menu_server_ip_lists": "IP名单", + "admin_menu_users": "用户设置", + "admin_menu_ns_access_logs": "访问日志", + "admin_menu_server_purge_fetch_caches": "刷新预热", + "admin_menu_node_distributed_monitors": "区域监控", + "admin_menu_finance_packages": "流量包", + "admin_menu_settings": "系统设置", + "admin_menu_ns_plans": "套餐设置", + "admin_dashboard_user": "用户", + "admin_dashboard_waf": "WAF", + "admin_menu_setting_advanced_settings": "高级设置", + "admin_menu_server_global_settings": "通用设置", + "admin_menu_node_logs": "节点日志", + "admin_menu_ns_clusters": "集群管理", + "admin_menu_server_cache_policies": "缓存策略", + "admin_menu_ns_resolve_test": "解析测试", + "admin_menu_finance_income": "统计报表", + "admin_menu_finance_accounts": "用户账户", + "admin_menu_server_certs": "证书管理", + "admin_menu_dashboard": "数据看板", + "admin_dashboard_events": "事件", + "admin_menu_node_ip_list": "节点IP", + "admin_dashboard_overview": "概况", + "admin_menu_server_access_logs": "访问日志", + "admin_menu_dns_providers": "DNS服务商", + "admin_menu_admins": "系统用户", + "admin_menu_setting_authority": "商业版本", + "admin_menu_dns_issues": "问题修复", + "admin_menu_server_metrics": "统计指标", + "admin_menu_node_ssh_grants": "节点SSH", + "admin_menu_server_waf_policies": "WAF策略", + "admin_menu_ns_domains": "域名管理", + "admin_menu_nodes": "边缘节点", + "admin_dashboard_dns": "DNS", + "admin_menu_logs": "日志审计", + "admin_menu_ns_user_plans": "用户套餐", + "admin_menu_ns_domain_groups": "域名分组", + "admin_menu_tickets": "工单系统", + "admin_menu_node_regions": "区域设置", + "admin_menu_ns_domain_batch_operations": "批量操作", + "admin_menu_plans": "套餐管理", + "admin_menu_finance_bills": "费用账单", + "admin_menu_ticket_category": "分类", + "admin_menu_finance": "财务管理", + "admin_menu_node_anti_ddos_products": "高防IP", + "admin_menu_ns": "智能DNS", + "admin_menu_dns": "域名解析", + "admin_menu_server_groups": "网站分组", + "admin_menu_server_scripts": "脚本库", + "admin_menu_ns_settings": "全局配置", + "admin_menu_ns_routes": "线路管理", + "admin_menu_server_access_log_policies": "日志策略", + "admin_menu_finance_orders": "订单管理", + "admin_menu_dns_clusters": "集群列表", + "admin_menu_admin_recipients": "通知媒介", + "admin_menu_setting_basic_settings": "基础设置", + "admin_menu_node_clusters": "集群列表", + }) +} diff --git a/pkg/langs/protos/zh-cn/admin_api_node.json b/pkg/langs/protos/zh-cn/admin_api_node.json new file mode 100644 index 0000000..7a73a41 --- /dev/null +++ b/pkg/langs/protos/zh-cn/admin_api_node.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/pkg/langs/protos/zh-cn/admin_cluster.json b/pkg/langs/protos/zh-cn/admin_cluster.json new file mode 100644 index 0000000..0e0dcd2 --- /dev/null +++ b/pkg/langs/protos/zh-cn/admin_cluster.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/pkg/langs/protos/zh-cn/admin_common.json b/pkg/langs/protos/zh-cn/admin_common.json new file mode 100644 index 0000000..0e0dcd2 --- /dev/null +++ b/pkg/langs/protos/zh-cn/admin_common.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/pkg/langs/protos/zh-cn/admin_dashboard.json b/pkg/langs/protos/zh-cn/admin_dashboard.json new file mode 100644 index 0000000..728cccd --- /dev/null +++ b/pkg/langs/protos/zh-cn/admin_dashboard.json @@ -0,0 +1,7 @@ +{ + "overview": "概况", + "waf": "WAF", + "dns": "DNS", + "user": "用户", + "events": "事件" +} \ No newline at end of file diff --git a/pkg/langs/protos/zh-cn/admin_login.json b/pkg/langs/protos/zh-cn/admin_login.json new file mode 100644 index 0000000..0e0dcd2 --- /dev/null +++ b/pkg/langs/protos/zh-cn/admin_login.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/pkg/langs/protos/zh-cn/admin_menu.json b/pkg/langs/protos/zh-cn/admin_menu.json new file mode 100644 index 0000000..756d9db --- /dev/null +++ b/pkg/langs/protos/zh-cn/admin_menu.json @@ -0,0 +1,70 @@ +{ + "dashboard": "数据看板", + "servers": "网站列表", + "server_access_logs": "访问日志", + "server_groups": "网站分组", + "server_certs": "证书管理", + "server_traffic_stats": "用量统计", + "server_cache_policies": "缓存策略", + "server_purge_fetch_caches": "刷新预热", + "server_waf_policies": "WAF策略", + "server_ip_lists": "IP名单", + "server_access_log_policies": "日志策略", + "server_metrics": "统计指标", + "server_scripts": "脚本库", + "server_global_settings": "通用设置", + + "nodes": "边缘节点", + "node_clusters": "集群列表", + "node_logs": "节点日志", + "node_ip_list": "节点IP", + "node_regions": "区域设置", + "node_ssh_grants": "节点SSH", + "node_distributed_monitors": "区域监控", + "node_anti_ddos_products": "高防IP", + + "dns": "域名解析", + "dns_clusters": "集群列表", + "dns_providers": "DNS服务商", + "dns_issues": "问题修复", + + "ns": "智能DNS", + "ns_domains": "域名管理", + "ns_domain_groups": "域名分组", + "ns_domain_batch_operations": "批量操作", + "ns_clusters": "集群管理", + "ns_routes": "线路管理", + "ns_settings": "全局配置", + "ns_user_plans": "用户套餐", + "ns_plans": "套餐设置", + "ns_access_logs": "访问日志", + "ns_node_logs": "运行日志", + "ns_resolve_test": "解析测试", + + "users": "用户设置", + + "finance": "财务管理", + "finance_bills": "费用账单", + "finance_accounts": "用户账户", + "finance_logs": "收支明细", + "finance_orders": "订单管理", + "finance_income": "统计报表", + "finance_fee": "计费设置", + "finance_packages": "流量包", + + "plans": "套餐管理", + "plan_user_plans": "已购套餐", + + "tickets": "工单系统", + "ticket_category": "分类", + + "admins": "系统用户", + "admin_recipients": "通知媒介", + + "logs": "日志审计", + + "settings": "系统设置", + "setting_basic_settings": "基础设置", + "setting_advanced_settings": "高级设置", + "setting_authority": "商业版本" +} \ No newline at end of file diff --git a/pkg/langs/protos/zh-cn/admin_node.json b/pkg/langs/protos/zh-cn/admin_node.json new file mode 100644 index 0000000..0e0dcd2 --- /dev/null +++ b/pkg/langs/protos/zh-cn/admin_node.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/pkg/langs/protos/zh-cn/admin_server.json b/pkg/langs/protos/zh-cn/admin_server.json new file mode 100644 index 0000000..0e0dcd2 --- /dev/null +++ b/pkg/langs/protos/zh-cn/admin_server.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/pkg/langs/protos/zh-cn/admin_server_origin.json b/pkg/langs/protos/zh-cn/admin_server_origin.json new file mode 100644 index 0000000..0e0dcd2 --- /dev/null +++ b/pkg/langs/protos/zh-cn/admin_server_origin.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/pkg/langs/protos/zh-cn/admin_setings.json b/pkg/langs/protos/zh-cn/admin_setings.json new file mode 100644 index 0000000..0e0dcd2 --- /dev/null +++ b/pkg/langs/protos/zh-cn/admin_setings.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/pkg/langs/protos/zh-cn/admin_waf.json b/pkg/langs/protos/zh-cn/admin_waf.json new file mode 100644 index 0000000..0e0dcd2 --- /dev/null +++ b/pkg/langs/protos/zh-cn/admin_waf.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/pkg/langs/utils.go b/pkg/langs/utils.go new file mode 100644 index 0000000..2cc0a4d --- /dev/null +++ b/pkg/langs/utils.go @@ -0,0 +1,74 @@ +// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn . + +package langs + +import ( + "github.com/TeaOSLab/EdgeCommon/pkg/configutils" + "github.com/iwind/TeaGo/actions" + "net/http" + "strings" +) + +// Message 读取消息 +// Read message +func Message(langCode string, messageCode MessageCode, args ...any) string { + return defaultManager.GetMessage(langCode, messageCode, args...) +} + +func ParseLangFromRequest(req *http.Request) (langCode string) { + // parse language from cookie + const cookieName = "edgelang" + cookie, _ := req.Cookie(cookieName) + if cookie != nil && len(cookie.Value) > 0 && defaultManager.HasLang(cookie.Value) { + return cookie.Value + } + + // parse language from 'Accept-Language' + var acceptLanguage = req.Header.Get("Accept-Language") + if len(acceptLanguage) > 0 { + var pieces = strings.Split(acceptLanguage, ",") + for _, lang := range pieces { + var index = strings.Index(lang, ";") + if index >= 0 { + lang = lang[:index] + } + + var match = defaultManager.MatchLang(lang) + if len(match) > 0 { + return match + } + } + } + + return defaultManager.DefaultLang() +} + +func ParseLangFromAction(action actions.ActionWrapper) (langCode string) { + return ParseLangFromRequest(action.Object().Request) +} + +// Format 格式化变量 +// Format string that contains message variables, such as ${lang.MESSAGE_CODE} +// +// 暂时不支持变量中加参数 +func Format(langCode string, varString string) string { + return configutils.ParseVariables(varString, func(varName string) (value string) { + const prefix = "lang." + if !strings.HasPrefix(varName, prefix) { + return "${" + varName + "}" // keep origin variable + } + return Message(langCode, varName[len(prefix):]) + }) +} + +// Load 加载消息定义 +// Load message definitions from map +func Load(langCode string, messageMap map[string]string) { + lang, ok := defaultManager.GetLang(langCode) + if !ok { + lang = defaultManager.AddLang(langCode) + } + for messageCode, messageText := range messageMap { + lang.Set(messageCode, messageText) + } +} diff --git a/pkg/langs/utils_test.go b/pkg/langs/utils_test.go new file mode 100644 index 0000000..46edcd2 --- /dev/null +++ b/pkg/langs/utils_test.go @@ -0,0 +1,36 @@ +// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn . + +package langs_test + +import ( + "github.com/TeaOSLab/EdgeCommon/pkg/langs" + "testing" +) + +func TestMessage(t *testing.T) { + var lang = langs.DefaultManager().AddLang("en-us") + lang.Set("user_description", "user: %s") + t.Log(langs.Message("en-us", "user_description", "Lily")) +} + +func TestMessageDefault(t *testing.T) { + var lang = langs.DefaultManager().AddLang("zh-cn") + lang.Set("user_description", "user: %s") + + t.Log(langs.Message("en-us", "user_description", "Lily")) +} + +func TestFormat(t *testing.T) { + { + var lang = langs.DefaultManager().AddLang("en-US") + lang.Set("book_name", "Golang") + } + + { + var lang = langs.DefaultManager().AddLang("zh-CN") + lang.Set("book_name", "Go语言") + } + + t.Log(langs.Format("en-US", "this is ${lang.book_name} book")) + t.Log(langs.Format("zh-CN", "this is ${lang.book_name} book")) +} diff --git a/pkg/rpc/pb/model_admin_list.pb.go b/pkg/rpc/pb/model_admin_list.pb.go index d775b19..dc5647f 100644 --- a/pkg/rpc/pb/model_admin_list.pb.go +++ b/pkg/rpc/pb/model_admin_list.pb.go @@ -34,7 +34,8 @@ type AdminModuleList struct { IsSuper bool `protobuf:"varint,2,opt,name=isSuper,proto3" json:"isSuper,omitempty"` Modules []*AdminModule `protobuf:"bytes,3,rep,name=Modules,proto3" json:"Modules,omitempty"` Fullname string `protobuf:"bytes,4,opt,name=fullname,proto3" json:"fullname,omitempty"` - Theme string `protobuf:"bytes,5,opt,name=theme,proto3" json:"theme,omitempty"` + Theme string `protobuf:"bytes,5,opt,name=theme,proto3" json:"theme,omitempty"` // 风格模板 + Lang string `protobuf:"bytes,6,opt,name=lang,proto3" json:"lang,omitempty"` // 界面语言 } func (x *AdminModuleList) Reset() { @@ -104,6 +105,13 @@ func (x *AdminModuleList) GetTheme() string { return "" } +func (x *AdminModuleList) GetLang() string { + if x != nil { + return x.Lang + } + return "" +} + var File_models_model_admin_list_proto protoreflect.FileDescriptor var file_models_model_admin_list_proto_rawDesc = []byte{ @@ -111,7 +119,7 @@ var file_models_model_admin_list_proto_rawDesc = []byte{ 0x64, 0x6d, 0x69, 0x6e, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x02, 0x70, 0x62, 0x1a, 0x1f, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x73, 0x2f, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x5f, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x5f, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xa2, 0x01, 0x0a, 0x0f, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x4d, 0x6f, + 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb6, 0x01, 0x0a, 0x0f, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x69, 0x73, 0x53, 0x75, 0x70, 0x65, 0x72, 0x18, 0x02, 0x20, @@ -121,8 +129,9 @@ var file_models_model_admin_list_proto_rawDesc = []byte{ 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x75, 0x6c, 0x6c, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x75, 0x6c, 0x6c, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x68, 0x65, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x74, 0x68, 0x65, 0x6d, 0x65, 0x42, 0x06, 0x5a, 0x04, 0x2e, 0x2f, 0x70, - 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x28, 0x09, 0x52, 0x05, 0x74, 0x68, 0x65, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6c, 0x61, 0x6e, + 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6c, 0x61, 0x6e, 0x67, 0x42, 0x06, 0x5a, + 0x04, 0x2e, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/pkg/rpc/protos/models/model_admin_list.proto b/pkg/rpc/protos/models/model_admin_list.proto index 06dad1a..c6a35e2 100644 --- a/pkg/rpc/protos/models/model_admin_list.proto +++ b/pkg/rpc/protos/models/model_admin_list.proto @@ -10,5 +10,6 @@ message AdminModuleList { bool isSuper = 2; repeated AdminModule Modules = 3; string fullname = 4; - string theme = 5; + string theme = 5; // 风格模板 + string lang = 6; // 界面语言 } \ No newline at end of file