Skip to content

Commit 97714e9

Browse files
authored
Merge pull request #36 from flyhope/20-i18n
support i18n
2 parents da312b9 + b1e9885 commit 97714e9

File tree

16 files changed

+348
-35
lines changed

16 files changed

+348
-35
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,5 @@ pkg/
2929
# bin
3030
__debug_bin*
3131
kubetea*
32+
33+
lang/translate.*.yaml

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ kubetea $ipOrPodname
7272
- [x] support kubeconfig
7373
- [x] support context
7474
- [x] support namespace
75+
- [x] support i18n multi language (en/zh/custom)
7576

7677
## Thanks
7778

comm/config.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,17 @@ import (
1313
//go:embed kubetea.yaml
1414
var kubeteaDefaultYaml []byte
1515

16+
// ShowConfigFilePath kubetea config file path
17+
var ShowConfigFilePath = sync.OnceValue(func() string {
18+
return FixPath(Context.String("config"))
19+
})
20+
1621
// ShowKubeteaConfig 获取k.yaml配置
1722
var ShowKubeteaConfig = sync.OnceValue(func() *KubeteaConfig {
1823
config := new(KubeteaConfig)
1924

2025
// 文件件在才继续加载配置
21-
configFilePath := FixPath(Context.String("config"))
26+
configFilePath := ShowConfigFilePath()
2227

2328
yamlContent := kubeteaDefaultYaml
2429
_, errStat := os.Stat(configFilePath)
@@ -54,6 +59,7 @@ var ShowKubeteaConfig = sync.OnceValue(func() *KubeteaConfig {
5459

5560
// KubeteaConfig YAML配置定义
5661
type KubeteaConfig struct {
62+
Language string `yaml:"language"` // 显示语言设置
5763
PodCacheLivetime uint32 `yaml:"pod_cache_livetime_second"` // 缓存Pod过期时间,过期自动刷新
5864
Log KubeteaConfigLog `yaml:"log"` // 日志配置
5965
ClusterByLabel string `yaml:"cluster_by_label"` // 筛选显示cluster的Label的名称

comm/kubetea.yaml

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# kubetea default config
22
# auto write this file content to ~/.kubetea/config when first run
33

4+
# support auto / en / zh
5+
language: "auto"
6+
47
# pod cache when x second for update
58
pod_cache_livetime_second: 5
69

@@ -32,19 +35,19 @@ sort:
3235
template:
3336
cluster:
3437
column:
35-
- { title: "集群", width: 0 }
36-
- { title: "数量", width: 10 }
38+
- { title: '{{ txt "cluster" }}', width: 0 }
39+
- { title: '{{ txt "number" }}', width: 10 }
3740
body:
3841
# first cloume must be .Name
3942
- "{{ .Name }}"
4043
- "{{ len .Pods }}"
4144
pod:
4245
column:
43-
- { title: "名称", width: 0 }
44-
- { title: "IP", width: 15 }
45-
- { title: "状态", width: 4 }
46-
- { title: "就绪", width: 4 }
47-
- { title: "启动时间", width: 19 }
46+
- { title: '{{ txt "name" }}', width: 0 }
47+
- { title: 'IP', width: 15 }
48+
- { title: '{{ txt "status" }}', width: 4 }
49+
- { title: '{{ txt "ready" }}', width: 4 }
50+
- { title: '{{ txt "createdAt" }}', width: 19 }
4851
body:
4952
# first cloume must be .Name
5053
- "{{ .Name }}"
@@ -54,10 +57,10 @@ template:
5457
- "{{ if .Status.StartTime }}{{ FormatTime .Status.StartTime.Time }}{{else}}-{{end}}"
5558
container:
5659
column:
57-
- { title: "容器名称", width: 0 }
58-
- { title: "镜像地址", width: 0 }
59-
- { title: "状态", width: 4 }
60-
- { title: "就绪", width: 4 }
60+
- { title: '{{ txt "containerName" }}', width: 0 }
61+
- { title: '{{ txt "imageUrl" }}', width: 0 }
62+
- { title: '{{ txt "status" }}', width: 4 }
63+
- { title: '{{ txt "createdAt" }}', width: 4 }
6164
body:
6265
# first cloume must be .Name
6366
- "{{ .Name }}"

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ require (
88
github.com/charmbracelet/lipgloss v0.12.1
99
github.com/fatih/color v1.17.0
1010
github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f
11+
github.com/nicksnyder/go-i18n/v2 v2.4.0
1112
github.com/nsf/termbox-go v1.1.1
1213
github.com/sirupsen/logrus v1.9.3
1314
github.com/urfave/cli/v2 v2.27.2
1415
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561
16+
golang.org/x/text v0.16.0
1517
gopkg.in/yaml.v3 v3.0.1
1618
k8s.io/api v0.30.2
1719
k8s.io/apimachinery v0.30.2
@@ -63,7 +65,6 @@ require (
6365
golang.org/x/sync v0.7.0 // indirect
6466
golang.org/x/sys v0.22.0 // indirect
6567
golang.org/x/term v0.18.0 // indirect
66-
golang.org/x/text v0.16.0 // indirect
6768
golang.org/x/time v0.3.0 // indirect
6869
google.golang.org/appengine v1.6.7 // indirect
6970
google.golang.org/protobuf v1.33.0 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
2+
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
13
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
24
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
35
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
@@ -99,6 +101,8 @@ github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo
99101
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
100102
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
101103
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
104+
github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM=
105+
github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4=
102106
github.com/nsf/termbox-go v1.1.1 h1:nksUPLCb73Q++DwbYUBEglYBRPZyoXJdrj5L+TkjyZY=
103107
github.com/nsf/termbox-go v1.1.1/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo=
104108
github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY=

lang/README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
2+
3+
4+
## Development
5+
6+
You must be installed go-i18n command
7+
8+
```bash
9+
go install -v github.com/nicksnyder/go-i18n/v2/goi18n@latest
10+
goi18n -help
11+
```
12+
13+
More go-i18n document:
14+
* english https://github.com/nicksnyder/go-i18n/blob/main/README.md
15+
* chinese https://github.com/nicksnyder/go-i18n/blob/main/.github/README.zh-Hans.md
16+
17+
### Add a new language
18+
19+
example: add a new language `fr`
20+
21+
1. clone this repository and cd to it.
22+
2. init your language file.
23+
```bash
24+
export KUBETEA_LANG=fr
25+
goi18n extract -format=yaml -outdir="./lang"
26+
cd lang
27+
goi18n merge -format=yaml view.en.yaml active.en.yaml
28+
touch translate.$KUBETEA_LANG.yaml
29+
goi18n merge -format=yaml active.en.yaml translate.$KUBETEA_LANG.yaml
30+
```
31+
3. translate your language file `translate.fr.yaml`.
32+
4. update merge your language file `active.fr.yaml`.
33+
```bash
34+
rename translate.$KUBETEA_LANG.yaml active.$KUBETEA_LANG.yaml
35+
```
36+
37+
### Update existing language
38+
39+
1. cd to this work directory.
40+
2. update different language translate file.
41+
```bash
42+
goi18n extract -format=yaml -outdir="./lang"
43+
cd lang
44+
goi18n merge -format=yaml view.en.yaml active.en.yaml
45+
goi18n merge -format=yaml active.*.yaml
46+
```
47+
3. Translate all the messages in the `translate.*.yaml` files.
48+
4. Run command merge the translated messages into the active message files.
49+
```bash
50+
goi18n merge -format=yaml active.*.yaml translate.*.yaml
51+
rm -f translate.*.yaml
52+
```
53+
54+
### Add a new language file, but not build in kubetea
55+
56+
1. look up ↑ `Add a new language` and do it.
57+
2. copy `active.xx.yaml` to `~/.kubetea/lang/active.xx.yaml`.

lang/active.en.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
cluster: cluster
2+
containerName: container
3+
createdAt: created at
4+
data_update_time: 'Data update time: {{.UpdateTime}}'
5+
imageUrl: image
6+
name: name
7+
number: number
8+
ready: ready
9+
status: status
10+
total_with_number: 'Total: {{.number}}'

lang/active.zh-CN.yaml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
cluster:
2+
hash: sha1-da68bf3a55223d19ffeabebe48b0af570d763307
3+
other: 集群
4+
containerName:
5+
hash: sha1-255a762c36cdae2fe950e67e8b8a4efb260e7540
6+
other: 容器名称
7+
createdAt:
8+
hash: sha1-19b6b586f60dbae898ca8e37ef98779f6bf2118c
9+
other: 启动时间
10+
data_update_time:
11+
hash: sha1-c9b4161183a72bcd0113aea04efa894d09c13f19
12+
other: '数据更新时间: {{.UpdateTime}}'
13+
imageUrl:
14+
hash: sha1-0e76292794888d4f1fa75fb3aff4ca27c58f56a6
15+
other: 镜像地址
16+
name:
17+
hash: sha1-6ae999552a0d2dca14d62e2bc8b764d377b1dd6c
18+
other: 名称
19+
number:
20+
hash: sha1-53b0a1b2fadf4e040cdc2155a7340de24aca93cb
21+
other: 数量
22+
ready:
23+
hash: sha1-75c0533730caf1f78561c0883fb87bc8d98ef04b
24+
other: 就绪
25+
status:
26+
hash: sha1-48a3661d846478fa991a825ebd10b78671444b5b
27+
other: 状态
28+
total_with_number:
29+
hash: sha1-fd5a6ff922cc49168c6cdb98bad09e7e13641c10
30+
other: '合计: {{.number}}'

lang/bundle.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package lang
2+
3+
import (
4+
"embed"
5+
"errors"
6+
"github.com/flyhope/kubetea/comm"
7+
"github.com/nicksnyder/go-i18n/v2/i18n"
8+
"github.com/sirupsen/logrus"
9+
"golang.org/x/text/language"
10+
"gopkg.in/yaml.v3"
11+
"os"
12+
"path/filepath"
13+
"strings"
14+
"sync"
15+
)
16+
17+
//go:embed active.*.yaml
18+
var localeFS embed.FS
19+
20+
// init bundle i18n function
21+
var bundle = sync.OnceValue(func() *i18n.Bundle {
22+
b := i18n.NewBundle(language.English)
23+
b.RegisterUnmarshalFunc("yaml", yaml.Unmarshal)
24+
25+
// load code all language yaml file
26+
dir, err := localeFS.ReadDir(".")
27+
if err != nil {
28+
logrus.Fatal(err)
29+
}
30+
for _, file := range dir {
31+
_, err := b.LoadMessageFileFS(localeFS, file.Name())
32+
if err != nil {
33+
logrus.WithFields(logrus.Fields{"file": file.Name()}).Fatal(err)
34+
}
35+
}
36+
37+
// scan dir, load user all language yaml file (active.*.yaml)
38+
userLangDir := filepath.Dir(comm.ShowConfigFilePath()) + "/lang"
39+
if _, err := os.Stat(userLangDir); err == nil {
40+
files, err := os.ReadDir(userLangDir)
41+
if err != nil {
42+
logrus.WithFields(logrus.Fields{"dir": userLangDir}).Fatal(err)
43+
}
44+
for _, file := range files {
45+
// filter active.*.yaml
46+
if !strings.HasPrefix(file.Name(), "active.") || !strings.HasSuffix(file.Name(), ".yaml") {
47+
continue
48+
}
49+
50+
_, err := b.LoadMessageFile(userLangDir + "/" + file.Name())
51+
if err != nil {
52+
logrus.WithFields(logrus.Fields{"file": file.Name()}).Fatal(err)
53+
}
54+
}
55+
}
56+
57+
return b
58+
})
59+
60+
// DefaultLang 获取当前环境的语言
61+
var DefaultLang = sync.OnceValue(func() string {
62+
lang := comm.ShowKubeteaConfig().Language
63+
if lang != "" && lang != "auto" {
64+
return lang
65+
}
66+
67+
// 获取 LANG 环境变量
68+
lang = os.Getenv("LANG")
69+
70+
// 如果 LANG 为空,尝试获取 LC_ALL
71+
if lang == "" {
72+
lang = os.Getenv("LC_ALL")
73+
}
74+
75+
// 如果仍然为空,返回默认值
76+
if lang == "" {
77+
return ""
78+
}
79+
80+
// 移除编码后缀(如 ".UTF-8")
81+
if idx := strings.Index(lang, "."); idx != -1 {
82+
lang = lang[:idx]
83+
}
84+
85+
// 将下划线替换为连字符(RFC 2616格式)
86+
lang = strings.Replace(lang, "_", "-", -1)
87+
88+
return lang
89+
})
90+
91+
// Txt Render text with i18n
92+
func Txt(lc *i18n.LocalizeConfig) string {
93+
localizer := i18n.NewLocalizer(bundle(), DefaultLang())
94+
str, err := localizer.Localize(lc)
95+
96+
var messageNotFoundErr *i18n.MessageNotFoundErr
97+
if errors.As(err, &messageNotFoundErr) {
98+
localizer = i18n.NewLocalizer(bundle(), "en")
99+
str, err = localizer.Localize(lc)
100+
}
101+
102+
if err != nil {
103+
str = lc.DefaultMessage.ID
104+
logrus.WithFields(logrus.Fields{"id": lc.MessageID}).Errorln(err)
105+
}
106+
return str
107+
}
108+
109+
// Map lang data
110+
type Map map[string]any
111+
112+
// Data Render text with i18n (not multi number)
113+
func Data(defaultMesage *i18n.Message, data Map) string {
114+
return Txt(&i18n.LocalizeConfig{
115+
DefaultMessage: defaultMesage,
116+
TemplateData: data,
117+
})
118+
}

0 commit comments

Comments
 (0)