├── internal ├── examples │ ├── example1 │ │ ├── example1.go │ │ ├── internal │ │ │ ├── i18n │ │ │ │ ├── zh-CN.yaml │ │ │ │ ├── en-US.yaml │ │ │ │ └── km-KH.yaml │ │ │ ├── message1 │ │ │ │ ├── i18n_test.go │ │ │ │ ├── i18n.gen_test.go │ │ │ │ ├── i18n.go │ │ │ │ └── i18n.gen.go │ │ │ └── message1v2 │ │ │ │ ├── i18n.gen_test.go │ │ │ │ └── i18n.gen.go │ │ └── example1_test.go │ ├── example2 │ │ ├── example2.go │ │ ├── internal │ │ │ ├── message2 │ │ │ │ ├── trans.zh-CN.json │ │ │ │ ├── i18n_test.go │ │ │ │ ├── trans.en-US.json │ │ │ │ ├── i18n.gen_test.go │ │ │ │ ├── i18n.go │ │ │ │ └── i18n.gen.go │ │ │ └── message2v2 │ │ │ │ ├── i18n.gen_test.go │ │ │ │ └── i18n.gen.go │ │ └── example2_test.go │ └── example3 │ │ ├── example3.go │ │ ├── internal │ │ ├── message3 │ │ │ ├── msg.zh-CN.yaml │ │ │ ├── msg.en-US.yaml │ │ │ ├── i18n_test.go │ │ │ ├── i18n.gen_test.go │ │ │ ├── i18n.go │ │ │ └── i18n.gen.go │ │ └── message3v2 │ │ │ ├── i18n.gen_test.go │ │ │ └── i18n.gen.go │ │ └── example3_test.go ├── sketches │ ├── sketch3 │ │ ├── active.zh.toml │ │ ├── active.en.toml │ │ ├── README.md │ │ └── main.go │ ├── sketch4 │ │ ├── active.zh.toml │ │ ├── active.en.toml │ │ ├── main.go │ │ └── README.md │ ├── sketch1 │ │ ├── README.md │ │ └── main.go │ └── sketch2 │ │ ├── main.go │ │ └── README.md └── utils │ ├── utils.go │ └── utils_test.go ├── plural_config_test.go ├── Makefile ├── .gitignore ├── LICENSE ├── options_test.go ├── go.mod ├── plural_config.go ├── .github └── workflows │ └── release.yml ├── options.go ├── go.sum ├── goi18n_test.go ├── README.zh.md ├── README.md └── goi18n.go /internal/examples/example1/example1.go: -------------------------------------------------------------------------------- 1 | package example1 2 | -------------------------------------------------------------------------------- /internal/examples/example2/example2.go: -------------------------------------------------------------------------------- 1 | package example2 2 | -------------------------------------------------------------------------------- /internal/examples/example3/example3.go: -------------------------------------------------------------------------------- 1 | package example3 2 | -------------------------------------------------------------------------------- /internal/sketches/sketch3/active.zh.toml: -------------------------------------------------------------------------------- 1 | [Cats] 2 | zero = "我没有养猫。" 3 | one = "我有一只猫。" 4 | two = "我有两只猫。" 5 | other = "我有 {{.count}} 只猫。" 6 | -------------------------------------------------------------------------------- /internal/sketches/sketch3/active.en.toml: -------------------------------------------------------------------------------- 1 | [Cats] 2 | zero = "I have no cats" 3 | one = "I have one cat." 4 | two = "I have two cats." 5 | other = "I have {{.count}} cats." 6 | -------------------------------------------------------------------------------- /internal/sketches/sketch4/active.zh.toml: -------------------------------------------------------------------------------- 1 | [Cats] 2 | Description = "Cats" 3 | other = "我有 {{.count}} 只猫。" 4 | 5 | ["我有几只猫"] 6 | Description = "Cats" 7 | other = "我有 {{.猫的数量}} 只猫。" 8 | -------------------------------------------------------------------------------- /internal/sketches/sketch4/active.en.toml: -------------------------------------------------------------------------------- 1 | [Cats] 2 | Description = "Cats" 3 | one = "I have one cat." 4 | other = "I have {{.count}} cats." 5 | 6 | ["我有几只猫"] 7 | Description = "Cats" 8 | one = "I have one cat." 9 | other = "I have {{.猫的数量}} cats." 10 | -------------------------------------------------------------------------------- /internal/examples/example3/internal/message3/msg.zh-CN.yaml: -------------------------------------------------------------------------------- 1 | 早上好呀: "早上好呀{{ .某某某 }}" 2 | 吃饭了没: "吃饭了没" 3 | 我这里有个X你吃吧: "我这里有个{{ .叉叉叉 }}你吃吧" 4 | 面包: "面包" 5 | 蛋糕: "蛋糕" 6 | 祝X节日快乐: "祝{{.}}们节日快乐" 7 | 老师: "老师" 8 | 同学: "同学" 9 | 祝X某X节快乐: "祝{{.某某人}}们{{.某某节}}快乐" 10 | 春节: "春节" 11 | 元旦: "元旦" 12 | -------------------------------------------------------------------------------- /internal/examples/example1/internal/i18n/zh-CN.yaml: -------------------------------------------------------------------------------- 1 | SAY_HELLO: "你好,{{.name}}!" 2 | WELCOME: "欢迎使用此应用!" 3 | NEW_MESSAGES: "嗨,{{.name}}!您有 {{.count}} 条新消息。" 4 | SUCCESS: "成功" 5 | ERROR_BAD_PARAM: "无效参数:{{.name}}" 6 | ERROR_ALREADY_EXIST: "{{.what}} {{.code}} 已存在" 7 | ERROR_NOT_EXIST: "{{.what}} {{.code}} 不存在" 8 | PLEASE_CONFIRM: "请确认{{.}}" 9 | -------------------------------------------------------------------------------- /plural_config_test.go: -------------------------------------------------------------------------------- 1 | package goi18n_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yyle88/goi18n" 8 | ) 9 | 10 | func TestNewPluralConfig(t *testing.T) { 11 | param := goi18n.NewPluralConfig(1) 12 | t.Log(param.PluralCount) 13 | require.Equal(t, 1, param.PluralCount) 14 | } 15 | -------------------------------------------------------------------------------- /internal/examples/example3/internal/message3/msg.en-US.yaml: -------------------------------------------------------------------------------- 1 | 早上好呀: "Good morning, {{ .某某某 }}" 2 | 吃饭了没: "Have you eaten?" 3 | 我这里有个X你吃吧: "I have a {{ .叉叉叉 }} for you to eat." 4 | 面包: "Bread" 5 | 蛋糕: "Cake" 6 | 祝X节日快乐: "Happy {{.}} festival!" 7 | 老师: "Teacher" 8 | 同学: "Classmate" 9 | 祝X某X节快乐: "Happy {{.某某人}} {{.某某节}}!" 10 | 春节: "Spring Festival" 11 | 元旦: "New Year's Day" -------------------------------------------------------------------------------- /internal/examples/example2/internal/message2/trans.zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "OPEN_ISSUES": { 3 | "other": "项目中有 {{.count}} 个未解决问题。" 4 | }, 5 | "COMPLETED_TASKS": { 6 | "other": "项目中完成了 {{.count}} 个任务。" 7 | }, 8 | "PENDING_REVIEWS": { 9 | "other": "项目中有 {{.}} 个待审。" 10 | }, 11 | "ACTIVE_USERS": { 12 | "other": "项目中有 {{.count}} 个活跃用户。" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /internal/examples/example1/internal/i18n/en-US.yaml: -------------------------------------------------------------------------------- 1 | SAY_HELLO: "Hello, {{ .name }}!" 2 | WELCOME: "Welcome to this app!" 3 | NEW_MESSAGES: "Hi, {{ .name }}! You have {{ .count }} new messages." 4 | SUCCESS: "Success" 5 | ERROR_BAD_PARAM: "Invalid parameter: {{ .name }}" 6 | ERROR_ALREADY_EXIST: "{{ .what }} {{ .code }} already exists" 7 | ERROR_NOT_EXIST: "{{ .what }} {{ .code }} does not exist" 8 | PLEASE_CONFIRM: "Please confirm to {{ . }}" 9 | -------------------------------------------------------------------------------- /internal/examples/example1/internal/i18n/km-KH.yaml: -------------------------------------------------------------------------------- 1 | SAY_HELLO: "សួស្តី {{ .name }}!" 2 | WELCOME: "សូមស្វាគមន៍មកកាន់កម្មវិធីនេះ!" 3 | NEW_MESSAGES: "សួស្តី {{ .name }}! អ្នកមានសារថ្មី {{ .count }} ។" 4 | SUCCESS: "ជោគជ័យ" 5 | ERROR_BAD_PARAM: "ប៉ារ៉ាម៉ែត្រមិនត្រឹមត្រូវ៖ {{ .name }}" 6 | ERROR_ALREADY_EXIST: "{{ .what }} {{ .code }} មានរួចហើយ" 7 | ERROR_NOT_EXIST: "{{ .what }} {{ .code }} មិនមានទេ" 8 | PLEASE_CONFIRM: "សូមបញ្ជាក់ដើម្បី {{ . }}" 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | COVERAGE_DIR ?= .coverage.out 2 | 3 | # cp from: https://github.com/yyle88/gormrepo/blob/c31435669714611c9ebde6975060f48cd5634451/Makefile#L4 4 | test: 5 | @if [ -d $(COVERAGE_DIR) ]; then rm -r $(COVERAGE_DIR); fi 6 | @mkdir $(COVERAGE_DIR) 7 | make test-with-flags TEST_FLAGS='-v -race -covermode atomic -coverprofile $$(COVERAGE_DIR)/combined.txt -bench=. -benchmem -timeout 20m' 8 | 9 | test-with-flags: 10 | @go test $(TEST_FLAGS) ./... 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # env file 25 | .env 26 | -------------------------------------------------------------------------------- /internal/examples/example2/internal/message2/i18n_test.go: -------------------------------------------------------------------------------- 1 | package message2_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/yyle88/goi18n/internal/examples/example2/internal/message2" 7 | "github.com/yyle88/neatjson/neatjsons" 8 | ) 9 | 10 | func TestLoadI18nFiles(t *testing.T) { 11 | bundle, messageFiles := message2.LoadI18nFiles() 12 | t.Log(len(messageFiles)) 13 | t.Log(len(bundle.LanguageTags())) 14 | } 15 | 16 | func TestNewActiveUsers(t *testing.T) { 17 | messageID, templateValues := message2.NewActiveUsers(&message2.ActiveUsersParam{ 18 | Count: 8888, 19 | }) 20 | t.Log(messageID) 21 | t.Log(neatjsons.S(templateValues)) 22 | } 23 | -------------------------------------------------------------------------------- /internal/examples/example3/internal/message3/i18n_test.go: -------------------------------------------------------------------------------- 1 | package message3_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/yyle88/goi18n/internal/examples/example3/internal/message3" 7 | "github.com/yyle88/neatjson/neatjsons" 8 | ) 9 | 10 | func TestLoadI18nFiles(t *testing.T) { 11 | bundle, messageFiles := message3.LoadI18nFiles() 12 | t.Log(len(messageFiles)) 13 | t.Log(len(bundle.LanguageTags())) 14 | } 15 | 16 | func TestNewI祝X某X节快乐(t *testing.T) { 17 | messageID, templateValues := message3.NewI祝X某X节快乐(&message3.P祝X某X节快乐{ 18 | V某某人: "乐乐", 19 | V某某节: "天天", 20 | }) 21 | t.Log(messageID) 22 | t.Log(neatjsons.S(templateValues)) 23 | } 24 | -------------------------------------------------------------------------------- /internal/examples/example1/internal/message1/i18n_test.go: -------------------------------------------------------------------------------- 1 | package message1_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/yyle88/goi18n/internal/examples/example1/internal/message1" 7 | "github.com/yyle88/neatjson/neatjsons" 8 | ) 9 | 10 | func TestLoadI18nFiles(t *testing.T) { 11 | bundle, messageFiles := message1.LoadI18nFiles() 12 | t.Log(len(messageFiles)) 13 | t.Log(len(bundle.LanguageTags())) 14 | } 15 | 16 | func TestNewErrorAlreadyExist(t *testing.T) { 17 | messageID, templateValues := message1.NewErrorAlreadyExist(&message1.ErrorAlreadyExistParam{ 18 | What: "abc", 19 | Code: "123", 20 | }) 21 | t.Log(messageID) 22 | t.Log(neatjsons.S(templateValues)) 23 | } 24 | -------------------------------------------------------------------------------- /internal/examples/example2/internal/message2/trans.en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "OPEN_ISSUES": { 3 | "one": "There is one open issue in the project.", 4 | "other": "There are {{.count}} open issues in the project." 5 | }, 6 | "COMPLETED_TASKS": { 7 | "one": "One task is completed in the project.", 8 | "other": "{{.count}} tasks are completed in the project." 9 | }, 10 | "PENDING_REVIEWS": { 11 | "one": "There is one pending review in the project.", 12 | "other": "There are {{.}} pending reviews in the project." 13 | }, 14 | "ACTIVE_USERS": { 15 | "one": "One user is active in the project.", 16 | "other": "{{.count}} users are active in the project." 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /internal/examples/example1/internal/message1v2/i18n.gen_test.go: -------------------------------------------------------------------------------- 1 | package message1v2_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/yyle88/goi18n" 7 | "github.com/yyle88/goi18n/internal/examples/example1/internal/message1" 8 | "github.com/yyle88/neatjson/neatjsons" 9 | "github.com/yyle88/osexistpath/osmustexist" 10 | "github.com/yyle88/runpath/runtestpath" 11 | "github.com/yyle88/zaplog" 12 | ) 13 | 14 | func TestGenerate(t *testing.T) { 15 | bundle, messageFiles := message1.LoadI18nFiles() 16 | zaplog.SUG.Debugln(neatjsons.S(bundle.LanguageTags())) 17 | 18 | outputPath := osmustexist.FILE(runtestpath.SrcPath(t)) 19 | options := goi18n.NewOptions().WithOutputPathWithPkgName(outputPath) 20 | t.Log(neatjsons.S(options)) 21 | goi18n.Generate(messageFiles, options) 22 | } 23 | -------------------------------------------------------------------------------- /internal/examples/example2/internal/message2v2/i18n.gen_test.go: -------------------------------------------------------------------------------- 1 | package message2v2_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/yyle88/goi18n" 7 | "github.com/yyle88/goi18n/internal/examples/example2/internal/message2" 8 | "github.com/yyle88/neatjson/neatjsons" 9 | "github.com/yyle88/osexistpath/osmustexist" 10 | "github.com/yyle88/runpath/runtestpath" 11 | "github.com/yyle88/zaplog" 12 | ) 13 | 14 | func TestGenerate(t *testing.T) { 15 | bundle, messageFiles := message2.LoadI18nFiles() 16 | zaplog.SUG.Debugln(neatjsons.S(bundle.LanguageTags())) 17 | 18 | outputPath := osmustexist.FILE(runtestpath.SrcPath(t)) 19 | options := goi18n.NewOptions().WithOutputPathWithPkgName(outputPath) 20 | t.Log(neatjsons.S(options)) 21 | goi18n.Generate(messageFiles, options) 22 | } 23 | -------------------------------------------------------------------------------- /internal/examples/example1/internal/message1/i18n.gen_test.go: -------------------------------------------------------------------------------- 1 | package message1_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/yyle88/goi18n" 7 | "github.com/yyle88/goi18n/internal/examples/example1/internal/message1" 8 | "github.com/yyle88/neatjson/neatjsons" 9 | "github.com/yyle88/osexistpath/osmustexist" 10 | "github.com/yyle88/runpath/runtestpath" 11 | "github.com/yyle88/zaplog" 12 | ) 13 | 14 | func TestGenerate(t *testing.T) { 15 | bundle, messageFiles := message1.LoadI18nFiles() 16 | zaplog.SUG.Debugln(neatjsons.S(bundle.LanguageTags())) 17 | 18 | outputPath := osmustexist.FILE(runtestpath.SrcPath(t)) 19 | options := goi18n.NewOptions(). 20 | WithOutputPathWithPkgName(outputPath). 21 | WithGenerateNewMessage(true) 22 | t.Log(neatjsons.S(options)) 23 | goi18n.Generate(messageFiles, options) 24 | } 25 | -------------------------------------------------------------------------------- /internal/examples/example2/internal/message2/i18n.gen_test.go: -------------------------------------------------------------------------------- 1 | package message2_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/yyle88/goi18n" 7 | "github.com/yyle88/goi18n/internal/examples/example2/internal/message2" 8 | "github.com/yyle88/neatjson/neatjsons" 9 | "github.com/yyle88/osexistpath/osmustexist" 10 | "github.com/yyle88/runpath/runtestpath" 11 | "github.com/yyle88/zaplog" 12 | ) 13 | 14 | func TestGenerate(t *testing.T) { 15 | bundle, messageFiles := message2.LoadI18nFiles() 16 | zaplog.SUG.Debugln(neatjsons.S(bundle.LanguageTags())) 17 | 18 | outputPath := osmustexist.FILE(runtestpath.SrcPath(t)) 19 | options := goi18n.NewOptions(). 20 | WithOutputPathWithPkgName(outputPath). 21 | WithGenerateNewMessage(true) 22 | t.Log(neatjsons.S(options)) 23 | goi18n.Generate(messageFiles, options) 24 | } 25 | -------------------------------------------------------------------------------- /internal/examples/example3/internal/message3v2/i18n.gen_test.go: -------------------------------------------------------------------------------- 1 | package message3v2_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/yyle88/goi18n" 7 | "github.com/yyle88/goi18n/internal/examples/example3/internal/message3" 8 | "github.com/yyle88/neatjson/neatjsons" 9 | "github.com/yyle88/osexistpath/osmustexist" 10 | "github.com/yyle88/runpath/runtestpath" 11 | "github.com/yyle88/zaplog" 12 | ) 13 | 14 | func TestGenerate(t *testing.T) { 15 | bundle, messageFiles := message3.LoadI18nFiles() 16 | zaplog.SUG.Debugln(neatjsons.S(bundle.LanguageTags())) 17 | 18 | outputPath := osmustexist.FILE(runtestpath.SrcPath(t)) 19 | options := goi18n.NewOptions(). 20 | WithOutputPathWithPkgName(outputPath). 21 | WithAllowNonAsciiRune(true) 22 | t.Log(neatjsons.S(options)) 23 | goi18n.Generate(messageFiles, options) 24 | } 25 | -------------------------------------------------------------------------------- /internal/examples/example3/internal/message3/i18n.gen_test.go: -------------------------------------------------------------------------------- 1 | package message3_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/yyle88/goi18n" 7 | "github.com/yyle88/goi18n/internal/examples/example3/internal/message3" 8 | "github.com/yyle88/neatjson/neatjsons" 9 | "github.com/yyle88/osexistpath/osmustexist" 10 | "github.com/yyle88/runpath/runtestpath" 11 | "github.com/yyle88/zaplog" 12 | ) 13 | 14 | func TestGenerate(t *testing.T) { 15 | bundle, messageFiles := message3.LoadI18nFiles() 16 | zaplog.SUG.Debugln(neatjsons.S(bundle.LanguageTags())) 17 | 18 | outputPath := osmustexist.FILE(runtestpath.SrcPath(t)) 19 | options := goi18n.NewOptions(). 20 | WithOutputPathWithPkgName(outputPath). 21 | WithGenerateNewMessage(true). 22 | WithAllowNonAsciiRune(true) 23 | t.Log(neatjsons.S(options)) 24 | goi18n.Generate(messageFiles, options) 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 yangyile-yyle88 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /internal/sketches/sketch3/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | In Chinese, there’s no plural concept, so only "other" is used, and setting "One" / "Zero" or "Two" has no effect. 4 | 5 | ```go 6 | catsMessage := &i18n.Message{ 7 | ID: "Cats", 8 | Zero: "I have no cats", // doesn’t work: English only uses "one" (count=1) and "other" (all other counts) 9 | One: "I have one cat.", 10 | Two: "I have two cats.", // doesn’t work: English only uses "one" (count=1) and "other" (all other counts) 11 | Other: "I have {{.count}} cats.", 12 | } 13 | ``` 14 | 15 | This is a hidden rule. I haven’t found where this rule is coded or how to turn it off (though I’m not sure if I really want to). 16 | It’s just there by default, so we have to remember it. 17 | 18 | --- 19 | 20 | # 知识总结 21 | 22 | 中文没有复数概念,所以只能用 "other" 类别,配置 "One"、"Zero" 或 "Two" 也不会生效。 23 | 24 | ```go 25 | catsMessage := &i18n.Message{ 26 | ID: "Cats", 27 | Zero: "我没有猫", // 没用的配置,中文只用 "other" 28 | One: "我有一只猫", // 没用的配置,中文只用 "other" 29 | Two: "我有两只猫", // 没用的配置,中文只用 "other" 30 | Other: "我有 {{.count}} 只猫", 31 | } 32 | ``` 33 | 34 | 这是个隐形规则,我甚至还没找到限制的代码位置,也不知道该如何关闭它(虽然我也不知真想关闭它,我就是想探索探索)。 35 | 但它确实是默认生效的,我们只能记住这个规则。 36 | -------------------------------------------------------------------------------- /internal/examples/example1/internal/message1/i18n.go: -------------------------------------------------------------------------------- 1 | package message1 2 | 3 | import ( 4 | "github.com/nicksnyder/go-i18n/v2/i18n" 5 | "github.com/yyle88/neatjson/neatjsons" 6 | "github.com/yyle88/osexistpath/osmustexist" 7 | "github.com/yyle88/rese" 8 | "github.com/yyle88/runpath" 9 | "github.com/yyle88/zaplog" 10 | "go.uber.org/zap" 11 | "golang.org/x/text/language" 12 | "gopkg.in/yaml.v3" 13 | ) 14 | 15 | // DefaultLanguage 配置默认语言 16 | var DefaultLanguage = language.AmericanEnglish 17 | 18 | func LoadI18nFiles() (*i18n.Bundle, []*i18n.MessageFile) { 19 | bundle := i18n.NewBundle(DefaultLanguage) 20 | bundle.RegisterUnmarshalFunc("yaml", yaml.Unmarshal) 21 | 22 | var messageFiles []*i18n.MessageFile 23 | for _, locale := range []string{"en-US", "zh-CN", "km-KH"} { 24 | path := runpath.PARENT.UpTo(1, "i18n", locale+".yaml") 25 | zaplog.LOG.Debug("LOAD", zap.String("path", path)) 26 | 27 | osmustexist.MustFile(path) 28 | 29 | messageFile := rese.P1(bundle.LoadMessageFile(path)) 30 | zaplog.SUG.Debugln(neatjsons.S(messageFile)) 31 | 32 | messageFiles = append(messageFiles, messageFile) 33 | } 34 | zaplog.SUG.Debugln(neatjsons.S(bundle.LanguageTags())) 35 | return bundle, messageFiles 36 | } 37 | -------------------------------------------------------------------------------- /options_test.go: -------------------------------------------------------------------------------- 1 | package goi18n_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yyle88/goi18n" 8 | "github.com/yyle88/must/muststrings" 9 | "github.com/yyle88/neatjson/neatjsons" 10 | "github.com/yyle88/osexistpath/osmustexist" 11 | "github.com/yyle88/runpath" 12 | "github.com/yyle88/runpath/runtestpath" 13 | "github.com/yyle88/syntaxgo" 14 | ) 15 | 16 | func TestNewOptions(t *testing.T) { 17 | options := goi18n.NewOptions().WithOutputPathWithPkgName(runtestpath.SrcPath(t)) 18 | t.Log(neatjsons.S(options)) 19 | 20 | require.Equal(t, "goi18n", options.GetPkgName()) 21 | } 22 | 23 | func TestOptions_WithOutputPath(t *testing.T) { 24 | options := goi18n.NewOptions().WithOutputPath(runpath.PARENT.Join("/output/message.go")) 25 | t.Log(neatjsons.S(options)) 26 | 27 | muststrings.HasSuffix(options.GetOutputPath(), "goi18n/output/message.go") 28 | } 29 | 30 | func TestOptions_WithPkgName(t *testing.T) { 31 | options := goi18n.NewOptions() 32 | 33 | path := osmustexist.FILE(runtestpath.SrcPath(t)) 34 | pkgName := syntaxgo.GetPkgName(path) 35 | options.WithOutputPath(path).WithPkgName(pkgName) 36 | 37 | t.Log(neatjsons.S(options)) 38 | require.Equal(t, "goi18n", options.GetPkgName()) 39 | } 40 | -------------------------------------------------------------------------------- /internal/sketches/sketch1/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | This is an easy way to write code for translations. 3 | By using `{{.PluralCount}}` in the text, you can pick the right plural form and include the number directly with `PluralCount: num`. 4 | 5 | ```go 6 | localizer.MustLocalize(&i18n.LocalizeConfig{ 7 | DefaultMessage: catsMessage, 8 | PluralCount: num, 9 | // TemplateData: map[string]interface{}{ 10 | // "PluralCount": num, 11 | // }, // Since PluralCount is a special name, you can skip this part and still get the same result. 12 | }) 13 | ``` 14 | 15 | While, I think this approach might be too complex. 16 | As I improve my coding skills, I might find it more useful. But as a beginner, I was really surprised when I first saw this method. 17 | 18 | --- 19 | 20 | # 知识总结 21 | 这是种比较省略的写法 22 | 通过配置 {{.PluralCount}} 这个特殊的名字为翻译的内容,就能够借由 `PluralCount: num` 既选中复数模版还把复数的参数传到模板里 23 | 24 | ``` 25 | localizer.MustLocalize(&i18n.LocalizeConfig{ 26 | DefaultMessage: catsMessage, 27 | PluralCount: num, 28 | // TemplateData: map[string]interface{}{ 29 | // "PluralCount": num, 30 | // }, // 因为 PluralCount 已经是特殊的名字,这里就能省掉这块,达到同样的效果 31 | } 32 | ``` 33 | 34 | 但我认为这种写法没卵用,这属于是过度设计的结果 35 | 当然随着以后的熟练,我或许也会常用这种写法,但目前作为新手我只能说,初见这个方法时我感到大受震撼。 36 | -------------------------------------------------------------------------------- /internal/examples/example3/internal/message3/i18n.go: -------------------------------------------------------------------------------- 1 | package message3 2 | 3 | import ( 4 | "embed" 5 | 6 | "github.com/nicksnyder/go-i18n/v2/i18n" 7 | "github.com/yyle88/neatjson/neatjsons" 8 | "github.com/yyle88/rese" 9 | "github.com/yyle88/zaplog" 10 | "go.uber.org/zap" 11 | "golang.org/x/text/language" 12 | "gopkg.in/yaml.v3" 13 | ) 14 | 15 | // DefaultLanguage 配置默认语言 16 | var DefaultLanguage = language.AmericanEnglish 17 | 18 | //go:embed msg.en-US.yaml msg.zh-CN.yaml 19 | var files embed.FS 20 | 21 | func LoadI18nFiles() (*i18n.Bundle, []*i18n.MessageFile) { 22 | bundle := i18n.NewBundle(DefaultLanguage) 23 | bundle.RegisterUnmarshalFunc("yaml", yaml.Unmarshal) 24 | 25 | var messageFiles []*i18n.MessageFile 26 | for _, name := range []string{"msg.en-US.yaml", "msg.zh-CN.yaml"} { 27 | zaplog.LOG.Debug("LOAD", zap.String("path", name)) 28 | 29 | content := rese.A1(files.ReadFile(name)) 30 | //这里文件名 file-name 写 "active.en-US.toml" 或者 "en-US.toml" 都行,内部会通过这个解析出语言标签名称 31 | messageFile := rese.P1(bundle.ParseMessageFileBytes(content, name)) 32 | zaplog.SUG.Debugln(neatjsons.S(messageFile)) //安利下我的俩工具包 33 | 34 | messageFiles = append(messageFiles, messageFile) 35 | } 36 | 37 | zaplog.SUG.Debugln(neatjsons.S(bundle.LanguageTags())) 38 | return bundle, messageFiles 39 | } 40 | -------------------------------------------------------------------------------- /internal/examples/example2/internal/message2/i18n.go: -------------------------------------------------------------------------------- 1 | package message2 2 | 3 | import ( 4 | "embed" 5 | "encoding/json" 6 | 7 | "github.com/nicksnyder/go-i18n/v2/i18n" 8 | "github.com/yyle88/neatjson/neatjsons" 9 | "github.com/yyle88/rese" 10 | "github.com/yyle88/zaplog" 11 | "go.uber.org/zap" 12 | "golang.org/x/text/language" 13 | ) 14 | 15 | // DefaultLanguage 配置默认语言 16 | var DefaultLanguage = language.AmericanEnglish 17 | 18 | //go:embed trans.en-US.json trans.zh-CN.json 19 | var files embed.FS 20 | 21 | func LoadI18nFiles() (*i18n.Bundle, []*i18n.MessageFile) { 22 | bundle := i18n.NewBundle(DefaultLanguage) 23 | bundle.RegisterUnmarshalFunc("json", json.Unmarshal) 24 | 25 | var messageFiles []*i18n.MessageFile 26 | for _, name := range []string{"trans.en-US.json", "trans.zh-CN.json"} { 27 | zaplog.LOG.Debug("LOAD", zap.String("path", name)) 28 | 29 | content := rese.A1(files.ReadFile(name)) 30 | //这里文件名 file-name 写 "active.en-US.toml" 或者 "en-US.toml" 都行,内部会通过这个解析出语言标签名称 31 | messageFile := rese.P1(bundle.ParseMessageFileBytes(content, name)) 32 | zaplog.SUG.Debugln(neatjsons.S(messageFile)) //安利下我的俩工具包 33 | 34 | messageFiles = append(messageFiles, messageFile) 35 | } 36 | 37 | zaplog.SUG.Debugln(neatjsons.S(bundle.LanguageTags())) 38 | return bundle, messageFiles 39 | } 40 | -------------------------------------------------------------------------------- /internal/sketches/sketch1/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/nicksnyder/go-i18n/v2/i18n" 7 | "golang.org/x/text/language" 8 | ) 9 | 10 | func main() { 11 | bundle := i18n.NewBundle(language.English) 12 | localizer := i18n.NewLocalizer(bundle, "en") 13 | catsMessage := &i18n.Message{ 14 | ID: "Cats", 15 | Zero: "I have no cats", // useless: English only supports "one" (count=1) and "other" (all other counts, including 0). 16 | One: "I have one cat.", 17 | Two: "I have two cats.", // useless: English only supports "one" (count=1) and "other" (all other counts, including 0). 18 | Other: "I have {{.PluralCount}} cats.", 19 | } 20 | // According to Unicode CLDR plural rules for English, the "zero" category is not defined. 21 | // When PluralCount is 0, go - i18n selects the "other" message ("I have 0 cats.") instead 22 | // of the "zero" message ("I have no cats"), as English only supports "one" (count=1) and 23 | // "other" (all other counts, including 0). 24 | for num := 0; num <= 3; num++ { 25 | fmt.Println(localizer.MustLocalize(&i18n.LocalizeConfig{ 26 | DefaultMessage: catsMessage, 27 | PluralCount: num, 28 | })) 29 | } 30 | //Output: 31 | //I have 0 cats. 32 | //I have one cat. 33 | //I have 2 cats. 34 | //I have 3 cats. 35 | } 36 | -------------------------------------------------------------------------------- /internal/sketches/sketch2/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/nicksnyder/go-i18n/v2/i18n" 7 | "golang.org/x/text/language" 8 | ) 9 | 10 | func main() { 11 | bundle := i18n.NewBundle(language.English) 12 | localizer := i18n.NewLocalizer(bundle, "en") 13 | catsMessage := &i18n.Message{ 14 | ID: "Cats", 15 | Zero: "I have no cats", // useless: English only supports "one" (count=1) and "other" (all other counts, including 0). 16 | One: "I have one cat.", 17 | Two: "I have two cats.", // useless: English only supports "one" (count=1) and "other" (all other counts, including 0). 18 | Other: "I have {{.count}} cats.", 19 | } 20 | // According to Unicode CLDR plural rules for English, the "zero" category is not defined. 21 | // When PluralCount is 0, go - i18n selects the "other" message ("I have 0 cats.") instead 22 | // of the "zero" message ("I have no cats"), as English only supports "one" (count=1) and 23 | // "other" (all other counts, including 0). 24 | for num := 0; num <= 3; num++ { 25 | fmt.Println(localizer.MustLocalize(&i18n.LocalizeConfig{ 26 | DefaultMessage: catsMessage, 27 | PluralCount: num, // this value is used to choose one/many template 28 | TemplateData: map[string]interface{}{ 29 | "count": num, // this value is used to set value into the template 30 | }, 31 | })) 32 | } 33 | //Output: 34 | //I have 0 cats. 35 | //I have one cat. 36 | //I have 2 cats. 37 | //I have 3 cats. 38 | } 39 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/yyle88/goi18n 2 | 3 | go 1.22.8 4 | 5 | require ( 6 | github.com/BurntSushi/toml v1.5.0 7 | github.com/emirpasic/gods/v2 v2.0.0-alpha 8 | github.com/iancoleman/strcase v0.3.0 9 | github.com/nicksnyder/go-i18n/v2 v2.5.1 10 | github.com/stretchr/testify v1.11.1 11 | github.com/yyle88/formatgo v1.0.27 12 | github.com/yyle88/must v0.0.26 13 | github.com/yyle88/neatjson v0.0.13 14 | github.com/yyle88/osexistpath v0.0.18 15 | github.com/yyle88/printgo v1.0.6 16 | github.com/yyle88/rese v0.0.11 17 | github.com/yyle88/runpath v1.0.24 18 | github.com/yyle88/sortx v1.0.10 19 | github.com/yyle88/syntaxgo v0.0.53 20 | github.com/yyle88/tern v0.0.9 21 | github.com/yyle88/zaplog v0.0.27 22 | go.uber.org/zap v1.27.0 23 | golang.org/x/text v0.22.0 24 | gopkg.in/yaml.v3 v3.0.1 25 | ) 26 | 27 | require ( 28 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 29 | github.com/pkg/errors v0.9.1 // indirect 30 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 31 | github.com/yyle88/done v1.0.27 // indirect 32 | github.com/yyle88/erero v1.0.24 // indirect 33 | github.com/yyle88/mutexmap v1.0.14 // indirect 34 | github.com/yyle88/sure v0.0.40 // indirect 35 | go.uber.org/multierr v1.11.0 // indirect 36 | golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect 37 | golang.org/x/mod v0.23.0 // indirect 38 | golang.org/x/sync v0.11.0 // indirect 39 | golang.org/x/tools v0.30.0 // indirect 40 | ) 41 | -------------------------------------------------------------------------------- /internal/sketches/sketch4/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/BurntSushi/toml" 7 | "github.com/nicksnyder/go-i18n/v2/i18n" 8 | "github.com/yyle88/neatjson/neatjsons" 9 | "github.com/yyle88/rese" 10 | "github.com/yyle88/runpath" 11 | "github.com/yyle88/zaplog" 12 | "golang.org/x/text/language" 13 | ) 14 | 15 | func main() { 16 | // 创建语言包 17 | bundle := i18n.NewBundle(language.English) 18 | bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) 19 | { 20 | messageFile := rese.P1(bundle.LoadMessageFile(runpath.PARENT.Join("active.en.toml"))) // 加载英文翻译文件 21 | zaplog.SUG.Debugln(neatjsons.S(messageFile)) 22 | } 23 | { 24 | messageFile := rese.P1(bundle.LoadMessageFile(runpath.PARENT.Join("active.zh.toml"))) // 加载中文翻译文件 25 | zaplog.SUG.Debugln(neatjsons.S(messageFile)) 26 | } 27 | 28 | zaplog.SUG.Debugln("---") 29 | 30 | { 31 | localizers := []*i18n.Localizer{ 32 | i18n.NewLocalizer(bundle, "en"), 33 | i18n.NewLocalizer(bundle, "zh"), 34 | } 35 | for num := 0; num <= 3; num++ { 36 | for _, localizer := range localizers { 37 | fmt.Println(localizer.MustLocalize(&i18n.LocalizeConfig{ 38 | MessageID: "Cats", 39 | PluralCount: num, 40 | TemplateData: map[string]interface{}{ 41 | "count": num, 42 | }, 43 | })) 44 | fmt.Println(localizer.MustLocalize(&i18n.LocalizeConfig{ 45 | MessageID: "我有几只猫", 46 | PluralCount: num, 47 | TemplateData: map[string]interface{}{ 48 | "猫的数量": num, 49 | }, 50 | })) 51 | } 52 | } 53 | } 54 | 55 | zaplog.SUG.Debugln("---") 56 | } 57 | -------------------------------------------------------------------------------- /plural_config.go: -------------------------------------------------------------------------------- 1 | package goi18n 2 | 3 | type numType interface { 4 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64 5 | } 6 | 7 | // PluralConfig provides count param for messages with one/other forms 8 | // Uses PluralCount to select between "one" and "other" translations 9 | // Note: Chinese and some languages use "other" for all counts 10 | // 11 | // CLDR defines different plural rules for languages: 12 | // - English: one (1), other (others) - "1 apple", "3 apples" 13 | // - Russian: one, few, many, other - 1 яблоко, 2 яблока, 5 яблок 14 | // - Arabic: zero, one, two, few, many, other - 6 categories 15 | // - Chinese: other - all counts use "other" 16 | // 17 | // PluralConfig 配置复数翻译的参数,通过数量选择使用 "one" 还是 "other" 翻译,然后填充模板参数完成语句 18 | // 19 | // 但是在使用中发现,中文没有单复数形式的翻译,即使配置也无法使用,原因: 20 | // 21 | // CLDR 如何定义其他语言的复数规则? 22 | // 不同语言的复数规则差异很大,例如: 23 | // 24 | // 语言 复数规则类别 示例(Count=0,1,2,5) 25 | // 英语 one(1), other(其他) "1 apple", "3 apples" 26 | // 俄语 one, few, many, other 1 яблоко, 2 яблока, 5 яблок 27 | // 阿拉伯语 zero, one, two, few, many, other 6 种分类 28 | // 中文 other 所有数量均用 other 29 | // 30 | // 因此结论是,很多语言是不支持单复数的,即使配置也是无效的,因此建议中文用户就不要分别配置单复数翻译啦 31 | type PluralConfig struct { 32 | PluralCount any 33 | } 34 | 35 | // NewPluralConfig creates PluralConfig with numeric count 36 | // Accepts any numeric type through generic constraint 37 | // 38 | // NewPluralConfig 创建带数值计数的 PluralConfig 39 | // 通过泛型约束接受任意数值类型 40 | func NewPluralConfig[Num numType](pluralCount Num) *PluralConfig { 41 | return &PluralConfig{ 42 | PluralCount: pluralCount, 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /internal/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "unicode" 5 | ) 6 | 7 | // HasNonASCII checks if string contains non-ASCII characters 8 | // HasNonASCII 检查字符串是否包含非 ASCII 字符 9 | func HasNonASCII(s string) bool { 10 | for _, c := range s { 11 | if c > unicode.MaxASCII { 12 | return true 13 | } 14 | } 15 | return false 16 | } 17 | 18 | // HasLetterPrefix checks if string starts with ASCII letter 19 | // HasLetterPrefix 检查字符串是否以 ASCII 字母开头 20 | func HasLetterPrefix(s string) bool { 21 | if len(s) == 0 { 22 | return false 23 | } 24 | runes := []rune(s) 25 | c := runes[0] 26 | return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z') 27 | } 28 | 29 | // CapitalizeFirst capitalizes first character of string 30 | // CapitalizeFirst 将字符串首字母大写 31 | func CapitalizeFirst(s string) string { 32 | if len(s) == 0 { 33 | return s 34 | } 35 | runes := []rune(s) 36 | c := runes[0] 37 | runes[0] = unicode.ToUpper(c) 38 | return string(runes) 39 | } 40 | 41 | // DefaultUnicodeMessageName converts message ID to function name 42 | // DefaultUnicodeMessageName 将消息 ID 转换为函数名 43 | func DefaultUnicodeMessageName(messageID string) string { 44 | s := messageID 45 | if HasLetterPrefix(s) { 46 | return CapitalizeFirst(s) 47 | } 48 | return "I" + s 49 | } 50 | 51 | // DefaultUnicodeStructName converts message ID to struct name 52 | // DefaultUnicodeStructName 将消息 ID 转换为结构体名 53 | func DefaultUnicodeStructName(messageID string) string { 54 | s := messageID 55 | if HasLetterPrefix(s) { 56 | return CapitalizeFirst(s) 57 | } 58 | return "P" + s 59 | } 60 | 61 | // DefaultUnicodeFieldName converts param name to field name 62 | // DefaultUnicodeFieldName 将参数名转换为字段名 63 | func DefaultUnicodeFieldName(paramName string) string { 64 | s := paramName 65 | if HasLetterPrefix(s) { 66 | return CapitalizeFirst(s) 67 | } 68 | return "V" + s 69 | } 70 | -------------------------------------------------------------------------------- /internal/sketches/sketch2/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This is a simple way to write translation code. 4 | By using `{{.count}}` in the text, you can add the number to the translation. 5 | The `PluralCount: num` picks the right plural form, and `TemplateData` with `"count": num` puts the number into the template. 6 | In English, only "one" (for count=1) and "other" (for all other counts, including 0) are used. 7 | So, setting "Zero" or "Two" in the code doesn’t work for English. 8 | Similarly, in Chinese, there’s no plural concept, so only "other" is used, and setting "One" / "Zero" or "Two" has no effect. 9 | 10 | ```go 11 | catsMessage := &i18n.Message{ 12 | ID: "Cats", 13 | Zero: "I have no cats", // doesn’t work: English only uses "one" (count=1) and "other" (all other counts) 14 | One: "I have one cat.", 15 | Two: "I have two cats.", // doesn’t work: English only uses "one" (count=1) and "other" (all other counts) 16 | Other: "I have {{.count}} cats.", 17 | } 18 | ``` 19 | 20 | This design feels a bit strange. When I first tried it, I spent hours wondering why the "One" translation for Chinese didn’t work, thinking my code was wrong. 21 | As a beginner, I found this approach confusing. I’d rather make my own mistakes than be limited by these hidden rules. 22 | But now, we can’t change the rules, so we just have to remember them. 23 | 24 | --- 25 | 26 | # 知识总结 27 | 28 | 虽然 i18n 允许配置 zero、one、two、few、many、other 这些复数类别,但在英文里只用 "one" (count=1) 和 "other" (其他所有计数),其他配置没用。 29 | 配置 "Zero" 或 "Two" 在英文里不起作用。 30 | 这是 `Unicode CLDR plural rules` 的规则限制。 31 | 同理,中文没有复数概念,所以只能用 "other" 类别,配置 "One"、"Zero" 或 "Two" 也不会生效。 32 | 33 | ```go 34 | catsMessage := &i18n.Message{ 35 | ID: "Cats", 36 | Zero: "我没有猫", // 没用的配置,中文只用 "other" 37 | One: "我有一只猫", // 没用的配置,中文只用 "other" 38 | Two: "我有两只猫", // 没用的配置,中文只用 "other" 39 | Other: "我有 {{.count}} 只猫", 40 | } 41 | ``` 42 | 43 | 这种设计真的很神奇。我一开始花了两个小时,甚至更久,研究为什么中文的 "One" 单数翻译不生效,还以为是自己的代码有问题。 44 | 这设计挺让人困惑的,作为新手,我宁愿自己犯错,也不喜欢被这种隐形规则限制。 45 | 但是现在,我们也没法去纠正它,只能记住这个规则。 46 | -------------------------------------------------------------------------------- /internal/sketches/sketch4/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | In this example, I wanted to try what happens if the `MessageID` is in Chinese and if the template parameter key name is in Chinese. 4 | It turns out this works, and it’s the same as using English. 5 | 6 | ```toml 7 | [Cats] 8 | Description = "Cats" 9 | other = "我有 {{.count}} 只猫。" 10 | 11 | ["我有几只猫"] 12 | Description = "Cats" 13 | other = "我有 {{.猫的数量}} 只猫。" 14 | ``` 15 | 16 | ```go 17 | fmt.Println(localizer.MustLocalize(&i18n.LocalizeConfig{ 18 | MessageID: "Cats", 19 | PluralCount: num, 20 | TemplateData: map[string]interface{}{ 21 | "count": num, 22 | }, 23 | })) 24 | fmt.Println(localizer.MustLocalize(&i18n.LocalizeConfig{ 25 | MessageID: "我有几只猫", 26 | PluralCount: num, 27 | TemplateData: map[string]interface{}{ 28 | "猫的数量": num, 29 | }, 30 | })) 31 | ``` 32 | 33 | This works well. It lets me do things like writing message configurations mainly in Chinese and then adding English translations. 34 | Since my team and customers mostly use Chinese, we usually focus on Chinese prompts and error messages first. We add English translations when the project is about 80% done. 35 | So, this experiment is invaluable. 36 | 37 | --- 38 | 39 | # 知识总结 40 | 41 | 在这个例子里,我想试试假如 "MessageID" 是【中文】的会怎么样,和,假如模板参数的键名称为【中文】是怎么样的。 42 | 实践证明这也是能运行的,而且和配置成英文是等效的。 43 | 44 | ```toml 45 | [Cats] 46 | Description = "Cats" 47 | other = "我有 {{.count}} 只猫。" 48 | 49 | ["我有几只猫"] 50 | Description = "Cats" 51 | other = "我有 {{.猫的数量}} 只猫。" 52 | ``` 53 | 54 | ```go 55 | fmt.Println(localizer.MustLocalize(&i18n.LocalizeConfig{ 56 | MessageID: "Cats", 57 | PluralCount: num, 58 | TemplateData: map[string]interface{}{ 59 | "count": num, 60 | }, 61 | })) 62 | fmt.Println(localizer.MustLocalize(&i18n.LocalizeConfig{ 63 | MessageID: "我有几只猫", 64 | PluralCount: num, 65 | TemplateData: map[string]interface{}{ 66 | "猫的数量": num, 67 | }, 68 | })) 69 | ``` 70 | 71 | 目前看来这确实是可以的,这样就能让我做很多事情,比如把消息的配置文件写成以中文为主体的的,再辅以英文翻译。 72 | 由于我的同事都是中文开发者,而我们的客户大多也是用中文的,我们通常开发思路就是先管好中文的提示语和报错信息,在项目开发至80%以上的时候再去翻译英语。 73 | 因此这样的探索还是很有必要的。 74 | -------------------------------------------------------------------------------- /internal/examples/example2/internal/message2v2/i18n.gen.go: -------------------------------------------------------------------------------- 1 | package message2v2 2 | 3 | import ( 4 | "github.com/nicksnyder/go-i18n/v2/i18n" 5 | "github.com/yyle88/goi18n" 6 | ) 7 | 8 | type ActiveUsersParam struct { 9 | Count any 10 | } 11 | 12 | func (p *ActiveUsersParam) GetTemplateValues() map[string]any { 13 | res := make(map[string]any) 14 | if p.Count != nil { 15 | res["count"] = p.Count 16 | } 17 | return res 18 | } 19 | 20 | func I18nActiveUsers(data *ActiveUsersParam, pluralConfig *goi18n.PluralConfig) *i18n.LocalizeConfig { 21 | const messageID = "ACTIVE_USERS" 22 | var valuesMap = data.GetTemplateValues() 23 | return &i18n.LocalizeConfig{ 24 | MessageID: messageID, 25 | TemplateData: valuesMap, 26 | PluralCount: pluralConfig.PluralCount, 27 | } 28 | } 29 | 30 | type CompletedTasksParam struct { 31 | Count any 32 | } 33 | 34 | func (p *CompletedTasksParam) GetTemplateValues() map[string]any { 35 | res := make(map[string]any) 36 | if p.Count != nil { 37 | res["count"] = p.Count 38 | } 39 | return res 40 | } 41 | 42 | func I18nCompletedTasks(data *CompletedTasksParam, pluralConfig *goi18n.PluralConfig) *i18n.LocalizeConfig { 43 | const messageID = "COMPLETED_TASKS" 44 | var valuesMap = data.GetTemplateValues() 45 | return &i18n.LocalizeConfig{ 46 | MessageID: messageID, 47 | TemplateData: valuesMap, 48 | PluralCount: pluralConfig.PluralCount, 49 | } 50 | } 51 | 52 | type OpenIssuesParam struct { 53 | Count any 54 | } 55 | 56 | func (p *OpenIssuesParam) GetTemplateValues() map[string]any { 57 | res := make(map[string]any) 58 | if p.Count != nil { 59 | res["count"] = p.Count 60 | } 61 | return res 62 | } 63 | 64 | func I18nOpenIssues(data *OpenIssuesParam, pluralConfig *goi18n.PluralConfig) *i18n.LocalizeConfig { 65 | const messageID = "OPEN_ISSUES" 66 | var valuesMap = data.GetTemplateValues() 67 | return &i18n.LocalizeConfig{ 68 | MessageID: messageID, 69 | TemplateData: valuesMap, 70 | PluralCount: pluralConfig.PluralCount, 71 | } 72 | } 73 | 74 | func I18nPendingReviews[Value comparable](value Value, pluralConfig *goi18n.PluralConfig) *i18n.LocalizeConfig { 75 | const messageID = "PENDING_REVIEWS" 76 | var tempValue = value 77 | return &i18n.LocalizeConfig{ 78 | MessageID: messageID, 79 | TemplateData: tempValue, 80 | PluralCount: pluralConfig.PluralCount, 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /internal/sketches/sketch3/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/BurntSushi/toml" 7 | "github.com/nicksnyder/go-i18n/v2/i18n" 8 | "github.com/yyle88/neatjson/neatjsons" 9 | "github.com/yyle88/rese" 10 | "github.com/yyle88/runpath" 11 | "github.com/yyle88/zaplog" 12 | "golang.org/x/text/language" 13 | ) 14 | 15 | func main() { 16 | // 创建语言包 17 | bundle := i18n.NewBundle(language.English) 18 | bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) 19 | { 20 | messageFile := rese.P1(bundle.LoadMessageFile(runpath.PARENT.Join("active.en.toml"))) // 加载英文翻译文件 21 | zaplog.SUG.Debugln(neatjsons.S(messageFile)) 22 | } 23 | { 24 | messageFile := rese.P1(bundle.LoadMessageFile(runpath.PARENT.Join("active.zh.toml"))) // 加载中文翻译文件 25 | zaplog.SUG.Debugln(neatjsons.S(messageFile)) 26 | } 27 | 28 | zaplog.SUG.Debugln("---") 29 | 30 | { 31 | localizer := i18n.NewLocalizer(bundle, "en") 32 | // According to Unicode CLDR plural rules for English, the "zero" category is not defined. 33 | // When PluralCount is 0, go - i18n selects the "other" message ("I have 0 cats.") instead 34 | // of the "zero" message ("I have no cats"), as English only supports "one" (count=1) and 35 | // "other" (all other counts, including 0). 36 | for num := 0; num <= 3; num++ { 37 | fmt.Println(localizer.MustLocalize(&i18n.LocalizeConfig{ 38 | MessageID: "Cats", 39 | PluralCount: num, // this value is used to choose one/many template 40 | TemplateData: map[string]interface{}{ 41 | "count": num, // this value is used to set value into the template 42 | }, 43 | })) 44 | } 45 | //Output: 46 | //I have 0 cats. 47 | //I have one cat. 48 | //I have 2 cats. 49 | //I have 3 cats. 50 | } 51 | 52 | zaplog.SUG.Debugln("---") 53 | 54 | { 55 | localizer := i18n.NewLocalizer(bundle, "zh") 56 | // According to Unicode CLDR plural rules for Chinese, only the "other" category is defined. 57 | // When PluralCount is 0, 1, or any other value, go - i18n selects the "other" message 58 | // (e.g., "我有 0 只猫。") as Chinese does not distinguish "zero", "one", or other plural forms. 59 | for num := 0; num <= 3; num++ { 60 | fmt.Println(localizer.MustLocalize(&i18n.LocalizeConfig{ 61 | MessageID: "Cats", 62 | PluralCount: num, // this value is used to choose one/many template 63 | TemplateData: map[string]interface{}{ 64 | "count": num, // this value is used to set value into the template 65 | }, 66 | })) 67 | } 68 | //Output: 69 | //我有 0 只猫。 70 | //我有 1 只猫。 71 | //我有 2 只猫。 72 | //我有 3 只猫。 73 | } 74 | 75 | zaplog.SUG.Debugln("---") 76 | } 77 | -------------------------------------------------------------------------------- /internal/examples/example2/internal/message2/i18n.gen.go: -------------------------------------------------------------------------------- 1 | package message2 2 | 3 | import ( 4 | "github.com/nicksnyder/go-i18n/v2/i18n" 5 | "github.com/yyle88/goi18n" 6 | ) 7 | 8 | type ActiveUsersParam struct { 9 | Count any 10 | } 11 | 12 | func (p *ActiveUsersParam) GetTemplateValues() map[string]any { 13 | res := make(map[string]any) 14 | if p.Count != nil { 15 | res["count"] = p.Count 16 | } 17 | return res 18 | } 19 | 20 | func NewActiveUsers(data *ActiveUsersParam) (string, map[string]any) { 21 | return "ACTIVE_USERS", data.GetTemplateValues() 22 | } 23 | 24 | func I18nActiveUsers(data *ActiveUsersParam, pluralConfig *goi18n.PluralConfig) *i18n.LocalizeConfig { 25 | messageID, valuesMap := NewActiveUsers(data) 26 | return &i18n.LocalizeConfig{ 27 | MessageID: messageID, 28 | TemplateData: valuesMap, 29 | PluralCount: pluralConfig.PluralCount, 30 | } 31 | } 32 | 33 | type CompletedTasksParam struct { 34 | Count any 35 | } 36 | 37 | func (p *CompletedTasksParam) GetTemplateValues() map[string]any { 38 | res := make(map[string]any) 39 | if p.Count != nil { 40 | res["count"] = p.Count 41 | } 42 | return res 43 | } 44 | 45 | func NewCompletedTasks(data *CompletedTasksParam) (string, map[string]any) { 46 | return "COMPLETED_TASKS", data.GetTemplateValues() 47 | } 48 | 49 | func I18nCompletedTasks(data *CompletedTasksParam, pluralConfig *goi18n.PluralConfig) *i18n.LocalizeConfig { 50 | messageID, valuesMap := NewCompletedTasks(data) 51 | return &i18n.LocalizeConfig{ 52 | MessageID: messageID, 53 | TemplateData: valuesMap, 54 | PluralCount: pluralConfig.PluralCount, 55 | } 56 | } 57 | 58 | type OpenIssuesParam struct { 59 | Count any 60 | } 61 | 62 | func (p *OpenIssuesParam) GetTemplateValues() map[string]any { 63 | res := make(map[string]any) 64 | if p.Count != nil { 65 | res["count"] = p.Count 66 | } 67 | return res 68 | } 69 | 70 | func NewOpenIssues(data *OpenIssuesParam) (string, map[string]any) { 71 | return "OPEN_ISSUES", data.GetTemplateValues() 72 | } 73 | 74 | func I18nOpenIssues(data *OpenIssuesParam, pluralConfig *goi18n.PluralConfig) *i18n.LocalizeConfig { 75 | messageID, valuesMap := NewOpenIssues(data) 76 | return &i18n.LocalizeConfig{ 77 | MessageID: messageID, 78 | TemplateData: valuesMap, 79 | PluralCount: pluralConfig.PluralCount, 80 | } 81 | } 82 | 83 | func NewPendingReviews[Value comparable](value Value) (string, Value) { 84 | return "PENDING_REVIEWS", value 85 | } 86 | 87 | func I18nPendingReviews[Value comparable](value Value, pluralConfig *goi18n.PluralConfig) *i18n.LocalizeConfig { 88 | messageID, tempValue := NewPendingReviews(value) 89 | return &i18n.LocalizeConfig{ 90 | MessageID: messageID, 91 | TemplateData: tempValue, 92 | PluralCount: pluralConfig.PluralCount, 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /internal/examples/example3/internal/message3v2/i18n.gen.go: -------------------------------------------------------------------------------- 1 | package message3v2 2 | 3 | import ( 4 | "github.com/nicksnyder/go-i18n/v2/i18n" 5 | ) 6 | 7 | func I18nI元旦() *i18n.LocalizeConfig { 8 | const messageID = "元旦" 9 | return &i18n.LocalizeConfig{ 10 | MessageID: messageID, 11 | } 12 | } 13 | 14 | func I18nI吃饭了没() *i18n.LocalizeConfig { 15 | const messageID = "吃饭了没" 16 | return &i18n.LocalizeConfig{ 17 | MessageID: messageID, 18 | } 19 | } 20 | 21 | func I18nI同学() *i18n.LocalizeConfig { 22 | const messageID = "同学" 23 | return &i18n.LocalizeConfig{ 24 | MessageID: messageID, 25 | } 26 | } 27 | 28 | type P我这里有个X你吃吧 struct { 29 | V叉叉叉 any 30 | } 31 | 32 | func (p *P我这里有个X你吃吧) GetTemplateValues() map[string]any { 33 | res := make(map[string]any) 34 | if p.V叉叉叉 != nil { 35 | res["叉叉叉"] = p.V叉叉叉 36 | } 37 | return res 38 | } 39 | 40 | func I18nI我这里有个X你吃吧(data *P我这里有个X你吃吧) *i18n.LocalizeConfig { 41 | const messageID = "我这里有个X你吃吧" 42 | var valuesMap = data.GetTemplateValues() 43 | return &i18n.LocalizeConfig{ 44 | MessageID: messageID, 45 | TemplateData: valuesMap, 46 | } 47 | } 48 | 49 | type P早上好呀 struct { 50 | V某某某 any 51 | } 52 | 53 | func (p *P早上好呀) GetTemplateValues() map[string]any { 54 | res := make(map[string]any) 55 | if p.V某某某 != nil { 56 | res["某某某"] = p.V某某某 57 | } 58 | return res 59 | } 60 | 61 | func I18nI早上好呀(data *P早上好呀) *i18n.LocalizeConfig { 62 | const messageID = "早上好呀" 63 | var valuesMap = data.GetTemplateValues() 64 | return &i18n.LocalizeConfig{ 65 | MessageID: messageID, 66 | TemplateData: valuesMap, 67 | } 68 | } 69 | 70 | func I18nI春节() *i18n.LocalizeConfig { 71 | const messageID = "春节" 72 | return &i18n.LocalizeConfig{ 73 | MessageID: messageID, 74 | } 75 | } 76 | 77 | type P祝X某X节快乐 struct { 78 | V某某人 any 79 | V某某节 any 80 | } 81 | 82 | func (p *P祝X某X节快乐) GetTemplateValues() map[string]any { 83 | res := make(map[string]any) 84 | if p.V某某人 != nil { 85 | res["某某人"] = p.V某某人 86 | } 87 | if p.V某某节 != nil { 88 | res["某某节"] = p.V某某节 89 | } 90 | return res 91 | } 92 | 93 | func I18nI祝X某X节快乐(data *P祝X某X节快乐) *i18n.LocalizeConfig { 94 | const messageID = "祝X某X节快乐" 95 | var valuesMap = data.GetTemplateValues() 96 | return &i18n.LocalizeConfig{ 97 | MessageID: messageID, 98 | TemplateData: valuesMap, 99 | } 100 | } 101 | 102 | func I18nI祝X节日快乐[Value comparable](value Value) *i18n.LocalizeConfig { 103 | const messageID = "祝X节日快乐" 104 | var tempValue = value 105 | return &i18n.LocalizeConfig{ 106 | MessageID: messageID, 107 | TemplateData: tempValue, 108 | } 109 | } 110 | 111 | func I18nI老师() *i18n.LocalizeConfig { 112 | const messageID = "老师" 113 | return &i18n.LocalizeConfig{ 114 | MessageID: messageID, 115 | } 116 | } 117 | 118 | func I18nI蛋糕() *i18n.LocalizeConfig { 119 | const messageID = "蛋糕" 120 | return &i18n.LocalizeConfig{ 121 | MessageID: messageID, 122 | } 123 | } 124 | 125 | func I18nI面包() *i18n.LocalizeConfig { 126 | const messageID = "面包" 127 | return &i18n.LocalizeConfig{ 128 | MessageID: messageID, 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: create-release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main # 监听 main 分支的 push 操作(编译和测试/代码检查) 7 | tags: 8 | - 'v*' # 监听以 'v' 开头的标签的 push 操作(发布 Release) 9 | 10 | jobs: 11 | lint: 12 | name: lint 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-go@v5 17 | with: 18 | go-version: stable 19 | cache: true 20 | - name: golangci-lint 21 | uses: golangci/golangci-lint-action@v8 22 | with: 23 | version: latest 24 | args: --timeout=5m 25 | 26 | test: 27 | runs-on: ubuntu-latest 28 | strategy: 29 | matrix: 30 | go: [ "1.22.x", "1.23.x", "1.24.x", "stable" ] 31 | steps: 32 | - uses: actions/checkout@v4 33 | 34 | - uses: actions/setup-go@v5 35 | with: 36 | go-version: ${{ matrix.go }} 37 | cache: true 38 | 39 | - name: Run govulncheck 40 | uses: golang/govulncheck-action@v1 41 | with: 42 | go-version-input: ${{ matrix.go }} 43 | go-package: ./... 44 | continue-on-error: true # 报错时允许工作流继续执行,因为项目依赖的底层包也会有错,很难做到百分百没问题,只打印检测结果就行 45 | 46 | - name: Run test 47 | run: make test COVERAGE_DIR=/tmp/coverage 48 | 49 | - name: Upload test results 50 | uses: actions/upload-artifact@v4 51 | if: always() 52 | with: 53 | name: test-results-${{ matrix.go }} 54 | path: /tmp/coverage/ 55 | retention-days: 30 56 | 57 | - name: Send goveralls coverage 58 | uses: shogo82148/actions-goveralls@v1 59 | with: 60 | path-to-profile: /tmp/coverage/combined.txt 61 | flag-name: Go-${{ matrix.go }} 62 | parallel: true 63 | if: ${{ github.event.repository.fork == false }} # 仅在非 fork 时上传覆盖率 64 | 65 | check-coverage: 66 | name: Check coverage 67 | needs: [ test ] 68 | runs-on: ubuntu-latest 69 | steps: 70 | - uses: shogo82148/actions-goveralls@v1 71 | with: 72 | parallel-finished: true 73 | if: ${{ github.event.repository.fork == false }} # 仅在非 fork 时检查覆盖率 74 | 75 | # 代码质量分析 76 | code-analysis: 77 | name: CodeQL Analysis 78 | runs-on: ubuntu-latest 79 | permissions: 80 | actions: read 81 | contents: read 82 | security-events: write 83 | steps: 84 | - name: Checkout repository 85 | uses: actions/checkout@v4 86 | 87 | - name: Initialize CodeQL 88 | uses: github/codeql-action/init@v3 89 | with: 90 | languages: go 91 | 92 | - name: Auto Build 93 | uses: github/codeql-action/autobuild@v3 94 | 95 | - name: Perform CodeQL Analysis 96 | uses: github/codeql-action/analyze@v3 97 | 98 | # 发布 Release 99 | release: 100 | name: Release a new version 101 | needs: [ lint, test, check-coverage, code-analysis ] 102 | runs-on: ubuntu-latest 103 | # 仅在推送标签时执行 - && - 仅在非 fork 时执行发布 104 | if: ${{ github.event.repository.fork == false && success() && startsWith(github.ref, 'refs/tags/v') }} 105 | steps: 106 | # 1. 检出代码 107 | - name: Checkout code 108 | uses: actions/checkout@v4 109 | with: 110 | fetch-depth: 0 # 获取完整历史用于生成更好的 release notes 111 | 112 | # 2. 创建 Release 和上传源码包 113 | - name: Create Release 114 | uses: softprops/action-gh-release@v2 115 | with: 116 | generate_release_notes: true 117 | env: 118 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 119 | -------------------------------------------------------------------------------- /internal/examples/example1/internal/message1v2/i18n.gen.go: -------------------------------------------------------------------------------- 1 | package message1v2 2 | 3 | import ( 4 | "github.com/nicksnyder/go-i18n/v2/i18n" 5 | ) 6 | 7 | type ErrorAlreadyExistParam struct { 8 | What any 9 | Code any 10 | } 11 | 12 | func (p *ErrorAlreadyExistParam) GetTemplateValues() map[string]any { 13 | res := make(map[string]any) 14 | if p.What != nil { 15 | res["what"] = p.What 16 | } 17 | if p.Code != nil { 18 | res["code"] = p.Code 19 | } 20 | return res 21 | } 22 | 23 | func I18nErrorAlreadyExist(data *ErrorAlreadyExistParam) *i18n.LocalizeConfig { 24 | const messageID = "ERROR_ALREADY_EXIST" 25 | var valuesMap = data.GetTemplateValues() 26 | return &i18n.LocalizeConfig{ 27 | MessageID: messageID, 28 | TemplateData: valuesMap, 29 | } 30 | } 31 | 32 | type ErrorBadParamParam struct { 33 | Name any 34 | } 35 | 36 | func (p *ErrorBadParamParam) GetTemplateValues() map[string]any { 37 | res := make(map[string]any) 38 | if p.Name != nil { 39 | res["name"] = p.Name 40 | } 41 | return res 42 | } 43 | 44 | func I18nErrorBadParam(data *ErrorBadParamParam) *i18n.LocalizeConfig { 45 | const messageID = "ERROR_BAD_PARAM" 46 | var valuesMap = data.GetTemplateValues() 47 | return &i18n.LocalizeConfig{ 48 | MessageID: messageID, 49 | TemplateData: valuesMap, 50 | } 51 | } 52 | 53 | type ErrorNotExistParam struct { 54 | What any 55 | Code any 56 | } 57 | 58 | func (p *ErrorNotExistParam) GetTemplateValues() map[string]any { 59 | res := make(map[string]any) 60 | if p.What != nil { 61 | res["what"] = p.What 62 | } 63 | if p.Code != nil { 64 | res["code"] = p.Code 65 | } 66 | return res 67 | } 68 | 69 | func I18nErrorNotExist(data *ErrorNotExistParam) *i18n.LocalizeConfig { 70 | const messageID = "ERROR_NOT_EXIST" 71 | var valuesMap = data.GetTemplateValues() 72 | return &i18n.LocalizeConfig{ 73 | MessageID: messageID, 74 | TemplateData: valuesMap, 75 | } 76 | } 77 | 78 | type NewMessagesParam struct { 79 | Name any 80 | Count any 81 | } 82 | 83 | func (p *NewMessagesParam) GetTemplateValues() map[string]any { 84 | res := make(map[string]any) 85 | if p.Name != nil { 86 | res["name"] = p.Name 87 | } 88 | if p.Count != nil { 89 | res["count"] = p.Count 90 | } 91 | return res 92 | } 93 | 94 | func I18nNewMessages(data *NewMessagesParam) *i18n.LocalizeConfig { 95 | const messageID = "NEW_MESSAGES" 96 | var valuesMap = data.GetTemplateValues() 97 | return &i18n.LocalizeConfig{ 98 | MessageID: messageID, 99 | TemplateData: valuesMap, 100 | } 101 | } 102 | 103 | func I18nPleaseConfirm[Value comparable](value Value) *i18n.LocalizeConfig { 104 | const messageID = "PLEASE_CONFIRM" 105 | var tempValue = value 106 | return &i18n.LocalizeConfig{ 107 | MessageID: messageID, 108 | TemplateData: tempValue, 109 | } 110 | } 111 | 112 | type SayHelloParam struct { 113 | Name any 114 | } 115 | 116 | func (p *SayHelloParam) GetTemplateValues() map[string]any { 117 | res := make(map[string]any) 118 | if p.Name != nil { 119 | res["name"] = p.Name 120 | } 121 | return res 122 | } 123 | 124 | func I18nSayHello(data *SayHelloParam) *i18n.LocalizeConfig { 125 | const messageID = "SAY_HELLO" 126 | var valuesMap = data.GetTemplateValues() 127 | return &i18n.LocalizeConfig{ 128 | MessageID: messageID, 129 | TemplateData: valuesMap, 130 | } 131 | } 132 | 133 | func I18nSuccess() *i18n.LocalizeConfig { 134 | const messageID = "SUCCESS" 135 | return &i18n.LocalizeConfig{ 136 | MessageID: messageID, 137 | } 138 | } 139 | 140 | func I18nWelcome() *i18n.LocalizeConfig { 141 | const messageID = "WELCOME" 142 | return &i18n.LocalizeConfig{ 143 | MessageID: messageID, 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /internal/examples/example3/internal/message3/i18n.gen.go: -------------------------------------------------------------------------------- 1 | package message3 2 | 3 | import ( 4 | "github.com/nicksnyder/go-i18n/v2/i18n" 5 | ) 6 | 7 | func NewI元旦() string { 8 | return "元旦" 9 | } 10 | 11 | func I18nI元旦() *i18n.LocalizeConfig { 12 | messageID := NewI元旦() 13 | return &i18n.LocalizeConfig{ 14 | MessageID: messageID, 15 | } 16 | } 17 | 18 | func NewI吃饭了没() string { 19 | return "吃饭了没" 20 | } 21 | 22 | func I18nI吃饭了没() *i18n.LocalizeConfig { 23 | messageID := NewI吃饭了没() 24 | return &i18n.LocalizeConfig{ 25 | MessageID: messageID, 26 | } 27 | } 28 | 29 | func NewI同学() string { 30 | return "同学" 31 | } 32 | 33 | func I18nI同学() *i18n.LocalizeConfig { 34 | messageID := NewI同学() 35 | return &i18n.LocalizeConfig{ 36 | MessageID: messageID, 37 | } 38 | } 39 | 40 | type P我这里有个X你吃吧 struct { 41 | V叉叉叉 any 42 | } 43 | 44 | func (p *P我这里有个X你吃吧) GetTemplateValues() map[string]any { 45 | res := make(map[string]any) 46 | if p.V叉叉叉 != nil { 47 | res["叉叉叉"] = p.V叉叉叉 48 | } 49 | return res 50 | } 51 | 52 | func NewI我这里有个X你吃吧(data *P我这里有个X你吃吧) (string, map[string]any) { 53 | return "我这里有个X你吃吧", data.GetTemplateValues() 54 | } 55 | 56 | func I18nI我这里有个X你吃吧(data *P我这里有个X你吃吧) *i18n.LocalizeConfig { 57 | messageID, valuesMap := NewI我这里有个X你吃吧(data) 58 | return &i18n.LocalizeConfig{ 59 | MessageID: messageID, 60 | TemplateData: valuesMap, 61 | } 62 | } 63 | 64 | type P早上好呀 struct { 65 | V某某某 any 66 | } 67 | 68 | func (p *P早上好呀) GetTemplateValues() map[string]any { 69 | res := make(map[string]any) 70 | if p.V某某某 != nil { 71 | res["某某某"] = p.V某某某 72 | } 73 | return res 74 | } 75 | 76 | func NewI早上好呀(data *P早上好呀) (string, map[string]any) { 77 | return "早上好呀", data.GetTemplateValues() 78 | } 79 | 80 | func I18nI早上好呀(data *P早上好呀) *i18n.LocalizeConfig { 81 | messageID, valuesMap := NewI早上好呀(data) 82 | return &i18n.LocalizeConfig{ 83 | MessageID: messageID, 84 | TemplateData: valuesMap, 85 | } 86 | } 87 | 88 | func NewI春节() string { 89 | return "春节" 90 | } 91 | 92 | func I18nI春节() *i18n.LocalizeConfig { 93 | messageID := NewI春节() 94 | return &i18n.LocalizeConfig{ 95 | MessageID: messageID, 96 | } 97 | } 98 | 99 | type P祝X某X节快乐 struct { 100 | V某某人 any 101 | V某某节 any 102 | } 103 | 104 | func (p *P祝X某X节快乐) GetTemplateValues() map[string]any { 105 | res := make(map[string]any) 106 | if p.V某某人 != nil { 107 | res["某某人"] = p.V某某人 108 | } 109 | if p.V某某节 != nil { 110 | res["某某节"] = p.V某某节 111 | } 112 | return res 113 | } 114 | 115 | func NewI祝X某X节快乐(data *P祝X某X节快乐) (string, map[string]any) { 116 | return "祝X某X节快乐", data.GetTemplateValues() 117 | } 118 | 119 | func I18nI祝X某X节快乐(data *P祝X某X节快乐) *i18n.LocalizeConfig { 120 | messageID, valuesMap := NewI祝X某X节快乐(data) 121 | return &i18n.LocalizeConfig{ 122 | MessageID: messageID, 123 | TemplateData: valuesMap, 124 | } 125 | } 126 | 127 | func NewI祝X节日快乐[Value comparable](value Value) (string, Value) { 128 | return "祝X节日快乐", value 129 | } 130 | 131 | func I18nI祝X节日快乐[Value comparable](value Value) *i18n.LocalizeConfig { 132 | messageID, tempValue := NewI祝X节日快乐(value) 133 | return &i18n.LocalizeConfig{ 134 | MessageID: messageID, 135 | TemplateData: tempValue, 136 | } 137 | } 138 | 139 | func NewI老师() string { 140 | return "老师" 141 | } 142 | 143 | func I18nI老师() *i18n.LocalizeConfig { 144 | messageID := NewI老师() 145 | return &i18n.LocalizeConfig{ 146 | MessageID: messageID, 147 | } 148 | } 149 | 150 | func NewI蛋糕() string { 151 | return "蛋糕" 152 | } 153 | 154 | func I18nI蛋糕() *i18n.LocalizeConfig { 155 | messageID := NewI蛋糕() 156 | return &i18n.LocalizeConfig{ 157 | MessageID: messageID, 158 | } 159 | } 160 | 161 | func NewI面包() string { 162 | return "面包" 163 | } 164 | 165 | func I18nI面包() *i18n.LocalizeConfig { 166 | messageID := NewI面包() 167 | return &i18n.LocalizeConfig{ 168 | MessageID: messageID, 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /internal/examples/example3/example3_test.go: -------------------------------------------------------------------------------- 1 | package example3_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nicksnyder/go-i18n/v2/i18n" 7 | "github.com/stretchr/testify/require" 8 | "github.com/yyle88/goi18n/internal/examples/example3/internal/message3" 9 | ) 10 | 11 | var caseBundle *i18n.Bundle 12 | 13 | func TestMain(m *testing.M) { 14 | caseBundle, _ = message3.LoadI18nFiles() 15 | m.Run() 16 | } 17 | 18 | func TestI18nI早上好呀(t *testing.T) { 19 | t.Run("other-en", func(t *testing.T) { 20 | localizer := i18n.NewLocalizer(caseBundle, "en-US") 21 | msg, err := localizer.Localize(message3.I18nI早上好呀(&message3.P早上好呀{ 22 | V某某某: localizer.MustLocalize(message3.I18nI老师()), 23 | })) 24 | require.NoError(t, err) 25 | t.Log(msg) 26 | require.Equal(t, "Good morning, Teacher", msg) 27 | }) 28 | t.Run("other-zh", func(t *testing.T) { 29 | localizer := i18n.NewLocalizer(caseBundle, "zh-CN") 30 | msg, err := localizer.Localize(message3.I18nI早上好呀(&message3.P早上好呀{ 31 | V某某某: localizer.MustLocalize(message3.I18nI老师()), 32 | })) 33 | require.NoError(t, err) 34 | t.Log(msg) 35 | require.Equal(t, "早上好呀老师", msg) 36 | }) 37 | } 38 | 39 | func TestI18nI吃饭了没(t *testing.T) { 40 | t.Run("other-en", func(t *testing.T) { 41 | localizer := i18n.NewLocalizer(caseBundle, "en-US") 42 | msg, err := localizer.Localize(message3.I18nI吃饭了没()) 43 | require.NoError(t, err) 44 | t.Log(msg) 45 | require.Equal(t, "Have you eaten?", msg) 46 | }) 47 | t.Run("other-zh", func(t *testing.T) { 48 | localizer := i18n.NewLocalizer(caseBundle, "zh-CN") 49 | msg, err := localizer.Localize(message3.I18nI吃饭了没()) 50 | require.NoError(t, err) 51 | t.Log(msg) 52 | require.Equal(t, "吃饭了没", msg) 53 | }) 54 | } 55 | 56 | func TestI18nI我这里有个X你吃吧(t *testing.T) { 57 | t.Run("other-en", func(t *testing.T) { 58 | localizer := i18n.NewLocalizer(caseBundle, "en-US") 59 | msg, err := localizer.Localize(message3.I18nI我这里有个X你吃吧( 60 | &message3.P我这里有个X你吃吧{ 61 | V叉叉叉: localizer.MustLocalize(message3.I18nI蛋糕()), 62 | }, 63 | )) 64 | require.NoError(t, err) 65 | t.Log(msg) 66 | require.Equal(t, "I have a Cake for you to eat.", msg) 67 | }) 68 | t.Run("other-zh", func(t *testing.T) { 69 | localizer := i18n.NewLocalizer(caseBundle, "zh-CN") 70 | msg, err := localizer.Localize(message3.I18nI我这里有个X你吃吧( 71 | &message3.P我这里有个X你吃吧{ 72 | V叉叉叉: localizer.MustLocalize(message3.I18nI蛋糕()), 73 | }, 74 | )) 75 | require.NoError(t, err) 76 | t.Log(msg) 77 | require.Equal(t, "我这里有个蛋糕你吃吧", msg) 78 | }) 79 | } 80 | 81 | func TestI18nI祝X节日快乐(t *testing.T) { 82 | t.Run("other-en", func(t *testing.T) { 83 | localizer := i18n.NewLocalizer(caseBundle, "en-US") 84 | msg, err := localizer.Localize(message3.I18nI祝X节日快乐( 85 | localizer.MustLocalize(message3.I18nI老师()), 86 | )) 87 | require.NoError(t, err) 88 | t.Log(msg) 89 | require.Equal(t, "Happy Teacher festival!", msg) 90 | }) 91 | t.Run("other-zh", func(t *testing.T) { 92 | localizer := i18n.NewLocalizer(caseBundle, "zh-CN") 93 | msg, err := localizer.Localize(message3.I18nI祝X节日快乐( 94 | localizer.MustLocalize(message3.I18nI老师()), 95 | )) 96 | require.NoError(t, err) 97 | t.Log(msg) 98 | require.Equal(t, "祝老师们节日快乐", msg) 99 | }) 100 | } 101 | 102 | func TestI18nI祝X某X节快乐(t *testing.T) { 103 | t.Run("other-en", func(t *testing.T) { 104 | localizer := i18n.NewLocalizer(caseBundle, "en-US") 105 | msg, err := localizer.Localize(message3.I18nI祝X某X节快乐( 106 | &message3.P祝X某X节快乐{ 107 | V某某人: localizer.MustLocalize(message3.I18nI同学()), 108 | V某某节: localizer.MustLocalize(message3.I18nI春节()), 109 | }, 110 | )) 111 | require.NoError(t, err) 112 | t.Log(msg) 113 | require.Equal(t, "Happy Classmate Spring Festival!", msg) 114 | }) 115 | t.Run("other-zh", func(t *testing.T) { 116 | localizer := i18n.NewLocalizer(caseBundle, "zh-CN") 117 | msg, err := localizer.Localize(message3.I18nI祝X某X节快乐( 118 | &message3.P祝X某X节快乐{ 119 | V某某人: localizer.MustLocalize(message3.I18nI同学()), 120 | V某某节: localizer.MustLocalize(message3.I18nI春节()), 121 | }, 122 | )) 123 | require.NoError(t, err) 124 | t.Log(msg) 125 | require.Equal(t, "祝同学们春节快乐", msg) 126 | }) 127 | } 128 | -------------------------------------------------------------------------------- /internal/utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yyle88/goi18n/internal/utils" 8 | ) 9 | 10 | func TestHasNonASCII(t *testing.T) { 11 | require.False(t, utils.HasNonASCII("abc")) 12 | require.False(t, utils.HasNonASCII("ABC123")) 13 | require.False(t, utils.HasNonASCII("hello_world")) 14 | require.True(t, utils.HasNonASCII("嘿嘿")) 15 | require.True(t, utils.HasNonASCII("你好world")) 16 | require.True(t, utils.HasNonASCII("café")) 17 | require.False(t, utils.HasNonASCII("")) 18 | } 19 | 20 | func TestHasLetterPrefix(t *testing.T) { 21 | // Test uppercase letters 22 | require.True(t, utils.HasLetterPrefix("ABC")) 23 | require.True(t, utils.HasLetterPrefix("Z123")) 24 | 25 | // Test lowercase letters 26 | require.True(t, utils.HasLetterPrefix("abc")) 27 | require.True(t, utils.HasLetterPrefix("hello")) 28 | 29 | // Test non-letter prefix 30 | require.False(t, utils.HasLetterPrefix("123abc")) 31 | require.False(t, utils.HasLetterPrefix("_test")) 32 | require.False(t, utils.HasLetterPrefix("@symbol")) 33 | 34 | // Test Chinese characters 35 | require.False(t, utils.HasLetterPrefix("你好")) 36 | require.False(t, utils.HasLetterPrefix("测试ABC")) 37 | 38 | // Test edge cases 39 | require.False(t, utils.HasLetterPrefix("")) 40 | require.True(t, utils.HasLetterPrefix("a")) 41 | require.True(t, utils.HasLetterPrefix("A")) 42 | } 43 | 44 | func TestCapitalizeFirst(t *testing.T) { 45 | // Test lowercase to uppercase 46 | require.Equal(t, "Hello", utils.CapitalizeFirst("hello")) 47 | require.Equal(t, "World", utils.CapitalizeFirst("world")) 48 | 49 | // Test already capitalized 50 | require.Equal(t, "ABC", utils.CapitalizeFirst("ABC")) 51 | require.Equal(t, "Hello", utils.CapitalizeFirst("Hello")) 52 | 53 | // Test Chinese characters 54 | require.Equal(t, "你好", utils.CapitalizeFirst("你好")) 55 | require.Equal(t, "测试", utils.CapitalizeFirst("测试")) 56 | 57 | // Test mixed content 58 | require.Equal(t, "Hello世界", utils.CapitalizeFirst("hello世界")) 59 | 60 | // Test single character 61 | require.Equal(t, "A", utils.CapitalizeFirst("a")) 62 | require.Equal(t, "B", utils.CapitalizeFirst("B")) 63 | 64 | // Test edge cases 65 | require.Equal(t, "", utils.CapitalizeFirst("")) 66 | require.Equal(t, "1abc", utils.CapitalizeFirst("1abc")) 67 | require.Equal(t, "_test", utils.CapitalizeFirst("_test")) 68 | } 69 | 70 | func TestDefaultUnicodeMessageName(t *testing.T) { 71 | // Test ASCII letter prefix 72 | require.Equal(t, "Hello", utils.DefaultUnicodeMessageName("hello")) 73 | require.Equal(t, "World", utils.DefaultUnicodeMessageName("World")) 74 | 75 | // Test Chinese characters - add "I" prefix 76 | require.Equal(t, "I你好", utils.DefaultUnicodeMessageName("你好")) 77 | require.Equal(t, "I测试消息", utils.DefaultUnicodeMessageName("测试消息")) 78 | 79 | // Test non-letter prefix - add "I" prefix 80 | require.Equal(t, "I123", utils.DefaultUnicodeMessageName("123")) 81 | require.Equal(t, "I_test", utils.DefaultUnicodeMessageName("_test")) 82 | } 83 | 84 | func TestDefaultUnicodeStructName(t *testing.T) { 85 | // Test ASCII letter prefix 86 | require.Equal(t, "Hello", utils.DefaultUnicodeStructName("hello")) 87 | require.Equal(t, "World", utils.DefaultUnicodeStructName("World")) 88 | 89 | // Test Chinese characters - add "P" prefix 90 | require.Equal(t, "P你好", utils.DefaultUnicodeStructName("你好")) 91 | require.Equal(t, "P测试结构", utils.DefaultUnicodeStructName("测试结构")) 92 | 93 | // Test non-letter prefix - add "P" prefix 94 | require.Equal(t, "P123", utils.DefaultUnicodeStructName("123")) 95 | require.Equal(t, "P_test", utils.DefaultUnicodeStructName("_test")) 96 | } 97 | 98 | func TestDefaultUnicodeFieldName(t *testing.T) { 99 | // Test ASCII letter prefix 100 | require.Equal(t, "Name", utils.DefaultUnicodeFieldName("name")) 101 | require.Equal(t, "Value", utils.DefaultUnicodeFieldName("Value")) 102 | 103 | // Test Chinese characters - add "V" prefix 104 | require.Equal(t, "V名称", utils.DefaultUnicodeFieldName("名称")) 105 | require.Equal(t, "V字段", utils.DefaultUnicodeFieldName("字段")) 106 | 107 | // Test non-letter prefix - add "V" prefix 108 | require.Equal(t, "V123", utils.DefaultUnicodeFieldName("123")) 109 | require.Equal(t, "V_field", utils.DefaultUnicodeFieldName("_field")) 110 | } 111 | -------------------------------------------------------------------------------- /internal/examples/example1/internal/message1/i18n.gen.go: -------------------------------------------------------------------------------- 1 | package message1 2 | 3 | import ( 4 | "github.com/nicksnyder/go-i18n/v2/i18n" 5 | ) 6 | 7 | type ErrorAlreadyExistParam struct { 8 | What any 9 | Code any 10 | } 11 | 12 | func (p *ErrorAlreadyExistParam) GetTemplateValues() map[string]any { 13 | res := make(map[string]any) 14 | if p.What != nil { 15 | res["what"] = p.What 16 | } 17 | if p.Code != nil { 18 | res["code"] = p.Code 19 | } 20 | return res 21 | } 22 | 23 | func NewErrorAlreadyExist(data *ErrorAlreadyExistParam) (string, map[string]any) { 24 | return "ERROR_ALREADY_EXIST", data.GetTemplateValues() 25 | } 26 | 27 | func I18nErrorAlreadyExist(data *ErrorAlreadyExistParam) *i18n.LocalizeConfig { 28 | messageID, valuesMap := NewErrorAlreadyExist(data) 29 | return &i18n.LocalizeConfig{ 30 | MessageID: messageID, 31 | TemplateData: valuesMap, 32 | } 33 | } 34 | 35 | type ErrorBadParamParam struct { 36 | Name any 37 | } 38 | 39 | func (p *ErrorBadParamParam) GetTemplateValues() map[string]any { 40 | res := make(map[string]any) 41 | if p.Name != nil { 42 | res["name"] = p.Name 43 | } 44 | return res 45 | } 46 | 47 | func NewErrorBadParam(data *ErrorBadParamParam) (string, map[string]any) { 48 | return "ERROR_BAD_PARAM", data.GetTemplateValues() 49 | } 50 | 51 | func I18nErrorBadParam(data *ErrorBadParamParam) *i18n.LocalizeConfig { 52 | messageID, valuesMap := NewErrorBadParam(data) 53 | return &i18n.LocalizeConfig{ 54 | MessageID: messageID, 55 | TemplateData: valuesMap, 56 | } 57 | } 58 | 59 | type ErrorNotExistParam struct { 60 | What any 61 | Code any 62 | } 63 | 64 | func (p *ErrorNotExistParam) GetTemplateValues() map[string]any { 65 | res := make(map[string]any) 66 | if p.What != nil { 67 | res["what"] = p.What 68 | } 69 | if p.Code != nil { 70 | res["code"] = p.Code 71 | } 72 | return res 73 | } 74 | 75 | func NewErrorNotExist(data *ErrorNotExistParam) (string, map[string]any) { 76 | return "ERROR_NOT_EXIST", data.GetTemplateValues() 77 | } 78 | 79 | func I18nErrorNotExist(data *ErrorNotExistParam) *i18n.LocalizeConfig { 80 | messageID, valuesMap := NewErrorNotExist(data) 81 | return &i18n.LocalizeConfig{ 82 | MessageID: messageID, 83 | TemplateData: valuesMap, 84 | } 85 | } 86 | 87 | type NewMessagesParam struct { 88 | Name any 89 | Count any 90 | } 91 | 92 | func (p *NewMessagesParam) GetTemplateValues() map[string]any { 93 | res := make(map[string]any) 94 | if p.Name != nil { 95 | res["name"] = p.Name 96 | } 97 | if p.Count != nil { 98 | res["count"] = p.Count 99 | } 100 | return res 101 | } 102 | 103 | func NewNewMessages(data *NewMessagesParam) (string, map[string]any) { 104 | return "NEW_MESSAGES", data.GetTemplateValues() 105 | } 106 | 107 | func I18nNewMessages(data *NewMessagesParam) *i18n.LocalizeConfig { 108 | messageID, valuesMap := NewNewMessages(data) 109 | return &i18n.LocalizeConfig{ 110 | MessageID: messageID, 111 | TemplateData: valuesMap, 112 | } 113 | } 114 | 115 | func NewPleaseConfirm[Value comparable](value Value) (string, Value) { 116 | return "PLEASE_CONFIRM", value 117 | } 118 | 119 | func I18nPleaseConfirm[Value comparable](value Value) *i18n.LocalizeConfig { 120 | messageID, tempValue := NewPleaseConfirm(value) 121 | return &i18n.LocalizeConfig{ 122 | MessageID: messageID, 123 | TemplateData: tempValue, 124 | } 125 | } 126 | 127 | type SayHelloParam struct { 128 | Name any 129 | } 130 | 131 | func (p *SayHelloParam) GetTemplateValues() map[string]any { 132 | res := make(map[string]any) 133 | if p.Name != nil { 134 | res["name"] = p.Name 135 | } 136 | return res 137 | } 138 | 139 | func NewSayHello(data *SayHelloParam) (string, map[string]any) { 140 | return "SAY_HELLO", data.GetTemplateValues() 141 | } 142 | 143 | func I18nSayHello(data *SayHelloParam) *i18n.LocalizeConfig { 144 | messageID, valuesMap := NewSayHello(data) 145 | return &i18n.LocalizeConfig{ 146 | MessageID: messageID, 147 | TemplateData: valuesMap, 148 | } 149 | } 150 | 151 | func NewSuccess() string { 152 | return "SUCCESS" 153 | } 154 | 155 | func I18nSuccess() *i18n.LocalizeConfig { 156 | messageID := NewSuccess() 157 | return &i18n.LocalizeConfig{ 158 | MessageID: messageID, 159 | } 160 | } 161 | 162 | func NewWelcome() string { 163 | return "WELCOME" 164 | } 165 | 166 | func I18nWelcome() *i18n.LocalizeConfig { 167 | messageID := NewWelcome() 168 | return &i18n.LocalizeConfig{ 169 | MessageID: messageID, 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /internal/examples/example1/example1_test.go: -------------------------------------------------------------------------------- 1 | package example1_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nicksnyder/go-i18n/v2/i18n" 7 | "github.com/stretchr/testify/require" 8 | "github.com/yyle88/goi18n/internal/examples/example1/internal/message1" 9 | ) 10 | 11 | var caseBundle *i18n.Bundle 12 | 13 | func TestMain(m *testing.M) { 14 | caseBundle, _ = message1.LoadI18nFiles() 15 | m.Run() 16 | } 17 | 18 | func TestI18nSayHello(t *testing.T) { 19 | t.Run("SAY_HELLO-zh", func(t *testing.T) { 20 | localizer := i18n.NewLocalizer(caseBundle, "zh-CN") 21 | 22 | msg, err := localizer.Localize(message1.I18nSayHello(&message1.SayHelloParam{ 23 | Name: "杨亦乐", 24 | })) 25 | require.NoError(t, err) 26 | t.Log(msg) 27 | require.Equal(t, "你好,杨亦乐!", msg) 28 | }) 29 | t.Run("SAY_HELLO-en", func(t *testing.T) { 30 | localizer := i18n.NewLocalizer(caseBundle, "en-US") 31 | 32 | msg, err := localizer.Localize(message1.I18nSayHello(&message1.SayHelloParam{ 33 | Name: "yangyile", 34 | })) 35 | require.NoError(t, err) 36 | t.Log(msg) 37 | require.Equal(t, "Hello, yangyile!", msg) 38 | }) 39 | t.Run("SAY_HELLO-km", func(t *testing.T) { 40 | localizer := i18n.NewLocalizer(caseBundle, "km-KH") 41 | 42 | msg, err := localizer.Localize(message1.I18nSayHello(&message1.SayHelloParam{ 43 | Name: "yangyile", 44 | })) 45 | require.NoError(t, err) 46 | t.Log(msg) 47 | require.Equal(t, "សួស្តី yangyile!", msg) //完全看不懂高棉语啊 48 | }) 49 | } 50 | 51 | func TestNewSayHello(t *testing.T) { 52 | t.Run("SAY_HELLO-zh", func(t *testing.T) { 53 | localizer := i18n.NewLocalizer(caseBundle, "zh-CN") 54 | 55 | messageID, msgValues := message1.NewSayHello(&message1.SayHelloParam{ 56 | Name: "杨亦乐", 57 | }) 58 | 59 | msg, err := localizer.Localize(&i18n.LocalizeConfig{ 60 | MessageID: messageID, 61 | TemplateData: msgValues, 62 | }) 63 | require.NoError(t, err) 64 | t.Log(msg) 65 | require.Equal(t, "你好,杨亦乐!", msg) 66 | }) 67 | t.Run("SAY_HELLO-en", func(t *testing.T) { 68 | localizer := i18n.NewLocalizer(caseBundle, "en-US") 69 | 70 | messageID, msgValues := message1.NewSayHello(&message1.SayHelloParam{ 71 | Name: "yangyile", 72 | }) 73 | 74 | msg, err := localizer.Localize(&i18n.LocalizeConfig{ 75 | MessageID: messageID, 76 | TemplateData: msgValues, 77 | }) 78 | require.NoError(t, err) 79 | t.Log(msg) 80 | require.Equal(t, "Hello, yangyile!", msg) 81 | }) 82 | t.Run("SAY_HELLO-km", func(t *testing.T) { 83 | localizer := i18n.NewLocalizer(caseBundle, "km-KH") 84 | 85 | messageID, msgValues := message1.NewSayHello(&message1.SayHelloParam{ 86 | Name: "yangyile", 87 | }) 88 | 89 | msg, err := localizer.Localize(&i18n.LocalizeConfig{ 90 | MessageID: messageID, 91 | TemplateData: msgValues, 92 | }) 93 | require.NoError(t, err) 94 | t.Log(msg) 95 | require.Equal(t, "សួស្តី yangyile!", msg) 96 | }) 97 | } 98 | 99 | func TestI18nWelcome(t *testing.T) { 100 | t.Run("WELCOME-zh", func(t *testing.T) { 101 | localizer := i18n.NewLocalizer(caseBundle, "zh-CN") 102 | 103 | msg, err := localizer.Localize(message1.I18nWelcome()) 104 | require.NoError(t, err) 105 | t.Log(msg) 106 | require.Equal(t, "欢迎使用此应用!", msg) 107 | }) 108 | } 109 | 110 | func TestI18nSuccess(t *testing.T) { 111 | t.Run("SUCCESS-zh", func(t *testing.T) { 112 | localizer := i18n.NewLocalizer(caseBundle, "zh-CN") 113 | 114 | msg, err := localizer.Localize(message1.I18nSuccess()) 115 | require.NoError(t, err) 116 | t.Log(msg) 117 | require.Equal(t, "成功", msg) 118 | }) 119 | } 120 | 121 | func TestI18nPleaseConfirm(t *testing.T) { 122 | t.Run("PLEASE_CONFIRM-zh", func(t *testing.T) { 123 | localizer := i18n.NewLocalizer(caseBundle, "zh-CN") 124 | 125 | msg, err := localizer.Localize(message1.I18nPleaseConfirm("提交材料")) 126 | require.NoError(t, err) 127 | t.Log(msg) 128 | require.Equal(t, "请确认提交材料", msg) 129 | }) 130 | } 131 | 132 | func TestI18nErrorNotExist(t *testing.T) { 133 | t.Run("ERROR_NOT_EXIST-zh", func(t *testing.T) { 134 | localizer := i18n.NewLocalizer(caseBundle, "zh-CN") 135 | 136 | msg, err := localizer.Localize(message1.I18nErrorNotExist(&message1.ErrorNotExistParam{ 137 | What: "数据库里", 138 | Code: "账号信息", 139 | })) 140 | require.NoError(t, err) 141 | t.Log(msg) 142 | require.Equal(t, "数据库里 账号信息 不存在", msg) 143 | }) 144 | } 145 | 146 | func TestI18nErrorAlreadyExist(t *testing.T) { 147 | t.Run("ERROR_ALREADY_EXIST-zh", func(t *testing.T) { 148 | localizer := i18n.NewLocalizer(caseBundle, "zh-CN") 149 | 150 | msg, err := localizer.Localize(message1.I18nErrorAlreadyExist(&message1.ErrorAlreadyExistParam{ 151 | What: "系统里", 152 | Code: "玩家名", 153 | })) 154 | require.NoError(t, err) 155 | t.Log(msg) 156 | require.Equal(t, "系统里 玩家名 已存在", msg) 157 | }) 158 | } 159 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package goi18n 2 | 3 | import ( 4 | "path/filepath" 5 | "strings" 6 | 7 | "github.com/iancoleman/strcase" 8 | "github.com/yyle88/goi18n/internal/utils" 9 | "github.com/yyle88/must" 10 | "github.com/yyle88/osexistpath/osmustexist" 11 | "github.com/yyle88/syntaxgo" 12 | ) 13 | 14 | // Options configures code generation settings 15 | // Controls output path, package name, and naming functions 16 | // Supports custom naming for non-ASCII message IDs 17 | // 18 | // Options 配置代码生成设置 19 | // 控制输出路径、包名和命名函数 20 | // 支持非 ASCII 消息 ID 的自定义命名 21 | type Options struct { 22 | outputPath string 23 | pkgName string 24 | allowNonAsciiRune bool // 是否允许非 ASCII 字符,默认 false 值表示只允许 ASCII 字符 25 | unicodeMessageName func(messageID string) string // 基础命名 26 | unicodeStructName func(messageID string) string // 类名命名 27 | unicodeFieldName func(paramName string) string // 字段命名 28 | generateNewMessage bool 29 | } 30 | 31 | // NewOptions creates Options with default settings 32 | // Sets up default naming functions for Unicode message IDs 33 | // 34 | // NewOptions 创建带默认设置的 Options 35 | // 为 Unicode 消息 ID 设置默认命名函数 36 | func NewOptions() *Options { 37 | return &Options{ 38 | outputPath: "", 39 | pkgName: "", 40 | allowNonAsciiRune: false, 41 | unicodeMessageName: func(messageID string) string { 42 | return utils.DefaultUnicodeMessageName(messageID) 43 | }, 44 | unicodeStructName: func(messageID string) string { 45 | return utils.DefaultUnicodeStructName(messageID) 46 | }, 47 | unicodeFieldName: func(paramName string) string { 48 | return utils.DefaultUnicodeFieldName(paramName) 49 | }, 50 | generateNewMessage: false, 51 | } 52 | } 53 | 54 | // WithOutputPath sets output file path 55 | // Path must have .go extension 56 | // 57 | // WithOutputPath 设置输出文件路径 58 | // 路径必须有 .go 扩展名 59 | func (o *Options) WithOutputPath(outputPath string) *Options { 60 | must.Same(filepath.Ext(outputPath), ".go") 61 | o.outputPath = outputPath 62 | return o 63 | } 64 | 65 | // WithPkgName sets package name for generated code 66 | // 67 | // WithPkgName 设置生成代码的包名 68 | func (o *Options) WithPkgName(pkgName string) *Options { 69 | o.pkgName = pkgName 70 | return o 71 | } 72 | 73 | // WithOutputPathWithPkgName sets output path and infers package name 74 | // If file exists: reads package name from existing file 75 | // If file not exists: derives package name from parent DIR name 76 | // 77 | // WithOutputPathWithPkgName 设置输出路径并推断包名 78 | // 如果文件存在:从现有文件读取包名 79 | // 如果文件不存在:从父目录名派生包名 80 | func (o *Options) WithOutputPathWithPkgName(outputPath string) *Options { 81 | must.Same(filepath.Ext(outputPath), ".go") 82 | 83 | var pkgName string 84 | if osmustexist.IsFile(outputPath) { 85 | // Read package name from existing file 86 | // 从现有文件读取包名 87 | pkgName = syntaxgo.GetPkgName(outputPath) 88 | } else { 89 | // Derive package name from parent DIR name 90 | // 从父目录名派生包名 91 | pkgName = filepath.Base(filepath.Dir(outputPath)) 92 | pkgName = strcase.ToSnake(pkgName) 93 | pkgName = strings.ReplaceAll(pkgName, "_", "") 94 | pkgName = strings.ToLower(pkgName) 95 | } 96 | 97 | o.outputPath = must.Nice(outputPath) 98 | o.pkgName = must.Nice(pkgName) 99 | return o 100 | } 101 | 102 | // GetOutputPath returns output file path 103 | // 104 | // GetOutputPath 返回输出文件路径 105 | func (o *Options) GetOutputPath() string { 106 | return o.outputPath 107 | } 108 | 109 | // GetPkgName returns package name 110 | // 111 | // GetPkgName 返回包名 112 | func (o *Options) GetPkgName() string { 113 | return o.pkgName 114 | } 115 | 116 | // WithAllowNonAsciiRune enables non-ASCII rune support in message IDs 117 | // When enabled: uses custom naming functions for Unicode message IDs 118 | // When disabled: uses strcase.ToCamel for ASCII-based naming 119 | // 120 | // WithAllowNonAsciiRune 启用消息 ID 中的非 ASCII 字符支持 121 | // 启用时:对 Unicode 消息 ID 使用自定义命名函数 122 | // 禁用时:使用 strcase.ToCamel 进行 ASCII 命名 123 | func (o *Options) WithAllowNonAsciiRune(allowNonAsciiRune bool) *Options { 124 | o.allowNonAsciiRune = allowNonAsciiRune 125 | return o 126 | } 127 | 128 | // WithUnicodeMessageName sets custom naming function for Unicode message IDs 129 | // 130 | // WithUnicodeMessageName 设置 Unicode 消息 ID 的自定义命名函数 131 | func (o *Options) WithUnicodeMessageName(unicodeMessageName func(string) string) *Options { 132 | o.unicodeMessageName = unicodeMessageName 133 | return o 134 | } 135 | 136 | // WithUnicodeStructName sets custom naming function for Unicode struct names 137 | // 138 | // WithUnicodeStructName 设置 Unicode 结构体名的自定义命名函数 139 | func (o *Options) WithUnicodeStructName(unicodeStructName func(string) string) *Options { 140 | o.unicodeStructName = unicodeStructName 141 | return o 142 | } 143 | 144 | // WithUnicodeFieldName sets custom naming function for Unicode field names 145 | // 146 | // WithUnicodeFieldName 设置 Unicode 字段名的自定义命名函数 147 | func (o *Options) WithUnicodeFieldName(unicodeFieldName func(string) string) *Options { 148 | o.unicodeFieldName = unicodeFieldName 149 | return o 150 | } 151 | 152 | // WithGenerateNewMessage enables generation of New* functions 153 | // New* functions return (messageID, templateData) tuples 154 | // 155 | // WithGenerateNewMessage 启用生成 New* 函数 156 | // New* 函数返回 (messageID, templateData) 元组 157 | func (o *Options) WithGenerateNewMessage(generateNewMessage bool) *Options { 158 | o.generateNewMessage = generateNewMessage 159 | return o 160 | } 161 | -------------------------------------------------------------------------------- /internal/examples/example2/example2_test.go: -------------------------------------------------------------------------------- 1 | package example2_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nicksnyder/go-i18n/v2/i18n" 7 | "github.com/stretchr/testify/require" 8 | "github.com/yyle88/goi18n" 9 | "github.com/yyle88/goi18n/internal/examples/example2/internal/message2" 10 | ) 11 | 12 | var caseBundle *i18n.Bundle 13 | 14 | func TestMain(m *testing.M) { 15 | caseBundle, _ = message2.LoadI18nFiles() 16 | m.Run() 17 | } 18 | 19 | func TestI18nActiveUsers(t *testing.T) { 20 | t.Run("one-1-en", func(t *testing.T) { 21 | localizer := i18n.NewLocalizer(caseBundle, "en-US") 22 | const count = 1 23 | msg, err := localizer.Localize(message2.I18nActiveUsers( 24 | &message2.ActiveUsersParam{ 25 | Count: count, 26 | }, 27 | goi18n.NewPluralConfig(count), 28 | )) 29 | require.NoError(t, err) 30 | t.Log(msg) 31 | require.Equal(t, "One user is active in the project.", msg) 32 | }) 33 | t.Run("other-en", func(t *testing.T) { 34 | localizer := i18n.NewLocalizer(caseBundle, "en-US") 35 | const count = 9999 36 | msg, err := localizer.Localize(message2.I18nActiveUsers( 37 | &message2.ActiveUsersParam{ 38 | Count: count, 39 | }, 40 | goi18n.NewPluralConfig(count), 41 | )) 42 | require.NoError(t, err) 43 | t.Log(msg) 44 | require.Equal(t, "9999 users are active in the project.", msg) 45 | }) 46 | t.Run("other-zh", func(t *testing.T) { 47 | localizer := i18n.NewLocalizer(caseBundle, "zh-CN") 48 | const count = 9999 49 | msg, err := localizer.Localize(message2.I18nActiveUsers( 50 | &message2.ActiveUsersParam{ 51 | Count: count, 52 | }, 53 | goi18n.NewPluralConfig(count), 54 | )) 55 | require.NoError(t, err) 56 | t.Log(msg) 57 | require.Equal(t, "项目中有 9999 个活跃用户。", msg) 58 | }) 59 | } 60 | 61 | func TestI18nCompletedTasks(t *testing.T) { 62 | t.Run("one-1-en", func(t *testing.T) { 63 | localizer := i18n.NewLocalizer(caseBundle, "en-US") 64 | const count = 1 65 | msg, err := localizer.Localize(message2.I18nCompletedTasks( 66 | &message2.CompletedTasksParam{ 67 | Count: count, 68 | }, 69 | goi18n.NewPluralConfig(count), 70 | )) 71 | require.NoError(t, err) 72 | t.Log(msg) 73 | require.Equal(t, "One task is completed in the project.", msg) 74 | }) 75 | t.Run("other-en", func(t *testing.T) { 76 | localizer := i18n.NewLocalizer(caseBundle, "en-US") 77 | const count = 9999 78 | msg, err := localizer.Localize(message2.I18nCompletedTasks( 79 | &message2.CompletedTasksParam{ 80 | Count: count, 81 | }, 82 | goi18n.NewPluralConfig(count), 83 | )) 84 | require.NoError(t, err) 85 | t.Log(msg) 86 | require.Equal(t, "9999 tasks are completed in the project.", msg) 87 | }) 88 | t.Run("other-zh", func(t *testing.T) { 89 | localizer := i18n.NewLocalizer(caseBundle, "zh-CN") 90 | const count = 9999 91 | msg, err := localizer.Localize(message2.I18nCompletedTasks( 92 | &message2.CompletedTasksParam{ 93 | Count: count, 94 | }, 95 | goi18n.NewPluralConfig(count), 96 | )) 97 | require.NoError(t, err) 98 | t.Log(msg) 99 | require.Equal(t, "项目中完成了 9999 个任务。", msg) 100 | }) 101 | } 102 | 103 | func TestI18nOpenIssues(t *testing.T) { 104 | t.Run("one-1-en", func(t *testing.T) { 105 | localizer := i18n.NewLocalizer(caseBundle, "en-US") 106 | const count = 1 107 | msg, err := localizer.Localize(message2.I18nOpenIssues( 108 | &message2.OpenIssuesParam{ 109 | Count: count, 110 | }, 111 | goi18n.NewPluralConfig(count), 112 | )) 113 | require.NoError(t, err) 114 | t.Log(msg) 115 | require.Equal(t, "There is one open issue in the project.", msg) 116 | }) 117 | t.Run("other-en", func(t *testing.T) { 118 | localizer := i18n.NewLocalizer(caseBundle, "en-US") 119 | const count = 3 120 | msg, err := localizer.Localize(message2.I18nOpenIssues( 121 | &message2.OpenIssuesParam{ 122 | Count: count, 123 | }, 124 | goi18n.NewPluralConfig(count), 125 | )) 126 | require.NoError(t, err) 127 | t.Log(msg) 128 | require.Equal(t, "There are 3 open issues in the project.", msg) 129 | }) 130 | t.Run("other-zh", func(t *testing.T) { 131 | localizer := i18n.NewLocalizer(caseBundle, "zh-CN") 132 | const count = 3 133 | msg, err := localizer.Localize(message2.I18nOpenIssues( 134 | &message2.OpenIssuesParam{ 135 | Count: count, 136 | }, 137 | goi18n.NewPluralConfig(count), 138 | )) 139 | require.NoError(t, err) 140 | t.Log(msg) 141 | require.Equal(t, "项目中有 3 个未解决问题。", msg) 142 | }) 143 | } 144 | 145 | func TestI18nPendingReviews(t *testing.T) { 146 | t.Run("one-1-en", func(t *testing.T) { 147 | localizer := i18n.NewLocalizer(caseBundle, "en-US") 148 | const count = 1 149 | msg, err := localizer.Localize(message2.I18nPendingReviews( 150 | count, 151 | goi18n.NewPluralConfig(count), 152 | )) 153 | require.NoError(t, err) 154 | t.Log(msg) 155 | require.Equal(t, "There is one pending review in the project.", msg) 156 | }) 157 | t.Run("other-en", func(t *testing.T) { 158 | localizer := i18n.NewLocalizer(caseBundle, "en-US") 159 | const count = 3 160 | msg, err := localizer.Localize(message2.I18nPendingReviews( 161 | count, 162 | goi18n.NewPluralConfig(count), 163 | )) 164 | require.NoError(t, err) 165 | t.Log(msg) 166 | require.Equal(t, "There are 3 pending reviews in the project.", msg) 167 | }) 168 | t.Run("other-zh", func(t *testing.T) { 169 | localizer := i18n.NewLocalizer(caseBundle, "zh-CN") 170 | const count = 3 171 | msg, err := localizer.Localize(message2.I18nPendingReviews( 172 | count, 173 | goi18n.NewPluralConfig(count), 174 | )) 175 | require.NoError(t, err) 176 | t.Log(msg) 177 | require.Equal(t, "项目中有 3 个待审。", msg) 178 | }) 179 | } 180 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= 2 | github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 3 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 4 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU= 6 | github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A= 7 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 8 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 9 | github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= 10 | github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= 11 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 12 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 13 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 14 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 15 | github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk= 16 | github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ= 17 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 18 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 19 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 20 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 21 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 22 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 23 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 24 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 25 | github.com/yyle88/done v1.0.27 h1:FaCbL0hUpsZ8DH4FLbDnjQDIYjvf0JgNxGVi6ZoDhGg= 26 | github.com/yyle88/done v1.0.27/go.mod h1:7fEv2NuCKW/XA/6a5BIwgX+A8MqYFtyTJMbDJdiDZrM= 27 | github.com/yyle88/erero v1.0.24 h1:yroawlW4IohY4bK4SonMBNI2tlZftPjtfhYYBtBfCxw= 28 | github.com/yyle88/erero v1.0.24/go.mod h1:KoTJmpyuKtQddt1QEQidwmewilCWwEH8rn5NsIAZ8XE= 29 | github.com/yyle88/formatgo v1.0.27 h1:hlBtDh8n0UYKWlEH+M638fJjTzu8XhfAzYWsfUs71ZM= 30 | github.com/yyle88/formatgo v1.0.27/go.mod h1:QjfyrRvoCnKgw8Jr6ifHSQwAvIlLkNFjMI37rA9qSNU= 31 | github.com/yyle88/must v0.0.26 h1:bxUtYq4S5e7FjdQsAVCXPNZOA8YVItnQF+VXVqCSrps= 32 | github.com/yyle88/must v0.0.26/go.mod h1:SO20wxYD9sahO1crPOPlWxwJIyEx0qGtq1K/zgy/8UU= 33 | github.com/yyle88/mutexmap v1.0.14 h1:aBdhtKR0XmFAJFoyswfjAEg9dzBvdaUBXU3Iw50AlB0= 34 | github.com/yyle88/mutexmap v1.0.14/go.mod h1:QUYDuARLPlGj414kHewQ5tt8jkDxQXoai8H3C4Gg+yc= 35 | github.com/yyle88/neatjson v0.0.13 h1:+1Ihb43IZLkYAd+lapnvUJN20bTjSAkoTE/2gssBew8= 36 | github.com/yyle88/neatjson v0.0.13/go.mod h1:BOgA69f27Bd/yj2xnIWldratF3WwIrMKrBgo9eIZGb0= 37 | github.com/yyle88/osexistpath v0.0.18 h1:mBi01278glkoTlPE3xTbO5/GT9iGEowOZ5pL/29qas0= 38 | github.com/yyle88/osexistpath v0.0.18/go.mod h1:iTuJ9S07VIiq2azsSYUiheZtDXKlU819v9YAuSL40Uk= 39 | github.com/yyle88/printgo v1.0.6 h1:b53uyCdlijObvuyHVECiiEWQIDfuOpuHVrkNQIdR5bU= 40 | github.com/yyle88/printgo v1.0.6/go.mod h1:14qsuuTovdfgHtle0e4ln6zci47Xtkdx9CCkUo3vxoc= 41 | github.com/yyle88/rese v0.0.11 h1:GjTlfhlEXiy6GPfTChlyOY9lqEVq1yY1O4Wpp1ZI4Ew= 42 | github.com/yyle88/rese v0.0.11/go.mod h1:Kst4nghSQBL0uAquA/A01BW9hmW4pfpMplEleGQkSpY= 43 | github.com/yyle88/runpath v1.0.24 h1:jyFwGS0RbgOJS1dblMh1DBWckRN+MVomTT89cgmsFAw= 44 | github.com/yyle88/runpath v1.0.24/go.mod h1:j1JvgyOK3WLRaEq6UHIiAp1voenfC5m6oGEav7ftBx4= 45 | github.com/yyle88/sortx v1.0.10 h1:k+14XWvMPt3u34WvLeylRNCmwp9IlJWoKt3ytMxG8Ac= 46 | github.com/yyle88/sortx v1.0.10/go.mod h1:2wpPR0SHoRdwkytwjMm3NKSuxNkAO66I/8+P+8CKHVI= 47 | github.com/yyle88/sure v0.0.40 h1:iWHAoeSDS0hVEupl65p4m+mRRbPrwniEgYF2751Eqo8= 48 | github.com/yyle88/sure v0.0.40/go.mod h1:xvpdDUrh5awr56DF75fiP4g1deev/sokyF706ogt8Ys= 49 | github.com/yyle88/syntaxgo v0.0.53 h1:3W4S5ncRdq3hUp3Qjw4GqB+mAxypJCycMo/mMJP+1vc= 50 | github.com/yyle88/syntaxgo v0.0.53/go.mod h1:68EidTlDxVi/iaCJeg0menpA4v/xbq+ITxa1aKdmjLo= 51 | github.com/yyle88/tern v0.0.9 h1:d/0afYxeAcUs/vjHqviswMq45NYGPHQQCh1cXA7CPRs= 52 | github.com/yyle88/tern v0.0.9/go.mod h1:OHHE2G1gYaX4q0uu3sG9JAK9dBjHboxGcTXbRPVoGeQ= 53 | github.com/yyle88/zaplog v0.0.27 h1:Bd/XWeAeRDEsFdtHphEqPK+W3M9WNd/dzf5x6YXeSkY= 54 | github.com/yyle88/zaplog v0.0.27/go.mod h1:0BOxIR1lFh4vdiCyR5zuj4DmTFK36FbpjOWAdjMwSDU= 55 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 56 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 57 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 58 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 59 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 60 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 61 | golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs= 62 | golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo= 63 | golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= 64 | golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 65 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 66 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 67 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 68 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 69 | golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= 70 | golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= 71 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 72 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 73 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 74 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 75 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 76 | -------------------------------------------------------------------------------- /goi18n_test.go: -------------------------------------------------------------------------------- 1 | package goi18n_test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/nicksnyder/go-i18n/v2/i18n" 8 | "github.com/yyle88/goi18n" 9 | "github.com/yyle88/neatjson/neatjsons" 10 | "github.com/yyle88/rese" 11 | "github.com/yyle88/zaplog" 12 | "golang.org/x/text/language" 13 | "gopkg.in/yaml.v3" 14 | ) 15 | 16 | // TestGenerate tests basic code generation with mixed message patterns 17 | // Includes messages with named params, without params, and anonymous params 18 | // Uses both English and Chinese message files with YAML format 19 | // 20 | // TestGenerate 测试包含多种消息模式的基本代码生成 21 | // 包括带命名参数、无参数和匿名参数的消息 22 | // 使用 YAML 格式的英文和中文消息文件 23 | func TestGenerate(t *testing.T) { 24 | // Create i18n bundle with American English as base language 25 | // 创建以美式英语为基础语言的 i18n 包 26 | bundle := i18n.NewBundle(language.AmericanEnglish) 27 | bundle.RegisterUnmarshalFunc("yaml", yaml.Unmarshal) 28 | 29 | var messageFiles []*i18n.MessageFile 30 | { 31 | // Parse English message file with various message patterns 32 | // 解析包含各种消息模式的英文消息文件 33 | messageFile := rese.P1(bundle.ParseMessageFileBytes([]byte(` 34 | SAY_HELLO: "Hello, {{ .name }}!" 35 | WELCOME: "Welcome to this app!" 36 | NEW_MESSAGES: "Hi, {{ .name }}! You have {{ .count }} new messages." 37 | SUCCESS: "Success" 38 | ERROR_BAD_PARAM: "Invalid parameter: {{ .name }}" 39 | ERROR_ALREADY_EXIST: "{{ .what }} {{ .code }} already exists" 40 | ERROR_NOT_EXIST: "{{ .what }} {{ .code }} does not exist" 41 | PLEASE_CONFIRM: "Please confirm to {{ . }}" 42 | `), "en-US.yaml")) 43 | zaplog.SUG.Debugln(neatjsons.S(messageFile)) 44 | messageFiles = append(messageFiles, messageFile) 45 | } 46 | { 47 | // Parse Chinese message file with same message IDs as English version 48 | // 解析与英文版本具有相同消息 ID 的中文消息文件 49 | messageFile := rese.P1(bundle.ParseMessageFileBytes([]byte(` 50 | SAY_HELLO: "你好,{{.name}}!" 51 | WELCOME: "欢迎使用此应用!" 52 | NEW_MESSAGES: "嗨,{{.name}}!您有 {{.count}} 条新消息。" 53 | SUCCESS: "成功" 54 | ERROR_BAD_PARAM: "无效参数:{{.name}}" 55 | ERROR_ALREADY_EXIST: "{{.what}} {{.code}} 已存在" 56 | ERROR_NOT_EXIST: "{{.what}} {{.code}} 不存在" 57 | PLEASE_CONFIRM: "请确认{{.}}" 58 | `), "zh-CN.yaml")) 59 | zaplog.SUG.Debugln(neatjsons.S(messageFile)) 60 | messageFiles = append(messageFiles, messageFile) 61 | } 62 | zaplog.SUG.Debugln(neatjsons.S(bundle.LanguageTags())) 63 | 64 | // Generate type-safe Go code from message files 65 | // 从消息文件生成类型安全的 Go 代码 66 | goi18n.Generate(messageFiles, goi18n.NewOptions()) 67 | } 68 | 69 | // TestGenerate_SingleValueYaml tests code generation from YAML files with simple named params 70 | // Messages use single template variables instead of complex multi-param patterns 71 | // Validates generation of param structs and support functions 72 | // 73 | // TestGenerate_SingleValueYaml 测试从 YAML 文件生成简单命名参数的代码 74 | // 消息使用单个模板变量而非复杂的多参数模式 75 | // 验证参数结构体和支持函数的生成 76 | func TestGenerate_SingleValueYaml(t *testing.T) { 77 | bundle := i18n.NewBundle(language.AmericanEnglish) 78 | bundle.RegisterUnmarshalFunc("yaml", yaml.Unmarshal) 79 | 80 | var messageFiles []*i18n.MessageFile 81 | { 82 | messageFile := rese.P1(bundle.ParseMessageFileBytes([]byte(` 83 | ORDER_PLACED: "Your order for {{ .item }} has been placed." 84 | THANK_YOU: "Thank you for shopping with us, {{ .name }}!" 85 | TOTAL_AMOUNT: "Your total is {{ .total }}." 86 | STOCK_AVAILABLE: "We have {{ .quantity }} of {{ .item }} in stock." 87 | OUT_OF_STOCK: "{{ .item }} is currently out of stock." 88 | SHIPPING_INFO: "Your order will be shipped to {{ .address }}." 89 | PAYMENT_SUCCESS: "Payment of {{ .amount }} has been successfully processed." 90 | INVALID_PRODUCT: "Product {{ .productId }} does not exist." 91 | LOW_BALANCE: "Your balance is low. Please add funds to continue shopping." 92 | `), "en-US.yaml")) 93 | zaplog.SUG.Debugln(neatjsons.S(messageFile)) 94 | messageFiles = append(messageFiles, messageFile) 95 | } 96 | { 97 | messageFile := rese.P1(bundle.ParseMessageFileBytes([]byte(` 98 | ORDER_PLACED: "您的{{ .item }}订单已提交。" 99 | THANK_YOU: "感谢您在我们这里购物,{{ .name }}!" 100 | TOTAL_AMOUNT: "您的总计是{{ .total }}。" 101 | STOCK_AVAILABLE: "我们有{{ .quantity }}件{{ .item }}库存。" 102 | OUT_OF_STOCK: "{{ .item }}目前缺货。" 103 | SHIPPING_INFO: "您的订单将寄送到{{ .address }}。" 104 | PAYMENT_SUCCESS: "已成功处理{{ .amount }}的付款。" 105 | INVALID_PRODUCT: "产品{{ .productId }}不存在。" 106 | LOW_BALANCE: "您的余额不足。请添加资金以继续购物。" 107 | `), "zh-CN.yaml")) 108 | zaplog.SUG.Debugln(neatjsons.S(messageFile)) 109 | messageFiles = append(messageFiles, messageFile) 110 | } 111 | zaplog.SUG.Debugln(neatjsons.S(bundle.LanguageTags())) 112 | 113 | // Generate type-safe Go code from message files 114 | // 从消息文件生成类型安全的 Go 代码 115 | goi18n.Generate(messageFiles, goi18n.NewOptions()) 116 | } 117 | 118 | // TestGenerate_PluralYaml tests code generation with CLDR plural forms from YAML files 119 | // Messages define both "one" and "other" forms to handle singular and plural cases 120 | // Demonstrates how plural messages generate functions that accept PluralCount param 121 | // 122 | // TestGenerate_PluralYaml 测试从 YAML 文件生成 CLDR 复数形式的代码 123 | // 消息定义"one"和"other"形式以处理单数和复数情况 124 | // 演示复数消息如何生成接受 PluralCount 参数的函数 125 | func TestGenerate_PluralYaml(t *testing.T) { 126 | bundle := i18n.NewBundle(language.AmericanEnglish) 127 | bundle.RegisterUnmarshalFunc("yaml", yaml.Unmarshal) 128 | 129 | var messageFiles []*i18n.MessageFile 130 | { 131 | messageFile := rese.P1(bundle.ParseMessageFileBytes([]byte(` 132 | NEW_NOTIFICATIONS: 133 | one: "You have one new notification." 134 | other: "You have {{.Count}} new notifications." 135 | NEW_COMMENTS: 136 | one: "Your post received one new comment." 137 | other: "Your post received {{.Count}} new comments." 138 | NEW_LIKES: 139 | one: "Your post has one new like." 140 | other: "Your post has {{.Count}} new likes." 141 | NEW_MESSAGES: 142 | one: "You have one new message." 143 | other: "You have {{.Count}} new messages." 144 | `), "en-US.yaml")) 145 | zaplog.SUG.Debugln(neatjsons.S(messageFile)) 146 | messageFiles = append(messageFiles, messageFile) 147 | } 148 | { 149 | messageFile := rese.P1(bundle.ParseMessageFileBytes([]byte(` 150 | NEW_NOTIFICATIONS: 151 | one: "您有一个新通知。" 152 | other: "您有 {{.Count}} 个新通知。" 153 | NEW_COMMENTS: 154 | one: "您的帖子收到一个新评论。" 155 | other: "您的帖子收到 {{.Count}} 个新评论。" 156 | NEW_LIKES: 157 | one: "您的帖子有一个新点赞。" 158 | other: "您的帖子有 {{.Count}} 个新点赞。" 159 | NEW_MESSAGES: 160 | one: "您有一条新消息。" 161 | other: "您有 {{.Count}} 条新消息。" 162 | `), "zh-CN.yaml")) 163 | zaplog.SUG.Debugln(neatjsons.S(messageFile)) 164 | messageFiles = append(messageFiles, messageFile) 165 | } 166 | zaplog.SUG.Debugln(neatjsons.S(bundle.LanguageTags())) 167 | 168 | // Generate type-safe Go code with plural support from message files 169 | // 从消息文件生成支持复数的类型安全 Go 代码 170 | goi18n.Generate(messageFiles, goi18n.NewOptions()) 171 | } 172 | 173 | // TestGenerate_SingleValueJson tests code generation from JSON format message files 174 | // Uses JSON instead of YAML format to demonstrate format-agnostic support 175 | // Validates param struct generation works consistently across formats 176 | // 177 | // TestGenerate_SingleValueJson 测试从 JSON 格式消息文件生成代码 178 | // 使用 JSON 而非 YAML 格式以演示格式无关的支持 179 | // 验证参数结构体生成在不同格式下都能正常工作 180 | func TestGenerate_SingleValueJson(t *testing.T) { 181 | bundle := i18n.NewBundle(language.AmericanEnglish) 182 | bundle.RegisterUnmarshalFunc("json", json.Unmarshal) 183 | 184 | var messageFiles []*i18n.MessageFile 185 | { 186 | messageFile := rese.P1(bundle.ParseMessageFileBytes([]byte(`{ 187 | "TASK_ASSIGNED": "Task {{.task}} has been assigned to {{.user}}.", 188 | "TASK_COMPLETED": "Task {{.task}} is completed.", 189 | "DEADLINE_REMINDER": "Reminder: Task {{.task}} is due on {{.date}}.", 190 | "PRIORITY_HIGH": "Task {{.task}} is marked as high priority." 191 | }`), "en-US.json")) 192 | zaplog.SUG.Debugln(neatjsons.S(messageFile)) 193 | messageFiles = append(messageFiles, messageFile) 194 | } 195 | { 196 | messageFile := rese.P1(bundle.ParseMessageFileBytes([]byte(`{ 197 | "TASK_ASSIGNED": "任务 {{.task}} 已分配给 {{.user}}。", 198 | "TASK_COMPLETED": "任务 {{.task}} 已完成。", 199 | "DEADLINE_REMINDER": "提醒:任务 {{.task}} 将于 {{.date}} 到期。", 200 | "PRIORITY_HIGH": "任务 {{.task}} 被标记为高优先级。" 201 | }`), "zh-CN.json")) 202 | zaplog.SUG.Debugln(neatjsons.S(messageFile)) 203 | messageFiles = append(messageFiles, messageFile) 204 | } 205 | zaplog.SUG.Debugln(neatjsons.S(bundle.LanguageTags())) 206 | 207 | // Generate type-safe Go code from JSON format message files 208 | // 从 JSON 格式消息文件生成类型安全的 Go 代码 209 | goi18n.Generate(messageFiles, goi18n.NewOptions()) 210 | } 211 | 212 | // TestGenerate_PluralJson tests code generation with plural forms from JSON format files 213 | // Combines JSON format with CLDR plural rules to handle one and other cases 214 | // Ensures plural support works consistently in JSON as it does in YAML 215 | // 216 | // TestGenerate_PluralJson 测试从 JSON 格式文件生成复数形式的代码 217 | // 将 JSON 格式与 CLDR 复数规则结合以处理 one 和 other 情况 218 | // 确保 JSON 中的复数支持与 YAML 中的工作方式一致 219 | func TestGenerate_PluralJson(t *testing.T) { 220 | bundle := i18n.NewBundle(language.AmericanEnglish) 221 | bundle.RegisterUnmarshalFunc("json", json.Unmarshal) 222 | 223 | var messageFiles []*i18n.MessageFile 224 | { 225 | messageFile := rese.P1(bundle.ParseMessageFileBytes([]byte(`{ 226 | "OPEN_ISSUES": { 227 | "one": "There is one open issue in project {{.project}}.", 228 | "other": "There are {{.count}} open issues in project {{.project}}." 229 | }, 230 | "COMPLETED_TASKS": { 231 | "one": "One task is completed in project {{.project}}.", 232 | "other": "{{.count}} tasks are completed in project {{.project}}." 233 | }, 234 | "PENDING_REVIEWS": { 235 | "one": "There is one pending review for project {{.project}}.", 236 | "other": "There are {{.count}} pending reviews for project {{.project}}." 237 | }, 238 | "ACTIVE_USERS": { 239 | "one": "One user is active on project {{.project}}.", 240 | "other": "{{.count}} users are active on project {{.project}}." 241 | } 242 | }`), "en-US.json")) 243 | zaplog.SUG.Debugln(neatjsons.S(messageFile)) 244 | messageFiles = append(messageFiles, messageFile) 245 | } 246 | { 247 | messageFile := rese.P1(bundle.ParseMessageFileBytes([]byte(`{ 248 | "OPEN_ISSUES": { 249 | "one": "项目 {{.project}} 中有一个未解决问题。", 250 | "other": "项目 {{.project}} 中有 {{.count}} 个未解决问题。" 251 | }, 252 | "COMPLETED_TASKS": { 253 | "one": "项目 {{.project}} 中完成了一个任务。", 254 | "other": "项目 {{.project}} 中完成了 {{.count}} 个任务。" 255 | }, 256 | "PENDING_REVIEWS": { 257 | "one": "项目 {{.project}} 中有一个待审。", 258 | "other": "项目 {{.project}} 中有 {{.count}} 个待审。" 259 | }, 260 | "ACTIVE_USERS": { 261 | "one": "项目 {{.project}} 中有一个活跃用户。", 262 | "other": "项目 {{.project}} 中有 {{.count}} 个活跃用户。" 263 | } 264 | }`), "zh-CN.json")) 265 | zaplog.SUG.Debugln(neatjsons.S(messageFile)) 266 | messageFiles = append(messageFiles, messageFile) 267 | } 268 | zaplog.SUG.Debugln(neatjsons.S(bundle.LanguageTags())) 269 | 270 | // Generate type-safe Go code with plural support from JSON message files 271 | // 从 JSON 消息文件生成支持复数的类型安全 Go 代码 272 | goi18n.Generate(messageFiles, goi18n.NewOptions()) 273 | } 274 | -------------------------------------------------------------------------------- /README.zh.md: -------------------------------------------------------------------------------- 1 | [![GitHub Workflow Status (branch)](https://img.shields.io/github/actions/workflow/status/yyle88/goi18n/release.yml?branch=main&label=BUILD)](https://github.com/yyle88/goi18n/actions/workflows/release.yml?query=branch%3Amain) 2 | [![GoDoc](https://pkg.go.dev/badge/github.com/yyle88/goi18n)](https://pkg.go.dev/github.com/yyle88/goi18n) 3 | [![Coverage Status](https://img.shields.io/coveralls/github/yyle88/goi18n/main.svg)](https://coveralls.io/github/yyle88/goi18n?branch=main) 4 | [![Supported Go Versions](https://img.shields.io/badge/Go-1.22--1.25-lightgrey.svg)](https://github.com/yyle88/goi18n) 5 | [![GitHub Release](https://img.shields.io/github/release/yyle88/goi18n.svg)](https://github.com/yyle88/goi18n/releases) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/yyle88/goi18n)](https://goreportcard.com/report/github.com/yyle88/goi18n) 7 | 8 | # goi18n 9 | 10 | `goi18n` 是一个 Go 包和代码生成工具集,通过使用泛型参数替换 `map[string]interface{}`,使 `go-i18n` 的使用更加简洁。 11 | 12 | ## 概述 13 | 14 | `goi18n` 简化了 Go 应用程序中的国际化(i18n)开发。它能够处理 i18n 消息文件(如 YAML),并生成类型安全的 Go 代码,包括消息处理的结构体和函数。生成的代码与 `go-i18n` 包集成,支持多种语言的高效、安全翻译渲染。 15 | 16 | 17 | ## 英文文档 18 | 19 | [ENGLISH README](README.md) 20 | 21 | 22 | ## 为什么使用 goi18n? 23 | 24 | 在使用优秀的 [go-i18n](https://github.com/nicksnyder/go-i18n) 包时,你经常需要编写重复的代码: 25 | 26 | ```go 27 | // 传统方式 - 冗长且容易出错 28 | localizer.Localize(&i18n.LocalizeConfig{ 29 | MessageID: "ERROR_NOT_EXIST", 30 | TemplateData: map[string]interface{}{ 31 | "what": "User", 32 | "code": "12345", 33 | }, 34 | }) 35 | ``` 36 | 37 | 这种方式存在多个问题: 38 | - **缺乏类型安全**:容易在消息 ID 和参数名称中出现拼写错误 39 | - **运行时错误**:错误在运行时才能发现,而不是编译时 40 | - **IDE 支持有限**:无法对消息 ID 和参数进行自动补全 41 | - **维护负担**:难以追踪每个消息需要哪些参数 42 | 43 | **goi18n 解决了这些问题**,通过生成类型安全的代码: 44 | 45 | ```go 46 | // 使用 goi18n - 类型安全且简洁 47 | message1.I18nErrorNotExist(&message1.ErrorNotExistParam{ 48 | What: "User", 49 | Code: "12345", 50 | }) 51 | ``` 52 | 53 | 优势: 54 | - ✅ **编译时安全**:在部署前捕获错误 55 | - ✅ **IDE 自动补全**:完整的 IntelliSense 支持 56 | - ✅ **重构友好**:可以自信地重命名参数 57 | - ✅ **自文档化**:生成的结构体显示所需参数 58 | - ✅ **零运行时开销**:性能与手写代码相同 59 | 60 | ## 功能特性 61 | 62 | - **代码生成**:从 i18n 消息文件自动生成 Go 结构体和函数。 63 | - **类型安全**:为命名参数生成结构体(如 `ErrorAlreadyExistParam`),为匿名参数生成函数(如 `NewConfirmAction`)。 64 | - **灵活输出**:支持自定义输出路径和包名,从目标目录自动推导。 65 | - **多语言支持**:已测试支持英语(`en-US`)、简体中文(`zh-CN`)和高棉语(`km-KH`)。 66 | - **与 go-i18n 集成**:生成返回 `i18n.LocalizeConfig` 的 `I18n*` 函数,直接用于 `go-i18n`。 67 | - **规范命名**:将消息 ID(如 `ERROR_ALREADY_EXIST`)转换为 PascalCase(如 `ErrorAlreadyExist`),使用 `strcase`。 68 | - **代码格式化**:使用 `formatgo` 确保代码格式规范,通过 `syntaxgo_ast` 自动添加必要导入。 69 | - **Unicode 支持**:使用自定义命名函数处理非 ASCII 消息 ID。 70 | - **复数形式**:支持多种语言的 CLDR 复数规则(one、other、few、many、zero、two)。 71 | 72 | ## 生成代码结构 73 | 74 | 针对每种消息类型,goi18n 生成不同的代码模式: 75 | 76 | ### 1. 带命名参数的消息 77 | 78 | **输入(YAML):** 79 | ```yaml 80 | ERROR_NOT_EXIST: "{{ .what }} {{ .code }} does not exist" 81 | ``` 82 | 83 | **生成的代码:** 84 | ```go 85 | type ErrorNotExistParam struct { 86 | What any 87 | Code any 88 | } 89 | 90 | func (p *ErrorNotExistParam) GetTemplateValues() map[string]any { 91 | res := make(map[string]any) 92 | if p.What != nil { 93 | res["what"] = p.What 94 | } 95 | if p.Code != nil { 96 | res["code"] = p.Code 97 | } 98 | return res 99 | } 100 | 101 | func NewErrorNotExist(data *ErrorNotExistParam) (string, map[string]any) { 102 | return "ERROR_NOT_EXIST", data.GetTemplateValues() 103 | } 104 | 105 | func I18nErrorNotExist(data *ErrorNotExistParam) *i18n.LocalizeConfig { 106 | messageID, valuesMap := NewErrorNotExist(data) 107 | return &i18n.LocalizeConfig{ 108 | MessageID: messageID, 109 | TemplateData: valuesMap, 110 | } 111 | } 112 | ``` 113 | 114 | ### 2. 带匿名参数的消息 115 | 116 | **输入(YAML):** 117 | ```yaml 118 | PLEASE_CONFIRM: "Please confirm {{ . }}" 119 | ``` 120 | 121 | **生成的代码:** 122 | ```go 123 | func NewPleaseConfirm[Value comparable](value Value) (string, Value) { 124 | return "PLEASE_CONFIRM", value 125 | } 126 | 127 | func I18nPleaseConfirm[Value comparable](value Value) *i18n.LocalizeConfig { 128 | messageID, tempValue := NewPleaseConfirm(value) 129 | return &i18n.LocalizeConfig{ 130 | MessageID: messageID, 131 | TemplateData: tempValue, 132 | } 133 | } 134 | ``` 135 | 136 | ### 3. 无参数的消息 137 | 138 | **输入(YAML):** 139 | ```yaml 140 | SUCCESS: "Success" 141 | ``` 142 | 143 | **生成的代码:** 144 | ```go 145 | func NewSuccess() string { 146 | return "SUCCESS" 147 | } 148 | 149 | func I18nSuccess() *i18n.LocalizeConfig { 150 | messageID := NewSuccess() 151 | return &i18n.LocalizeConfig{ 152 | MessageID: messageID, 153 | } 154 | } 155 | ``` 156 | 157 | ## 配置选项 158 | 159 | goi18n 通过 `Options` 类型提供灵活的配置: 160 | 161 | ```go 162 | options := goi18n.NewOptions(). 163 | WithOutputPath("internal/i18n/messages.go"). 164 | WithPkgName("i18n"). 165 | WithGenerateNewMessage(true). 166 | WithAllowNonAsciiRune(false) 167 | ``` 168 | 169 | 可用选项: 170 | 171 | - **`WithOutputPath(path string)`**:设置输出文件路径(必须以 `.go` 结尾) 172 | - **`WithPkgName(name string)`**:设置生成代码中使用的包名 173 | - **`WithOutputPathWithPkgName(path string)`**:设置输出路径并从现有文件/父 DIR 自动推断包名 174 | - **`WithGenerateNewMessage(bool)`**:启用生成返回 `(messageID, templateData)` 元组的 `New*` 函数 175 | - **`WithAllowNonAsciiRune(bool)`**:启用对消息 ID 中非 ASCII 字符的支持 176 | - **`WithUnicodeMessageName(func)`**:自定义 Unicode 消息 ID 的命名函数 177 | - **`WithUnicodeStructName(func)`**:自定义 Unicode 结构体名称的命名函数 178 | - **`WithUnicodeFieldName(func)`**:自定义 Unicode 字段名称的命名函数 179 | 180 | ## 安装 181 | 182 | ```bash 183 | go get github.com/yyle88/goi18n 184 | ``` 185 | 186 | ## 使用方法 187 | 188 | ### 步骤 1:准备 i18n 消息文件 189 | 190 | 在目录(如 `i18n/`)中为每种支持的语言创建 YAML 文件: 191 | 192 | **`i18n/en-US.yaml`**: 193 | ```yaml 194 | SAY_HELLO: "Hello, {{ .name }}!" 195 | WELCOME: "Welcome to this app!" 196 | SUCCESS: "Success" 197 | PLEASE_CONFIRM: "Please confirm {{ . }}" 198 | ERROR_NOT_EXIST: "{{ .what }} {{ .code }} does not exist" 199 | ERROR_ALREADY_EXIST: "{{ .what }} {{ .code }} already exists" 200 | ``` 201 | 202 | **`i18n/zh-CN.yaml`**: 203 | ```yaml 204 | SAY_HELLO: "你好,{{ .name }}!" 205 | WELCOME: "欢迎使用此应用!" 206 | SUCCESS: "成功" 207 | PLEASE_CONFIRM: "请确认{{ . }}" 208 | ERROR_NOT_EXIST: "{{ .what }} {{ .code }} 不存在" 209 | ERROR_ALREADY_EXIST: "{{ .what }} {{ .code }} 已存在" 210 | ``` 211 | 212 | ### 步骤 2:生成代码 213 | 214 | 使用 `goi18n.Generate` 函数处理消息文件并生成 Go 代码: 215 | 216 | ```go 217 | package main 218 | 219 | import ( 220 | "github.com/nicksnyder/go-i18n/v2/i18n" 221 | "github.com/yyle88/goi18n" 222 | "github.com/yyle88/rese" 223 | "gopkg.in/yaml.v3" 224 | ) 225 | 226 | func main() { 227 | bundle := i18n.NewBundle(language.AmericanEnglish) 228 | bundle.RegisterUnmarshalFunc("yaml", yaml.Unmarshal) 229 | messageFile := rese.P1(bundle.LoadMessageFile("i18n/en-US.yaml")) 230 | 231 | options := goi18n.NewOptions().WithOutputPathWithPkgName("output/message.go") 232 | goi18n.Generate([]*i18n.MessageFile{messageFile}, options) 233 | } 234 | ``` 235 | 236 | 这将生成一个文件(`output/message.go`),包名为 `output`,包含结构体(如 `ErrorAlreadyExistParam`)和函数(如 `NewSayHello`、`I18nSayHello`)。 237 | 238 | ### 步骤 3:使用生成的代码 239 | 240 | 导入生成的包并使用函数进行翻译: 241 | 242 | ```go 243 | package main 244 | 245 | import ( 246 | "fmt" 247 | "github.com/nicksnyder/go-i18n/v2/i18n" 248 | "github.com/yyle88/goi18n/internal/examples/example1/example1generate/example1message" 249 | "golang.org/x/text/language" 250 | "gopkg.in/yaml.v3" 251 | ) 252 | 253 | func main() { 254 | bundle := i18n.NewBundle(language.AmericanEnglish) 255 | bundle.RegisterUnmarshalFunc("yaml", yaml.Unmarshal) 256 | bundle.MustLoadMessageFile("i18n/zh-CN.yaml") 257 | 258 | localizer := i18n.NewLocalizer(bundle, "zh-CN") 259 | 260 | // 使用 I18nSayHello 261 | config := example1message.I18nSayHello(&example1message.SayHelloParam{Name: "杨亦乐"}) 262 | msg, err := localizer.Localize(config) 263 | if err != nil { 264 | panic(err) 265 | } 266 | fmt.Println(msg) // 输出:你好,杨亦乐! 267 | 268 | // 使用 NewSayHello 269 | messageID, data := example1message.NewSayHello(&example1message.SayHelloParam{Name: "杨亦乐"}) 270 | msg, err = localizer.Localize(&i18n.LocalizeConfig{ 271 | MessageID: messageID, 272 | TemplateData: data, 273 | }) 274 | fmt.Println(msg) // 输出:你好,杨亦乐! 275 | } 276 | ``` 277 | 278 | ## 示例 279 | 280 | ``` 281 | goi18n/ 282 | ├── goi18n.go # 核心逻辑 283 | ├── internal/ 284 | │ └── examples/ 285 | │ ├── example1/ # 展示如何读取 yaml 配置获取翻译信息 286 | │ │ ├── example1_test.go 287 | │ │ └── internal/ 288 | │ │ └── message1/ 289 | │ │ ├── en-US.yaml 290 | │ │ ├── zh-CN.yaml 291 | │ │ └── km-KH.yaml 292 | │ ├── example2/ # 展示如何读取 json 配置获取翻译信息 293 | │ │ ├── example2_test.go 294 | │ │ └── internal/ 295 | │ │ └── message2/ 296 | │ │ ├── trans.en-US.json 297 | │ │ └── trans.zh-CN.json 298 | │ └── example3/ # 展示如何使用【中文优先】的国际化配置 299 | │ ├── example3_test.go 300 | │ └── internal/ 301 | │ └── message3/ 302 | │ ├── msg.en-US.yaml 303 | │ └── msg.zh-CN.yaml 304 | ``` 305 | 306 | - See [生成逻辑1](internal/examples/example1/internal/message1/i18n.gen_test.go) and [使用示例1](internal/examples/example1/example1_test.go). 307 | - See [生成逻辑2](internal/examples/example2/internal/message2/i18n.gen_test.go) and [使用示例2](internal/examples/example2/example2_test.go). 308 | - See [生成逻辑3](internal/examples/example3/internal/message3/i18n.gen_test.go) and [使用示例3](internal/examples/example3/example3_test.go). 309 | 310 | ## 测试 311 | 312 | 项目在 `internal/examples/example1/example1_test.go` 中包含测试用例,覆盖以下场景: 313 | 314 | - 命名参数:`I18nSayHello`、`I18nErrorAlreadyExist` 315 | - 匿名参数:`I18nPleaseConfirm` 316 | - 无参数:`I18nWelcome`、`I18nSuccess` 317 | 318 | 319 | 320 | 321 | ## 📄 许可证类型 322 | 323 | MIT 许可证。详情请参阅 [LICENSE](LICENSE)。 324 | 325 | --- 326 | 327 | ## 🤝 贡献新代码 328 | 329 | 欢迎贡献代码!报告错误、建议功能和贡献代码: 330 | 331 | - 🐛 **发现错误?** 在 GitHub 上提出问题并附上复现步骤 332 | - 💡 **有功能想法?** 创建问题讨论建议 333 | - 📖 **文档令人困惑?** 报告它以便我们改进 334 | - 🚀 **需要新功能?** 分享使用场景以帮助我们理解需求 335 | - ⚡ **性能问题?** 通过报告慢速操作帮助我们优化 336 | - 🔧 **配置问题?** 询问关于复杂设置的问题 337 | - 📢 **关注项目进展?** Watch 仓库以获取新版本和功能 338 | - 🌟 **成功案例?** 分享这个包如何改进工作流程 339 | - 💬 **反馈?** 我们欢迎建议和评论 340 | 341 | --- 342 | 343 | ## 🔧 开发流程 344 | 345 | 新代码贡献,请遵循以下流程: 346 | 347 | 1. **Fork**:在 GitHub 上 Fork 仓库(使用网页界面)。 348 | 2. **Clone**:克隆 Forked 项目(`git clone https://github.com/yourname/repo-name.git`)。 349 | 3. **Navigate**:进入克隆的项目(`cd repo-name`) 350 | 4. **Branch**:创建功能分支(`git checkout -b feature/xxx`)。 351 | 5. **Code**:实现更改并编写全面的测试 352 | 6. **Testing**:(Golang 项目)确保测试通过(`go test ./...`)并遵循 Go 代码风格约定 353 | 7. **Documentation**:更新文档以支持面向客户端的更改,并使用有意义的提交消息 354 | 8. **Stage**:暂存更改(`git add .`) 355 | 9. **Commit**:提交更改(`git commit -m "Add feature xxx"`)确保向后兼容的代码 356 | 10. **Push**:推送到分支(`git push origin feature/xxx`)。 357 | 11. **PR**:在 GitHub 上打开合并请求(在 GitHub 网页上)并附上详细描述。 358 | 359 | 请确保测试通过并包含相关文档更新。 360 | 361 | --- 362 | 363 | ## 🌟 贡献与支持 364 | 365 | 欢迎通过提交合并请求和报告问题为此项目做出贡献。 366 | 367 | **项目支持:** 368 | 369 | - ⭐ **给 GitHub 点星** 如果这个项目对你有帮助 370 | - 🤝 **与团队成员分享** 和(golang)编程朋友 371 | - 📝 **撰写技术博客** 关于开发工具和工作流程 - 我们提供内容写作支持 372 | - 🌟 **加入生态系统** - 致力于支持开源和(golang)开发场景 373 | 374 | **祝编程愉快!** 🎉🎉🎉 375 | 376 | 377 | 378 | --- 379 | 380 | ## GitHub Stars 381 | 382 | [![starring](https://starchart.cc/yyle88/goi18n.svg?variant=adaptive)](https://starchart.cc/yyle88/goi18n) 383 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GitHub Workflow Status (branch)](https://img.shields.io/github/actions/workflow/status/yyle88/goi18n/release.yml?branch=main&label=BUILD)](https://github.com/yyle88/goi18n/actions/workflows/release.yml?query=branch%3Amain) 2 | [![GoDoc](https://pkg.go.dev/badge/github.com/yyle88/goi18n)](https://pkg.go.dev/github.com/yyle88/goi18n) 3 | [![Coverage Status](https://img.shields.io/coveralls/github/yyle88/goi18n/main.svg)](https://coveralls.io/github/yyle88/goi18n?branch=main) 4 | [![Supported Go Versions](https://img.shields.io/badge/Go-1.22--1.25-lightgrey.svg)](https://github.com/yyle88/goi18n) 5 | [![GitHub Release](https://img.shields.io/github/release/yyle88/goi18n.svg)](https://github.com/yyle88/goi18n/releases) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/yyle88/goi18n)](https://goreportcard.com/report/github.com/yyle88/goi18n) 7 | 8 | # Goi18n 9 | 10 | `goi18n` replace map[string]interface{} with generic parameters to make go-i18n more concise. 11 | 12 | ## Overview 13 | 14 | `goi18n` is a Go package and code generation toolkit that simplifies internationalization (i18n) in Go applications. It processes i18n message files (e.g., YAML) and generates type-safe Go code, including structs and functions to handle messages. The generated code integrates with the `go-i18n` package, enabling efficient and safe translation rendering across multiple languages. 15 | 16 | ## Why goi18n? 17 | 18 | When working with the excellent [go-i18n](https://github.com/nicksnyder/go-i18n) package, you often need to write repetitive code like this: 19 | 20 | ```go 21 | // Traditional approach - verbose and error-prone 22 | localizer.Localize(&i18n.LocalizeConfig{ 23 | MessageID: "ERROR_NOT_EXIST", 24 | TemplateData: map[string]interface{}{ 25 | "what": "User", 26 | "code": "12345", 27 | }, 28 | }) 29 | ``` 30 | 31 | This approach has multiple issues: 32 | - **No type safety**: Simple to make typos in message IDs and param names 33 | - **Runtime errors**: Mistakes are caught at runtime, not compile time 34 | - **Limited IDE support**: No auto-completion on message IDs and params 35 | - **Maintenance burden**: Hard to track which params each message needs 36 | 37 | **goi18n solves these problems** by generating type-safe code: 38 | 39 | ```go 40 | // With goi18n - type-safe and clean 41 | message1.I18nErrorNotExist(&message1.ErrorNotExistParam{ 42 | What: "User", 43 | Code: "12345", 44 | }) 45 | ``` 46 | 47 | Benefits: 48 | - ✅ **Compile-time safety**: Catch errors before deployment 49 | - ✅ **IDE auto-completion**: Complete IntelliSense support 50 | - ✅ **Refactoring friendly**: Rename params with confidence 51 | - ✅ **Self-documenting**: Generated structs show required params 52 | - ✅ **Zero runtime overhead**: Same performance as hand-written code 53 | 54 | 55 | ## CHINESE README 56 | 57 | [中文说明](README.zh.md) 58 | 59 | 60 | ## Features 61 | 62 | - **Code Generation**: Auto-generates Go structs and functions from i18n message files. 63 | - **Type Safety**: Creates structs with named parameters (e.g., `ErrorAlreadyExistParam`) and functions with anonymous parameters (e.g., `NewConfirmAction`). 64 | - **Flexible Output**: Supports custom output paths and package names, derived from the target DIR. 65 | - **Multi-Language Support**: Tested with English (`en-US`), Simplified Chinese (`zh-CN`), and Khmer (`km-KH`). 66 | - **Integration with go-i18n**: Generates `I18n*` functions that return `i18n.LocalizeConfig`, enabling direct use with `go-i18n`. 67 | - **Clean Naming**: Converts message IDs (e.g., `ERROR_ALREADY_EXIST`) to PascalCase (e.g., `ErrorAlreadyExist`) using `strcase`. 68 | - **Format and Imports**: Ensures generated code is well-formatted (`formatgo`) and includes needed imports (`syntaxgo_ast`). 69 | - **Unicode Support**: Handles non-ASCII message IDs with custom naming functions. 70 | - **Plural Forms**: Supports CLDR plural rules (one, other, few, many, zero, two) across multiple languages. 71 | 72 | ## Generated Code Structure 73 | 74 | For each message type, goi18n generates different code patterns: 75 | 76 | ### 1. Messages with Named Parameters 77 | 78 | **Input (YAML):** 79 | ```yaml 80 | ERROR_NOT_EXIST: "{{ .what }} {{ .code }} does not exist" 81 | ``` 82 | 83 | **Generated Code:** 84 | ```go 85 | type ErrorNotExistParam struct { 86 | What any 87 | Code any 88 | } 89 | 90 | func (p *ErrorNotExistParam) GetTemplateValues() map[string]any { 91 | res := make(map[string]any) 92 | if p.What != nil { 93 | res["what"] = p.What 94 | } 95 | if p.Code != nil { 96 | res["code"] = p.Code 97 | } 98 | return res 99 | } 100 | 101 | func NewErrorNotExist(data *ErrorNotExistParam) (string, map[string]any) { 102 | return "ERROR_NOT_EXIST", data.GetTemplateValues() 103 | } 104 | 105 | func I18nErrorNotExist(data *ErrorNotExistParam) *i18n.LocalizeConfig { 106 | messageID, valuesMap := NewErrorNotExist(data) 107 | return &i18n.LocalizeConfig{ 108 | MessageID: messageID, 109 | TemplateData: valuesMap, 110 | } 111 | } 112 | ``` 113 | 114 | ### 2. Messages with Anonymous Parameters 115 | 116 | **Input (YAML):** 117 | ```yaml 118 | PLEASE_CONFIRM: "Please confirm {{ . }}" 119 | ``` 120 | 121 | **Generated Code:** 122 | ```go 123 | func NewPleaseConfirm[Value comparable](value Value) (string, Value) { 124 | return "PLEASE_CONFIRM", value 125 | } 126 | 127 | func I18nPleaseConfirm[Value comparable](value Value) *i18n.LocalizeConfig { 128 | messageID, tempValue := NewPleaseConfirm(value) 129 | return &i18n.LocalizeConfig{ 130 | MessageID: messageID, 131 | TemplateData: tempValue, 132 | } 133 | } 134 | ``` 135 | 136 | ### 3. Messages without Parameters 137 | 138 | **Input (YAML):** 139 | ```yaml 140 | SUCCESS: "Success" 141 | ``` 142 | 143 | **Generated Code:** 144 | ```go 145 | func NewSuccess() string { 146 | return "SUCCESS" 147 | } 148 | 149 | func I18nSuccess() *i18n.LocalizeConfig { 150 | messageID := NewSuccess() 151 | return &i18n.LocalizeConfig{ 152 | MessageID: messageID, 153 | } 154 | } 155 | ``` 156 | 157 | ## Configuration Options 158 | 159 | goi18n provides flexible configuration through the `Options` type: 160 | 161 | ```go 162 | options := goi18n.NewOptions(). 163 | WithOutputPath("internal/i18n/messages.go"). 164 | WithPkgName("i18n"). 165 | WithGenerateNewMessage(true). 166 | WithAllowNonAsciiRune(false) 167 | ``` 168 | 169 | Available options: 170 | 171 | - **`WithOutputPath(path string)`**: Set the output file path (must end with `.go`) 172 | - **`WithPkgName(name string)`**: Set the package name used in generated code 173 | - **`WithOutputPathWithPkgName(path string)`**: Set output path and auto-infer package name from existing file/parent DIR 174 | - **`WithGenerateNewMessage(bool)`**: Enable generation of `New*` functions that return `(messageID, templateData)` tuples 175 | - **`WithAllowNonAsciiRune(bool)`**: Enable support on non-ASCII characters in message IDs 176 | - **`WithUnicodeMessageName(func)`**: Customize naming function on Unicode message IDs 177 | - **`WithUnicodeStructName(func)`**: Customize naming function on Unicode struct names 178 | - **`WithUnicodeFieldName(func)`**: Customize naming function on Unicode field names 179 | 180 | ## Installation 181 | 182 | ```bash 183 | go get github.com/yyle88/goi18n 184 | ``` 185 | 186 | ## Usage 187 | 188 | ### Step 1: Prepare i18n Message Files 189 | 190 | Create YAML files with each supported locale in a DIR (e.g., `i18n/`): 191 | 192 | **`i18n/en-US.yaml`**: 193 | ```yaml 194 | SAY_HELLO: "Hello, {{ .name }}!" 195 | WELCOME: "Welcome to this app!" 196 | SUCCESS: "Success" 197 | PLEASE_CONFIRM: "Please confirm {{ . }}" 198 | ERROR_NOT_EXIST: "{{ .what }} {{ .code }} does not exist" 199 | ERROR_ALREADY_EXIST: "{{ .what }} {{ .code }} already exists" 200 | ``` 201 | 202 | **`i18n/zh-CN.yaml`**: 203 | ```yaml 204 | SAY_HELLO: "你好,{{ .name }}!" 205 | WELCOME: "欢迎使用此应用!" 206 | SUCCESS: "成功" 207 | PLEASE_CONFIRM: "请确认{{ . }}" 208 | ERROR_NOT_EXIST: "{{ .what }} {{ .code }} 不存在" 209 | ERROR_ALREADY_EXIST: "{{ .what }} {{ .code }} 已存在" 210 | ``` 211 | 212 | ### Step 2: Generate Code 213 | 214 | Use the `goi18n.Generate` function to process message files and generate Go code: 215 | 216 | ```go 217 | package main 218 | 219 | import ( 220 | "github.com/nicksnyder/go-i18n/v2/i18n" 221 | "github.com/yyle88/goi18n" 222 | "github.com/yyle88/rese" 223 | "golang.org/x/text/language" 224 | "gopkg.in/yaml.v3" 225 | ) 226 | 227 | func main() { 228 | bundle := i18n.NewBundle(language.AmericanEnglish) 229 | bundle.RegisterUnmarshalFunc("yaml", yaml.Unmarshal) 230 | messageFile := rese.P1(bundle.LoadMessageFile("i18n/en-US.yaml")) 231 | 232 | options := goi18n.NewOptions().WithOutputPathWithPkgName("output/message.go") 233 | goi18n.Generate([]*i18n.MessageFile{messageFile}, options) 234 | } 235 | ``` 236 | 237 | This generates a file (`output/message.go`) with package `output`, containing structs (e.g., `ErrorAlreadyExistParam`) and functions (e.g., `NewSayHello`, `I18nSayHello`). 238 | 239 | ### Step 3: Use Generated Code 240 | 241 | Import the generated package and use the functions to translate: 242 | 243 | ```go 244 | package example1_test 245 | 246 | import ( 247 | "testing" 248 | "github.com/nicksnyder/go-i18n/v2/i18n" 249 | "github.com/stretchr/testify/require" 250 | "github.com/yyle88/goi18n/internal/examples/example1/internal/message1" 251 | ) 252 | 253 | func TestI18nSayHello(t *testing.T) { 254 | bundle, _ := message1.LoadI18nFiles() 255 | localizer := i18n.NewLocalizer(bundle, "zh-CN") 256 | 257 | // Using I18nSayHello 258 | msg, err := localizer.Localize(message1.I18nSayHello(&message1.SayHelloParam{ 259 | Name: "杨亦乐", 260 | })) 261 | require.NoError(t, err) 262 | require.Equal(t, "你好,杨亦乐!", msg) 263 | } 264 | 265 | func TestNewSayHello(t *testing.T) { 266 | bundle, _ := message1.LoadI18nFiles() 267 | localizer := i18n.NewLocalizer(bundle, "zh-CN") 268 | 269 | // Using NewSayHello 270 | messageID, msgValues := message1.NewSayHello(&message1.SayHelloParam{ 271 | Name: "杨亦乐", 272 | }) 273 | 274 | msg, err := localizer.Localize(&i18n.LocalizeConfig{ 275 | MessageID: messageID, 276 | TemplateData: msgValues, 277 | }) 278 | require.NoError(t, err) 279 | require.Equal(t, "你好,杨亦乐!", msg) 280 | } 281 | 282 | func TestI18nErrorNotExist(t *testing.T) { 283 | bundle, _ := message1.LoadI18nFiles() 284 | localizer := i18n.NewLocalizer(bundle, "zh-CN") 285 | 286 | msg, err := localizer.Localize(message1.I18nErrorNotExist(&message1.ErrorNotExistParam{ 287 | What: "数据库里", 288 | Code: "账号信息", 289 | })) 290 | require.NoError(t, err) 291 | require.Equal(t, "数据库里 账号信息 不存在", msg) 292 | } 293 | ``` 294 | 295 | ## Example 296 | 297 | ``` 298 | goi18n/ 299 | ├── goi18n.go # Main logic of the goi18n package 300 | ├── internal/ 301 | │ └── examples/ 302 | │ ├── example1/ # Example and test code with YAML-based internationalization 303 | │ │ ├── example1_test.go 304 | │ │ └── internal/ 305 | │ │ └── message1/ 306 | │ │ ├── en-US.yaml 307 | │ │ ├── zh-CN.yaml 308 | │ │ └── km-KH.yaml 309 | │ ├── example2/ # Example and test code with JSON-based internationalization 310 | │ │ ├── example2_test.go 311 | │ │ └── internal/ 312 | │ │ └── message2/ 313 | │ │ ├── trans.en-US.json 314 | │ │ └── trans.zh-CN.json 315 | │ └── example3/ # Example and test code with 【chinese-first】 internationalization 316 | │ ├── example3_test.go 317 | │ └── internal/ 318 | │ └── message3/ 319 | │ ├── msg.en-US.yaml 320 | │ └── msg.zh-CN.yaml 321 | ``` 322 | 323 | 324 | - See [generate logic in example1](internal/examples/example1/internal/message1/i18n.gen_test.go) and [usage examples in example1](internal/examples/example1/example1_test.go). 325 | - See [generate logic in example2](internal/examples/example2/internal/message2/i18n.gen_test.go) and [usage examples in example2](internal/examples/example2/example2_test.go). 326 | - See [generate logic in example3](internal/examples/example3/internal/message3/i18n.gen_test.go) and [usage examples in example3](internal/examples/example3/example3_test.go). 327 | 328 | ## Testing 329 | 330 | The project includes tests in `internal/examples/example1/example1_test.go`, covering various use cases: 331 | 332 | - Named parameters: `I18nSayHello`, `I18nErrorAlreadyExist` 333 | - Anonymous parameters: `I18nPleaseConfirm` 334 | - No parameters: `I18nWelcome`, `I18nSuccess` 335 | 336 | 337 | 338 | 339 | ## 📄 License 340 | 341 | MIT License. See [LICENSE](LICENSE). 342 | 343 | --- 344 | 345 | ## 🤝 Contributing 346 | 347 | Contributions are welcome! Report bugs, suggest features, and contribute code: 348 | 349 | - 🐛 **Found a mistake?** Open an issue on GitHub with reproduction steps 350 | - 💡 **Have a feature idea?** Create an issue to discuss the suggestion 351 | - 📖 **Documentation confusing?** Report it so we can improve 352 | - 🚀 **Need new features?** Share the use cases to help us understand requirements 353 | - ⚡ **Performance issue?** Help us optimize through reporting slow operations 354 | - 🔧 **Configuration problem?** Ask questions about complex setups 355 | - 📢 **Follow project progress?** Watch the repo to get new releases and features 356 | - 🌟 **Success stories?** Share how this package improved the workflow 357 | - 💬 **Feedback?** We welcome suggestions and comments 358 | 359 | --- 360 | 361 | ## 🔧 Development 362 | 363 | New code contributions, follow this process: 364 | 365 | 1. **Fork**: Fork the repo on GitHub (using the webpage UI). 366 | 2. **Clone**: Clone the forked project (`git clone https://github.com/yourname/repo-name.git`). 367 | 3. **Navigate**: Navigate to the cloned project (`cd repo-name`) 368 | 4. **Branch**: Create a feature branch (`git checkout -b feature/xxx`). 369 | 5. **Code**: Implement the changes with comprehensive tests 370 | 6. **Testing**: (Golang project) Ensure tests pass (`go test ./...`) and follow Go code style conventions 371 | 7. **Documentation**: Update documentation to support client-facing changes and use significant commit messages 372 | 8. **Stage**: Stage changes (`git add .`) 373 | 9. **Commit**: Commit changes (`git commit -m "Add feature xxx"`) ensuring backward compatible code 374 | 10. **Push**: Push to the branch (`git push origin feature/xxx`). 375 | 11. **PR**: Open a merge request on GitHub (on the GitHub webpage) with detailed description. 376 | 377 | Please ensure tests pass and include relevant documentation updates. 378 | 379 | --- 380 | 381 | ## 🌟 Support 382 | 383 | Welcome to contribute to this project via submitting merge requests and reporting issues. 384 | 385 | **Project Support:** 386 | 387 | - ⭐ **Give GitHub stars** if this project helps you 388 | - 🤝 **Share with teammates** and (golang) programming friends 389 | - 📝 **Write tech blogs** about development tools and workflows - we provide content writing support 390 | - 🌟 **Join the ecosystem** - committed to supporting open source and the (golang) development scene 391 | 392 | **Have Fun Coding with this package!** 🎉🎉🎉 393 | 394 | 395 | 396 | --- 397 | 398 | ## GitHub Stars 399 | 400 | [![starring](https://starchart.cc/yyle88/goi18n.svg?variant=adaptive)](https://starchart.cc/yyle88/goi18n) 401 | -------------------------------------------------------------------------------- /goi18n.go: -------------------------------------------------------------------------------- 1 | // Package goi18n provides code generation for go-i18n with type-safe message handling 2 | // Auto generates Go structs and functions from i18n message files 3 | // Supports named params, anonymous params, and message without params 4 | // Integrates with go-i18n package to provide type-safe translation 5 | // 6 | // Package goi18n 为 go-i18n 提供类型安全的代码生成 7 | // 从国际化消息文件自动生成 Go 结构体和函数 8 | // 支持命名参数、匿名参数和无参数消息 9 | // 集成 go-i18n 包以提供类型安全的翻译 10 | package goi18n 11 | 12 | import ( 13 | "os" 14 | "regexp" 15 | 16 | "github.com/emirpasic/gods/v2/maps/linkedhashmap" 17 | "github.com/emirpasic/gods/v2/sets/linkedhashset" 18 | "github.com/iancoleman/strcase" 19 | "github.com/nicksnyder/go-i18n/v2/i18n" 20 | "github.com/yyle88/formatgo" 21 | "github.com/yyle88/goi18n/internal/utils" 22 | "github.com/yyle88/must" 23 | "github.com/yyle88/must/mustslice" 24 | "github.com/yyle88/neatjson/neatjsons" 25 | "github.com/yyle88/printgo" 26 | "github.com/yyle88/sortx" 27 | "github.com/yyle88/syntaxgo/syntaxgo_ast" 28 | "github.com/yyle88/tern" 29 | "github.com/yyle88/zaplog" 30 | "go.uber.org/zap" 31 | ) 32 | 33 | // Generate creates type-safe Go code from i18n message files 34 | // Parses message files, extracts params, and generates structs and functions 35 | // Output code provides type-safe wrappers for go-i18n LocalizeConfig 36 | // 37 | // Generate 从国际化消息文件生成类型安全的 Go 代码 38 | // 解析消息文件,提取参数,生成结构体和函数 39 | // 输出代码为 go-i18n LocalizeConfig 提供类型安全的包装 40 | func Generate(messageFiles []*i18n.MessageFile, options *Options) { 41 | mapParams := ParseParamNames(messageFiles, options) 42 | zaplog.SUG.Debugln(neatjsons.S(mapParams)) 43 | 44 | messageParams := SortMessageParams(mapParams) 45 | zaplog.SUG.Debugln(neatjsons.S(messageParams)) 46 | 47 | srcCode := CreateMessageFunctions(messageParams, options) 48 | zaplog.SUG.Debugln(string(srcCode)) 49 | 50 | if options.outputPath != "" && options.pkgName != "" { 51 | WriteContentToCodeFile(srcCode, options) 52 | } else { 53 | zaplog.SUG.Debugln("NOT write-content-to-code-file RETURN") 54 | } 55 | } 56 | 57 | // Param contains message param metadata for code generation 58 | // Names: named params extracted from message template 59 | // HasOneAnonymous: whether message uses anonymous param {{ . }} 60 | // NeedPluralCount: whether message needs count for one/other forms 61 | // 62 | // Param 包含用于代码生成的消息参数元数据 63 | // Names: 从消息模板提取的命名参数 64 | // HasOneAnonymous: 消息是否使用匿名参数 {{ . }} 65 | // NeedPluralCount: 消息是否需要 one/other 形式的计数 66 | type Param struct { 67 | Names *linkedhashset.Set[string] 68 | HasOneAnonymous bool 69 | NeedPluralCount bool 70 | } 71 | 72 | // ParseParamNames extracts params from message files and builds metadata map 73 | // Scans message templates using regex to find named and anonymous params 74 | // Detects when messages need count for one/other forms 75 | // Returns map of message ID to Param metadata 76 | // 77 | // ParseParamNames 从消息文件提取参数并构建元数据映射 78 | // 使用正则扫描消息模板查找命名和匿名参数 79 | // 检测消息是否需要 one/other 形式的计数 80 | // 返回消息 ID 到 Param 元数据的映射 81 | func ParseParamNames(messageFiles []*i18n.MessageFile, options *Options) map[string]*Param { 82 | res := map[string]*Param{} 83 | 84 | // Select regex pattern based on ASCII vs Unicode support 85 | // ASCII mode: \w matches [a-zA-Z0-9_] 86 | // Unicode mode: \S matches any non-whitespace character 87 | // 88 | // 根据 ASCII 或 Unicode 支持选择正则模式 89 | // ASCII 模式:\w 匹配 [a-zA-Z0-9_] 90 | // Unicode 模式:\S 匹配任意非空白字符 91 | pattern := tern.BFF(!options.allowNonAsciiRune, func() *regexp.Regexp { 92 | // Match {{ .variable }}, variable contains ASCII letters/digits/underscores 93 | // 匹配 {{ .变量名 }},变量名由 ASCII 字母数字下划线组成 94 | return regexp.MustCompile(`\{\{\s*\.(\w*?)\s*}}`) 95 | }, func() *regexp.Regexp { 96 | // Match {{ .variable }}, variable contains any non-whitespace chars 97 | // 匹配 {{ .变量名 }},变量名由任意非空白字符组成 98 | return regexp.MustCompile(`\{\{\s*\.(\S*?)\s*}}`) 99 | }) 100 | 101 | for _, messageFile := range messageFiles { 102 | for _, message := range messageFile.Messages { 103 | // Get or create Param metadata for this message ID 104 | // 获取或创建此消息 ID 的 Param 元数据 105 | param, ok := res[message.ID] 106 | if !ok { 107 | param = &Param{ 108 | Names: linkedhashset.New[string](), 109 | } 110 | res[message.ID] = param 111 | } 112 | 113 | // Process all forms: Other/One/Zero/Two/Few/Many 114 | // 处理所有形式:Other/One/Zero/Two/Few/Many 115 | messageTemplates := []string{ 116 | message.Other, // Process default translation first // 优先处理默认翻译 117 | message.One, 118 | message.Zero, 119 | message.Two, 120 | message.Few, 121 | message.Many, 122 | } 123 | 124 | // Extract params from each template form 125 | // 从每个模板形式提取参数 126 | for _, msgTemplate := range messageTemplates { 127 | names := parseParamNames(msgTemplate, pattern) 128 | for _, name := range names { 129 | if name == "" { 130 | // Anonymous param {{ . }} - must be the one param, no others allowed 131 | // 匿名参数 {{ . }} - 必须是唯一参数,不允许其他参数 132 | mustslice.Length(names, 1) 133 | must.True(param.Names.Empty()) 134 | param.HasOneAnonymous = true 135 | } else { 136 | // Named param {{ .name }} - cannot mix with anonymous param 137 | // 命名参数 {{ .name }} - 不能与匿名参数混合 138 | must.FALSE(param.HasOneAnonymous) 139 | param.Names.Add(name) 140 | } 141 | } 142 | } 143 | 144 | // Detect if message needs count for one/other forms 145 | // Count >= 2 means at least two forms defined (e.g., Other + One) 146 | // 147 | // 检测消息是否需要 one/other 形式的计数 148 | // 计数 >= 2 表示至少定义了两种形式(例如 Other + One) 149 | var count = 0 150 | for _, msgTemplate := range messageTemplates { 151 | if msgTemplate != "" { 152 | count++ 153 | } 154 | } 155 | if count >= 2 { 156 | param.NeedPluralCount = true 157 | } 158 | } 159 | } 160 | return res 161 | } 162 | 163 | // parseParamNames extracts param names from message template using regex 164 | // Matches {{ .paramName }} patterns and returns param names 165 | // Returns empty slice for blank messages 166 | // 167 | // parseParamNames 使用正则从消息模板提取参数名 168 | // 匹配 {{ .参数名 }} 模式并返回参数名 169 | // 空消息返回空切片 170 | func parseParamNames(msg string, pattern *regexp.Regexp) []string { 171 | var results = make([]string, 0) 172 | if msg != "" { 173 | // Extract all {{ .param }} matches from template 174 | // 从模板提取所有 {{ .param }} 匹配 175 | subs := pattern.FindAllStringSubmatch(msg, -1) 176 | zaplog.LOG.Debug("parse-param-names", zap.String("msg", msg), zap.Any("subs", subs)) 177 | for _, sub := range subs { 178 | mustslice.Length(sub, 2) // sub[0] is full match, sub[1] is param name // sub[0] 是完整匹配,sub[1] 是参数名 179 | results = append(results, sub[1]) 180 | } 181 | } 182 | zaplog.LOG.Debug("parse-param-names", zap.String("msg", msg), zap.Any("results", results)) 183 | return results 184 | } 185 | 186 | // SortMessageParams converts param map to sorted slice of MessageParam 187 | // Sorts messages alphabetically by ID for stable code generation 188 | // Uses sortx to maintain consistent output across runs 189 | // 190 | // SortMessageParams 将参数映射转换为排序的 MessageParam 切片 191 | // 按 ID 字母顺序排序消息以保证稳定的代码生成 192 | // 使用 sortx 保持多次运行输出一致 193 | func SortMessageParams(res map[string]*Param) []*MessageParam { 194 | var messageParams = make([]*MessageParam, 0, len(res)) 195 | for messageID, param := range res { 196 | messageParams = append(messageParams, &MessageParam{ 197 | MessageID: messageID, 198 | Param: param, 199 | }) 200 | } 201 | sortx.SortByValue(messageParams, func(a, b *MessageParam) bool { 202 | return a.MessageID < b.MessageID 203 | }) 204 | return messageParams 205 | } 206 | 207 | // MessageParam combines message ID with param metadata 208 | // Used in sorted slice for stable code generation 209 | // 210 | // MessageParam 结合消息 ID 和参数元数据 211 | // 用于排序切片以保证稳定的代码生成 212 | type MessageParam struct { 213 | MessageID string 214 | Param *Param 215 | } 216 | 217 | // CreateMessageFunctions generates Go code for message functions 218 | // Creates type-safe structs and functions for each message 219 | // Handles named params, anonymous params, and messages without params 220 | // 221 | // CreateMessageFunctions 为消息函数生成 Go 代码 222 | // 为每个消息创建类型安全的结构体和函数 223 | // 处理命名参数、匿名参数和无参数消息 224 | func CreateMessageFunctions(messageParams []*MessageParam, options *Options) []byte { 225 | ptx := printgo.NewPTX() 226 | for _, messageParam := range messageParams { 227 | writeNewMsgFunction(ptx, messageParam, options) 228 | ptx.Println() 229 | } 230 | return ptx.Bytes() 231 | } 232 | 233 | // writeNewMsgFunction generates code for single message 234 | // Handles three cases: anonymous param, named params, or no params 235 | // Generates struct definitions and I18n* functions for each message 236 | // 237 | // writeNewMsgFunction 为单个消息生成代码 238 | // 处理三种情况:匿名参数、命名参数或无参数 239 | // 为每个消息生成结构体定义和 I18n* 函数 240 | func writeNewMsgFunction(ptx *printgo.PTX, messageParam *MessageParam, options *Options) { 241 | // Convert message ID to function name (PascalCase or custom naming) 242 | // 将消息 ID 转换为函数名(驼峰命名或自定义命名) 243 | var messageName string 244 | if options.allowNonAsciiRune && utils.HasNonASCII(messageParam.MessageID) { 245 | messageName = options.unicodeMessageName(messageParam.MessageID) 246 | } else { 247 | messageName = strcase.ToCamel(messageParam.MessageID) 248 | } 249 | must.Nice(messageName) 250 | 251 | // Case 1: Anonymous param {{ . }} - generates generic function 252 | // 情况1:匿名参数 {{ . }} - 生成泛型函数 253 | if messageParam.Param.HasOneAnonymous { 254 | if options.generateNewMessage { 255 | ptx.Println("func New"+messageName+"[Value comparable](value Value)", "(string, Value) {") 256 | ptx.Println("\treturn", `"`+messageParam.MessageID+`"`, ",", "value") 257 | ptx.Println("}") 258 | ptx.Println() 259 | } 260 | 261 | ptx.Print("func I18n" + messageName + "[Value comparable](value Value,") 262 | if messageParam.Param.NeedPluralCount { 263 | ptx.Print("pluralConfig *goi18n.PluralConfig,") 264 | } 265 | ptx.Println(")", "*i18n.LocalizeConfig {") 266 | 267 | if options.generateNewMessage { 268 | ptx.Println("messageID, tempValue := New" + messageName + "(value)") 269 | } else { 270 | ptx.Println("const messageID =", `"`+messageParam.MessageID+`"`) 271 | ptx.Println("var tempValue = value") 272 | } 273 | 274 | ptx.Println("\treturn &i18n.LocalizeConfig{") 275 | ptx.Println("\t\tMessageID: messageID", ",") 276 | ptx.Println("\t\tTemplateData: tempValue", ",") 277 | if messageParam.Param.NeedPluralCount { 278 | ptx.Println("\t\tPluralCount: pluralConfig.PluralCount,") 279 | } 280 | ptx.Println("\t}") 281 | ptx.Println("}") 282 | } else if !messageParam.Param.Names.Empty() { 283 | // Case 2: Named params {{ .name }} {{ .code }} - generates struct and methods 284 | // 情况2:命名参数 {{ .name }} {{ .code }} - 生成结构体和方法 285 | var structName string 286 | if options.allowNonAsciiRune && utils.HasNonASCII(messageParam.MessageID) { 287 | structName = options.unicodeStructName(messageParam.MessageID) 288 | } else { 289 | structName = messageName + "Param" 290 | } 291 | must.Nice(structName) 292 | 293 | fieldNames := linkedhashmap.New[string, string]() 294 | for _, paramName := range messageParam.Param.Names.Values() { 295 | var fieldName string 296 | if options.allowNonAsciiRune && utils.HasNonASCII(paramName) { 297 | fieldName = options.unicodeFieldName(paramName) 298 | } else { 299 | fieldName = strcase.ToCamel(paramName) 300 | } 301 | must.Nice(fieldName) 302 | 303 | fieldNames.Put(paramName, fieldName) 304 | } 305 | methodName := "GetTemplateValues" 306 | 307 | ptx.Println("type", structName, "struct {") 308 | fieldNames.Each(func(key string, camelcase string) { 309 | ptx.Println("\t", camelcase, "any") 310 | }) 311 | ptx.Println("}") 312 | ptx.Println() 313 | 314 | ptx.Println("func (p *", structName, ") ", methodName, "() map[string]any {") 315 | ptx.Println("\tres := make(map[string]any)") 316 | fieldNames.Each(func(key string, camelcase string) { 317 | ptx.Println("\tif p.", camelcase, " != nil {") 318 | ptx.Println("\t\tres[", `"`+key+`"`, "] = p.", camelcase) 319 | ptx.Println("\t}") 320 | }) 321 | ptx.Println("\treturn res") 322 | ptx.Println("}") 323 | ptx.Println() 324 | 325 | if options.generateNewMessage { 326 | ptx.Println("func New"+messageName+"(data *", structName, ")", "(string, map[string]any) {") 327 | ptx.Println("\treturn", `"`+messageParam.MessageID+`"`, ",", "data.", methodName, "()") 328 | ptx.Println("}") 329 | ptx.Println() 330 | } 331 | 332 | ptx.Print("func I18n"+messageName+"(data *", structName, ",") 333 | if messageParam.Param.NeedPluralCount { 334 | ptx.Print("pluralConfig *goi18n.PluralConfig,") 335 | } 336 | ptx.Println(")", "*i18n.LocalizeConfig {") 337 | 338 | if options.generateNewMessage { 339 | ptx.Println("messageID, valuesMap := New" + messageName + "(data)") 340 | } else { 341 | ptx.Println("const messageID =", `"`+messageParam.MessageID+`"`) 342 | ptx.Println("var valuesMap = data.", methodName, "()") 343 | } 344 | 345 | ptx.Println("\treturn &i18n.LocalizeConfig{") 346 | ptx.Println("\t\tMessageID: messageID", ",") 347 | ptx.Println("\t\tTemplateData: valuesMap", ",") 348 | if messageParam.Param.NeedPluralCount { 349 | ptx.Println("\t\tPluralCount: pluralConfig.PluralCount,") 350 | } 351 | ptx.Println("\t}") 352 | ptx.Println("}") 353 | } else { 354 | // Case 3: No params - generates simple function returning config 355 | // 情况3:无参数 - 生成简单函数返回配置 356 | if options.generateNewMessage { 357 | ptx.Println("func New"+messageName+"()", "string {") 358 | ptx.Println("\treturn", `"`+messageParam.MessageID+`"`) 359 | ptx.Println("}") 360 | ptx.Println() 361 | } 362 | 363 | ptx.Print("func I18n" + messageName + "(") 364 | if messageParam.Param.NeedPluralCount { 365 | ptx.Print("pluralConfig *goi18n.PluralConfig,") 366 | } 367 | ptx.Println(")", "*i18n.LocalizeConfig {") 368 | 369 | if options.generateNewMessage { 370 | ptx.Println("messageID := New" + messageName + "()") 371 | } else { 372 | ptx.Println("const messageID =", `"`+messageParam.MessageID+`"`) 373 | } 374 | 375 | ptx.Println("\treturn &i18n.LocalizeConfig{") 376 | ptx.Println("\t\tMessageID: messageID", ",") 377 | if messageParam.Param.NeedPluralCount { 378 | ptx.Println("\t\tPluralCount: pluralConfig.PluralCount,") 379 | } 380 | ptx.Println("\t}") 381 | ptx.Println("}") 382 | } 383 | } 384 | 385 | // WriteContentToCodeFile writes generated code to output file 386 | // Adds package declaration, injects imports, and formats code 387 | // Uses syntaxgo_ast to auto inject needed imports 388 | // Uses formatgo to format the generated code 389 | // 390 | // WriteContentToCodeFile 将生成的代码写入输出文件 391 | // 添加包声明,注入导入,格式化代码 392 | // 使用 syntaxgo_ast 自动注入所需导入 393 | // 使用 formatgo 格式化生成的代码 394 | func WriteContentToCodeFile(srcCode []byte, options *Options) { 395 | ptx := printgo.NewPTX() 396 | ptx.Println("package", must.Nice(options.pkgName)) 397 | ptx.Println() 398 | ptx.Write(srcCode) 399 | ptx.Println() 400 | 401 | srcCode = ptx.Bytes() 402 | zaplog.SUG.Debugln(string(srcCode)) 403 | 404 | //把要引用的包写到代码的 import 里面(这样能提高format的速度,否则 format 还得找包,就会很慢,而且也未必能找到引用,因此这里主动设置引用) 405 | importOptions := syntaxgo_ast.NewPackageImportOptions() 406 | importOptions.SetInferredObject(&i18n.Message{}) 407 | importOptions.SetInferredObject(&MessageParam{}) 408 | srcCode = importOptions.InjectImports(srcCode) 409 | 410 | //调整代码格式和风格。注意:这里即使出错也能返回原来的代码,而且内部已有 warn 级别的日志,因此直接忽略错误 411 | srcCode, _ = formatgo.FormatBytes(srcCode) 412 | //前面不判断是否有错,只需要判断结果有没有内容,格式化出错也不影响结果 413 | must.Have(srcCode) 414 | 415 | path := must.Nice(options.outputPath) 416 | 417 | // when file exist WriteFile truncates it before writing, without changing permissions. 418 | must.Done(os.WriteFile(path, srcCode, 0666)) 419 | } 420 | --------------------------------------------------------------------------------