├── .gitignore ├── LICENSE ├── README.md ├── easyi18n ├── catalog │ └── catalog.go ├── locales │ ├── en.json │ ├── zh-Hans.json │ └── zh-Hant.json └── main.go ├── example ├── catalog │ └── catalog.go ├── example ├── locales │ ├── en.json │ └── zh-Hans.json └── main.go ├── go.mod ├── go.sum └── i18n ├── extract.go ├── generate.go ├── i18n.go ├── printer.go └── update.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Lukin 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Easy-i18n 2 | 3 | Easy-i18n is a Go package and a command that helps you translate Go programs into multiple languages. 4 | 5 | - Supports pluralized strings with =x or >x expression. 6 | - Supports strings with similar to [fmt.Sprintf](https://golang.org/pkg/fmt/) format syntax. 7 | - Supports message files of any format (e.g. JSON, TOML, YAML). 8 | 9 | # Package i18n 10 | 11 | The i18n package provides support for looking up messages according to a set of locale preferences. 12 | 13 | ```go 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "os" 19 | 20 | "golang.org/x/text/language" 21 | 22 | _ "github.com/mylukin/easy-i18n/example/catalog" 23 | "github.com/mylukin/easy-i18n/i18n" 24 | ) 25 | 26 | func main() { 27 | 28 | p := i18n.NewPrinter(language.SimplifiedChinese) 29 | p.Printf(`hello world.`) 30 | fmt.Println() 31 | 32 | i18n.SetLang(language.SimplifiedChinese) 33 | 34 | i18n.Printf(`hello world!`) 35 | fmt.Println() 36 | 37 | i18n.Printf(`hello world!`, i18n.Domain{`example`}) 38 | fmt.Println() 39 | 40 | name := `Lukin` 41 | 42 | i18n.Printf(`hello %s!`, name) 43 | fmt.Println() 44 | 45 | i18n.Printf(`%s has %d cat.`, name, 1) 46 | fmt.Println() 47 | 48 | i18n.Printf(`%s has %d cat.`, name, 2, i18n.Plural( 49 | `%[2]d=1`, `%s has %d cat.`, 50 | `%[2]d>1`, `%s has %d cats.`, 51 | )) 52 | fmt.Println() 53 | 54 | i18n.Fprintf(os.Stderr, `%s have %d apple.`, name, 2, i18n.Plural( 55 | `%[2]d=1`, `%s have an apple.`, 56 | `%[2]d=2`, `%s have two apples.`, 57 | `%[2]d>2`, `%s have %d apples.`, 58 | )) 59 | fmt.Println() 60 | 61 | } 62 | ``` 63 | 64 | # Command easyi18n 65 | 66 | The easyi18n command manages message files used by the i18n package. 67 | 68 | ``` 69 | go get -u github.com/mylukin/easy-i18n/easyi18n 70 | easyi18n -h 71 | 72 | update, u merge translations and generate catalog 73 | extract, e extracts strings to be translated from code 74 | generate, g generates code to insert translated messages 75 | ``` 76 | 77 | ### Extracting messages 78 | 79 | Use `easyi18n extract . ./locales/en.json` to extract all i18n.Sprintf function literals in Go source files to a message file for translation. 80 | 81 | `./locales/en.json` 82 | 83 | ```json 84 | { 85 | "%s has %d cat.": "%s has %d cat.", 86 | "%s has %d cats.": "%s has %d cats.", 87 | "%s have %d apples.": "%s have %d apples.", 88 | "%s have an apple.": "%s have an apple.", 89 | "%s have two apples.": "%s have two apples.", 90 | "hello %s!": "hello %s!", 91 | "hello world!": "hello world!" 92 | } 93 | ``` 94 | 95 | ### Translating a new language 96 | 97 | 1. Create an empty message file for the language that you want to add (e.g. `zh-Hans.json`). 98 | 2. Run `easyi18n update ./locales/en.json ./locales/zh-Hans.json` to populate `zh-Hans.json` with the mesages to be translated. 99 | 100 | `./locales/zh-Hans.json` 101 | 102 | ```json 103 | { 104 | "%s has %d cat.": "%s有%d只猫。", 105 | "%s has %d cats.": "%s有%d只猫。", 106 | "%s have %d apples.": "%s有%d个苹果。", 107 | "%s have an apple.": "%s有一个苹果。", 108 | "%s have two apples.": "%s有两个苹果。", 109 | "hello %s!": "你好%s!", 110 | "hello world!": "你好世界!" 111 | } 112 | ``` 113 | 114 | 3. After `zh-Hans.json` has been translated, run `easyi18n generate --pkg=catalog ./locales ./catalog/catalog.go`. 115 | 116 | 4. Import `catalog` package in main.go, example: `import _ "github.com/mylukin/easy-i18n/example/catalog"` 117 | 118 | ### Translating new messages 119 | 120 | If you have added new messages to your program: 121 | 122 | 1. Run `easyi18n extract` to update `./locales/en.json` with the new messages. 123 | 2. Run `easyi18n update ./locales/en.json` to generate updated `./locales/new-language.json` files. 124 | 3. Translate all the messages in the `./locales/new-language.json` files. 125 | 4. Run `easyi18n generate --pkg=catalog ./locales ./catalog/catalog.go` to merge the translated messages into the go files. 126 | 127 | ## For examples 128 | 129 | - Look at an example [application](https://github.com/mylukin/easy-i18n/tree/master/example). 130 | 131 | ## Thanks 132 | 133 | - 134 | - 135 | - 136 | - 137 | 138 | ## License 139 | 140 | Easy-i18n is available under the MIT license. See the [LICENSE](LICENSE) file for more info. 141 | -------------------------------------------------------------------------------- /easyi18n/catalog/catalog.go: -------------------------------------------------------------------------------- 1 | package catalog 2 | 3 | import ( 4 | "golang.org/x/text/language" 5 | "golang.org/x/text/message" 6 | ) 7 | 8 | // init 9 | func init() { 10 | initEn(language.Make("en")) 11 | initZhHans(language.Make("zh-Hans")) 12 | initZhHant(language.Make("zh-Hant")) 13 | } 14 | // initEn will init en support. 15 | func initEn(tag language.Tag) { 16 | message.SetString(tag, "%s extract [path] [outfile]", "%s extract [path] [outfile]") 17 | message.SetString(tag, "%s generate [path] [outfile]", "%s generate [path] [outfile]") 18 | message.SetString(tag, "%s update srcfile destfile", "%s update srcfile destfile") 19 | message.SetString(tag, "a tool for managing message translations.", "a tool for managing message translations.") 20 | message.SetString(tag, "destfile cannot be empty", "destfile cannot be empty") 21 | message.SetString(tag, "extracts strings to be translated from code", "extracts strings to be translated from code") 22 | message.SetString(tag, "generated go file package name", "generated go file package name") 23 | message.SetString(tag, "generates code to insert translated messages", "generates code to insert translated messages") 24 | message.SetString(tag, "merge translations and generate catalog", "merge translations and generate catalog") 25 | message.SetString(tag, "package name", "package name") 26 | message.SetString(tag, "srcfile cannot be empty", "srcfile cannot be empty") 27 | } 28 | // initZhHans will init zh-Hans support. 29 | func initZhHans(tag language.Tag) { 30 | message.SetString(tag, "%s extract [path] [outfile]", "%s extract [path] [outfile]") 31 | message.SetString(tag, "%s generate [path] [outfile]", "%s generate [path] [outfile]") 32 | message.SetString(tag, "%s update srcfile destfile", "%s update srcfile destfile") 33 | message.SetString(tag, "a tool for managing message translations.", "a tool for managing message translations.") 34 | message.SetString(tag, "destfile cannot be empty", "destfile cannot be empty") 35 | message.SetString(tag, "extracts strings to be translated from code", "extracts strings to be translated from code") 36 | message.SetString(tag, "generated go file package name", "generated go file package name") 37 | message.SetString(tag, "generates code to insert translated messages", "generates code to insert translated messages") 38 | message.SetString(tag, "merge translations and generate catalog", "merge translations and generate catalog") 39 | message.SetString(tag, "package name", "package name") 40 | message.SetString(tag, "srcfile cannot be empty", "srcfile cannot be empty") 41 | } 42 | // initZhHant will init zh-Hant support. 43 | func initZhHant(tag language.Tag) { 44 | message.SetString(tag, "%s extract [path] [outfile]", "%s 提取 [路徑] [輸出文件]") 45 | message.SetString(tag, "%s generate [path] [outfile]", "%s 生成 [路徑] [輸出文件]") 46 | message.SetString(tag, "%s update srcfile destfile", "%s 更新 源文件 輸出文件") 47 | message.SetString(tag, "a tool for managing message translations.", "用於管理消息翻譯的工具。") 48 | message.SetString(tag, "destfile cannot be empty", "輸出文件不能為空") 49 | message.SetString(tag, "extracts strings to be translated from code", "從代碼中提取要翻譯的字符串") 50 | message.SetString(tag, "generated go file package name", "生成的go文件包名稱") 51 | message.SetString(tag, "generates code to insert translated messages", "生成代碼以插入翻譯後的消息") 52 | message.SetString(tag, "merge translations and generate catalog", "合併翻譯並生成目錄") 53 | message.SetString(tag, "package name", "package name") 54 | message.SetString(tag, "srcfile cannot be empty", "源文件不能為空") 55 | } 56 | -------------------------------------------------------------------------------- /easyi18n/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "%s extract [path] [outfile]": "%s extract [path] [outfile]", 3 | "%s generate [path] [outfile]": "%s generate [path] [outfile]", 4 | "%s update srcfile destfile": "%s update srcfile destfile", 5 | "a tool for managing message translations.": "a tool for managing message translations.", 6 | "destfile cannot be empty": "destfile cannot be empty", 7 | "extracts strings to be translated from code": "extracts strings to be translated from code", 8 | "generated go file package name": "generated go file package name", 9 | "generates code to insert translated messages": "generates code to insert translated messages", 10 | "merge translations and generate catalog": "merge translations and generate catalog", 11 | "package name": "package name", 12 | "srcfile cannot be empty": "srcfile cannot be empty" 13 | } 14 | -------------------------------------------------------------------------------- /easyi18n/locales/zh-Hans.json: -------------------------------------------------------------------------------- 1 | { 2 | "%s extract [path] [outfile]": "%s extract [path] [outfile]", 3 | "%s generate [path] [outfile]": "%s generate [path] [outfile]", 4 | "%s update srcfile destfile": "%s update srcfile destfile", 5 | "a tool for managing message translations.": "a tool for managing message translations.", 6 | "destfile cannot be empty": "destfile cannot be empty", 7 | "extracts strings to be translated from code": "extracts strings to be translated from code", 8 | "generated go file package name": "generated go file package name", 9 | "generates code to insert translated messages": "generates code to insert translated messages", 10 | "merge translations and generate catalog": "merge translations and generate catalog", 11 | "package name": "package name", 12 | "srcfile cannot be empty": "srcfile cannot be empty" 13 | } 14 | -------------------------------------------------------------------------------- /easyi18n/locales/zh-Hant.json: -------------------------------------------------------------------------------- 1 | { 2 | "%s extract [path] [outfile]": "%s 提取 [路徑] [輸出文件]", 3 | "%s generate [path] [outfile]": "%s 生成 [路徑] [輸出文件]", 4 | "%s update srcfile destfile": "%s 更新 源文件 輸出文件", 5 | "a tool for managing message translations.": "用於管理消息翻譯的工具。", 6 | "destfile cannot be empty": "輸出文件不能為空", 7 | "extracts strings to be translated from code": "從代碼中提取要翻譯的字符串", 8 | "generated go file package name": "生成的go文件包名稱", 9 | "generates code to insert translated messages": "生成代碼以插入翻譯後的消息", 10 | "merge translations and generate catalog": "合併翻譯並生成目錄", 11 | "package name": "package name", 12 | "srcfile cannot be empty": "源文件不能為空" 13 | } 14 | -------------------------------------------------------------------------------- /easyi18n/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run main.go extract . ./locales/en.json 4 | //go:generate go run main.go update ./locales/en.json ./locales/zh-Hans.json 5 | //go:generate go run main.go update ./locales/en.json ./locales/zh-Hant.json 6 | //go:generate go run main.go generate --pkg=catalog ./locales ./catalog/catalog.go 7 | 8 | import ( 9 | "fmt" 10 | "log" 11 | "os" 12 | 13 | "github.com/Xuanwo/go-locale" 14 | _ "github.com/mylukin/easy-i18n/easyi18n/catalog" 15 | "github.com/mylukin/easy-i18n/i18n" 16 | "github.com/urfave/cli/v2" 17 | ) 18 | 19 | func main() { 20 | // Detect OS language 21 | tag, _ := locale.Detect() 22 | 23 | // Set Language 24 | i18n.SetLang(tag) 25 | 26 | appName := "easyi18n" 27 | 28 | app := &cli.App{ 29 | HelpName: appName, 30 | Name: appName, 31 | Usage: i18n.Sprintf(`a tool for managing message translations.`), 32 | Action: func(c *cli.Context) error { 33 | cli.ShowAppHelp(c) 34 | return nil 35 | }, 36 | 37 | Commands: []*cli.Command{ 38 | { 39 | Name: "update", 40 | Aliases: []string{"u"}, 41 | Usage: i18n.Sprintf(`merge translations and generate catalog`), 42 | UsageText: i18n.Sprintf(`%s update srcfile destfile`, appName), 43 | Flags: []cli.Flag{ 44 | &cli.BoolFlag{ 45 | Name: "flush", 46 | Aliases: []string{"f"}, 47 | Value: false, 48 | Usage: fmt.Sprintf(`flush messages`), 49 | }, 50 | }, 51 | Action: func(c *cli.Context) error { 52 | srcFile := c.Args().Get(0) 53 | if len(srcFile) == 0 { 54 | return fmt.Errorf(i18n.Sprintf(`srcfile cannot be empty`)) 55 | } 56 | 57 | destFile := c.Args().Get(1) 58 | if len(destFile) == 0 { 59 | return fmt.Errorf(i18n.Sprintf(`destfile cannot be empty`)) 60 | } 61 | 62 | flush := c.Bool("flush") 63 | err := i18n.Update(srcFile, destFile, flush) 64 | 65 | return err 66 | }, 67 | }, 68 | { 69 | Name: "extract", 70 | Aliases: []string{"e"}, 71 | Usage: i18n.Sprintf(`extracts strings to be translated from code`), 72 | UsageText: i18n.Sprintf(`%s extract [path] [outfile]`, appName), 73 | Flags: []cli.Flag{ 74 | &cli.StringFlag{ 75 | Name: "pkg", 76 | Value: "i18n", 77 | Usage: i18n.Sprintf(`package name`), 78 | }, 79 | }, 80 | Action: func(c *cli.Context) error { 81 | path := c.Args().Get(0) 82 | if len(path) == 0 { 83 | path = "." 84 | } 85 | outFile := c.Args().Get(1) 86 | if len(outFile) == 0 { 87 | outFile = "./locales/en.json" 88 | } 89 | pkgName := c.String("pkg") 90 | err := i18n.Extract(pkgName, []string{ 91 | path, 92 | }, outFile) 93 | return err 94 | }, 95 | }, 96 | { 97 | Name: "generate", 98 | Aliases: []string{"g"}, 99 | Usage: i18n.Sprintf(`generates code to insert translated messages`), 100 | UsageText: i18n.Sprintf(`%s generate [path] [outfile]`, appName), 101 | Flags: []cli.Flag{ 102 | &cli.StringFlag{ 103 | Name: "pkg", 104 | Value: "catalog", 105 | Usage: i18n.Sprintf(`generated go file package name`), 106 | }, 107 | }, 108 | Action: func(c *cli.Context) error { 109 | path := c.Args().Get(0) 110 | if len(path) == 0 { 111 | path = "./locales" 112 | } 113 | outFile := c.Args().Get(1) 114 | if len(outFile) == 0 { 115 | outFile = "./catalog/catalog.go" 116 | } 117 | pkgName := c.String("pkg") 118 | err := i18n.Generate( 119 | pkgName, 120 | []string{ 121 | path, 122 | }, outFile) 123 | return err 124 | }, 125 | }, 126 | }, 127 | } 128 | 129 | err := app.Run(os.Args) 130 | if err != nil { 131 | log.Fatal(err) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /example/catalog/catalog.go: -------------------------------------------------------------------------------- 1 | package catalog 2 | 3 | import ( 4 | "golang.org/x/text/language" 5 | "golang.org/x/text/message" 6 | ) 7 | 8 | // init 9 | func init() { 10 | initEn(language.Make("en")) 11 | initZhHans(language.Make("zh-Hans")) 12 | } 13 | // initEn will init en support. 14 | func initEn(tag language.Tag) { 15 | message.SetString(tag, "%s has %d cat.", "%s has %d cat.") 16 | message.SetString(tag, "%s has %d cats.", "%s has %d cats.") 17 | message.SetString(tag, "%s have %d apple.", "%s have %d apple.") 18 | message.SetString(tag, "%s have %d apples.", "%s have %d apples.") 19 | message.SetString(tag, "%s have an apple.", "%s have an apple.") 20 | message.SetString(tag, "%s have two apples.", "%s have two apples.") 21 | message.SetString(tag, "example.hello world!", "hello world!") 22 | message.SetString(tag, "hello %s!", "hello %s!") 23 | message.SetString(tag, "hello world!", "hello world!") 24 | message.SetString(tag, "hello world.", "hello world.") 25 | } 26 | // initZhHans will init zh-Hans support. 27 | func initZhHans(tag language.Tag) { 28 | message.SetString(tag, "%s has %d cat.", "%s有%d只猫。") 29 | message.SetString(tag, "%s has %d cats.", "%s有%d只猫。") 30 | message.SetString(tag, "%s have %d apple.", "%s have %d apple.") 31 | message.SetString(tag, "%s have %d apples.", "%s有%d个苹果。") 32 | message.SetString(tag, "%s have an apple.", "%s有一个苹果。") 33 | message.SetString(tag, "%s have two apples.", "%s有两个苹果。") 34 | message.SetString(tag, "example.hello world!", "举个例子:你好,我的世界!") 35 | message.SetString(tag, "hello %s!", "你好,%s!") 36 | message.SetString(tag, "hello world!", "你好,世界!") 37 | message.SetString(tag, "hello world.", "你好,世界。") 38 | } 39 | -------------------------------------------------------------------------------- /example/example: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mylukin/easy-i18n/77e476a0fe2285e76a4410aa7765fe0a4d731de1/example/example -------------------------------------------------------------------------------- /example/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "%s has %d cat.": "%s has %d cat.", 3 | "%s has %d cats.": "%s has %d cats.", 4 | "%s have %d apple.": "%s have %d apple.", 5 | "%s have %d apples.": "%s have %d apples.", 6 | "%s have an apple.": "%s have an apple.", 7 | "%s have two apples.": "%s have two apples.", 8 | "example.hello world!": "hello world!", 9 | "hello %s!": "hello %s!", 10 | "hello world!": "hello world!", 11 | "hello world.": "hello world." 12 | } 13 | -------------------------------------------------------------------------------- /example/locales/zh-Hans.json: -------------------------------------------------------------------------------- 1 | { 2 | "%s has %d cat.": "%s有%d只猫。", 3 | "%s has %d cats.": "%s有%d只猫。", 4 | "%s have %d apple.": "%s have %d apple.", 5 | "%s have %d apples.": "%s有%d个苹果。", 6 | "%s have an apple.": "%s有一个苹果。", 7 | "%s have two apples.": "%s有两个苹果。", 8 | "example.hello world!": "举个例子:你好,我的世界!", 9 | "hello %s!": "你好,%s!", 10 | "hello world!": "你好,世界!", 11 | "hello world.": "你好,世界。" 12 | } 13 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate easyi18n extract . ./locales/en.json 4 | //go:generate easyi18n update ./locales/en.json ./locales/zh-Hans.json 5 | //go:generate easyi18n generate --pkg=catalog ./locales ./catalog/catalog.go 6 | //go:generate go build -o example 7 | 8 | import ( 9 | "fmt" 10 | "os" 11 | 12 | "golang.org/x/text/language" 13 | 14 | _ "github.com/mylukin/easy-i18n/example/catalog" 15 | "github.com/mylukin/easy-i18n/i18n" 16 | ) 17 | 18 | func main() { 19 | 20 | p := i18n.NewPrinter(language.SimplifiedChinese) 21 | p.Printf(`hello world.`) 22 | fmt.Println() 23 | 24 | i18n.SetLang(language.SimplifiedChinese) 25 | 26 | i18n.Printf(`hello world!`) 27 | fmt.Println() 28 | 29 | i18n.Printf(`hello world!`, i18n.Domain{`example`}) 30 | fmt.Println() 31 | 32 | name := `Lukin` 33 | 34 | i18n.Printf(`hello %s!`, name) 35 | fmt.Println() 36 | 37 | i18n.Printf(`%s has %d cat.`, name, 1) 38 | fmt.Println() 39 | 40 | i18n.Printf(`%s has %d cat.`, name, 2, i18n.Plural( 41 | `%[2]d=1`, `%s has %d cat.`, 42 | `%[2]d>1`, `%s has %d cats.`, 43 | )) 44 | fmt.Println() 45 | 46 | i18n.Fprintf(os.Stderr, `%s have %d apple.`, name, 2, i18n.Plural( 47 | `%[2]d=1`, `%s have an apple.`, 48 | `%[2]d=2`, `%s have two apples.`, 49 | `%[2]d>2`, `%s have %d apples.`, 50 | )) 51 | fmt.Println() 52 | 53 | } 54 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mylukin/easy-i18n 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/BurntSushi/toml v1.3.2 7 | github.com/Xuanwo/go-locale v1.1.0 8 | github.com/urfave/cli/v2 v2.27.1 9 | golang.org/x/text v0.14.0 10 | gopkg.in/yaml.v2 v2.4.0 11 | ) 12 | 13 | require ( 14 | github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect 15 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 16 | github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect 17 | golang.org/x/sys v0.16.0 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= 2 | github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 3 | github.com/Xuanwo/go-locale v1.1.0 h1:51gUxhxl66oXAjI9uPGb2O0qwPECpriKQb2hl35mQkg= 4 | github.com/Xuanwo/go-locale v1.1.0/go.mod h1:UKrHoZB3FPIk9wIG2/tVSobnHgNnceGSH3Y8DY5cASs= 5 | github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= 6 | github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 7 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 10 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 11 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 12 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 13 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 15 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 16 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 17 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 18 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 19 | github.com/smartystreets/goconvey v1.6.7 h1:I6tZjLXD2Q1kjvNbIzB1wvQBsXmKXiVrhpRE8ZjP5jY= 20 | github.com/smartystreets/goconvey v1.6.7/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 21 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 22 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 23 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 24 | github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= 25 | github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= 26 | github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI= 27 | github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 28 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 29 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 30 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 31 | golang.org/x/sys v0.0.0-20211023085530-d6a326fbbf70/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 32 | golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= 33 | golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 34 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 35 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 36 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 37 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 38 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 39 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 40 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 41 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 42 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 43 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 44 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 45 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 46 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 47 | -------------------------------------------------------------------------------- /i18n/extract.go: -------------------------------------------------------------------------------- 1 | package i18n 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/parser" 7 | "go/token" 8 | "io/ioutil" 9 | "os" 10 | "path" 11 | "path/filepath" 12 | "strings" 13 | ) 14 | 15 | // Extract messages 16 | func Extract(packName string, paths []string, outFile string) error { 17 | if len(paths) == 0 { 18 | paths = []string{"."} 19 | } 20 | messages := map[string]string{} 21 | for _, path := range paths { 22 | if err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error { 23 | if err != nil { 24 | return err 25 | } 26 | if info.IsDir() { 27 | return nil 28 | } 29 | // ignore easy-i18n 30 | if strings.Index(path, "github.com/mylukin/easy-i18n") > -1 { 31 | return nil 32 | } 33 | if filepath.Ext(path) != ".go" { 34 | return nil 35 | } 36 | 37 | // Don't extract from test files. 38 | if strings.HasSuffix(path, "_test.go") { 39 | return nil 40 | } 41 | buf, err := ioutil.ReadFile(path) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | fset := token.NewFileSet() 47 | file, err := parser.ParseFile(fset, path, buf, parser.AllErrors) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | // fmt.Printf("Extract %+v ...\n", path) 53 | i18NPackName := i18nPackageName(file) 54 | if i18NPackName == "" { 55 | i18NPackName = packName 56 | } 57 | 58 | //ast.Print(fset, file) 59 | ast.Inspect(file, func(n ast.Node) bool { 60 | switch v := n.(type) { 61 | case *ast.CallExpr: 62 | if fn, ok := v.Fun.(*ast.SelectorExpr); ok { 63 | var packName string 64 | if pack, ok := fn.X.(*ast.Ident); ok { 65 | if pack.Obj != nil { 66 | if as, ok := pack.Obj.Decl.(*ast.AssignStmt); ok { 67 | if vv, ok := as.Rhs[0].(*ast.CallExpr); ok { 68 | if vfn, ok := vv.Fun.(*ast.SelectorExpr); ok { 69 | if vpack, ok := vfn.X.(*ast.Ident); ok { 70 | packName = vpack.Name 71 | } 72 | } 73 | } 74 | } 75 | } else { 76 | packName = pack.Name 77 | } 78 | 79 | } 80 | funcName := fn.Sel.Name 81 | namePos := fset.Position(fn.Sel.NamePos) 82 | 83 | // Package name must be equal 84 | if len(packName) > 0 && i18NPackName == packName { 85 | // Function name must be equal 86 | if funcName == "Printf" || funcName == "Sprintf" || funcName == "Fprintf" { 87 | id := "" 88 | domain := "" 89 | // get domain 90 | for _, expr := range v.Args { 91 | if cv, ok := expr.(*ast.CompositeLit); ok { 92 | if cvt, ok := cv.Type.(*ast.SelectorExpr); ok { 93 | if pack, ok := cvt.X.(*ast.Ident); ok { 94 | if pack.Name == "i18n" && cvt.Sel.Name == "Domain" { 95 | // 读取 domain 96 | if dv, ok := cv.Elts[0].(*ast.BasicLit); ok { 97 | domain = strings.Trim(dv.Value, "`") 98 | domain = strings.Trim(domain, `"`) 99 | } else if kv, ok := cv.Elts[0].(*ast.KeyValueExpr); ok { 100 | if dv, ok := kv.Value.(*ast.BasicLit); ok { 101 | domain = strings.Trim(dv.Value, "`") 102 | domain = strings.Trim(domain, `"`) 103 | } 104 | } 105 | } 106 | } 107 | } 108 | } 109 | } 110 | var fn func(arg ast.Expr) string 111 | fn = func(arg ast.Expr) string { 112 | switch value := arg.(type) { 113 | case *ast.BasicLit: 114 | id = trim(value.Value) 115 | case *ast.Ident: 116 | if value.Obj.Kind == ast.Con { 117 | if spec, ok := value.Obj.Decl.(*ast.ValueSpec); ok { 118 | for _, v := range spec.Values { 119 | val := fn(v) 120 | if val != "" { 121 | return val 122 | } 123 | } 124 | } 125 | } 126 | } 127 | return "" 128 | } 129 | // Find the string to be translated 130 | for _, arg := range v.Args { 131 | val := fn(arg) 132 | if val != "" { 133 | id = val 134 | break 135 | } 136 | } 137 | 138 | if id != "" { 139 | value := id 140 | if domain != "" { 141 | id = fmt.Sprintf("%s.%s", domain, id) 142 | } 143 | if _, ok := messages[id]; !ok { 144 | messages[id] = value 145 | } 146 | fmt.Printf("Extract %+v %v.%v => %s\n", namePos, packName, funcName, id) 147 | } 148 | } 149 | if funcName == "Plural" { 150 | // Find the string to be translated 151 | for i := 0; i < len(v.Args); { 152 | if i++; i >= len(v.Args) { 153 | break 154 | } 155 | if str, ok := v.Args[i].(*ast.BasicLit); ok { 156 | id := trim(str.Value) 157 | if _, ok := messages[id]; !ok { 158 | messages[id] = id 159 | } 160 | fmt.Printf("Extract %+v %v.%v => %s\n", namePos, packName, funcName, id) 161 | } 162 | i++ 163 | } 164 | } 165 | 166 | } 167 | } 168 | } 169 | return true 170 | }) 171 | return nil 172 | }); err != nil { 173 | fmt.Printf("Extract error: %s\n", err) 174 | } 175 | } 176 | 177 | var content []byte 178 | var err error 179 | of := strings.ToLower(outFile) 180 | if strings.HasSuffix(of, ".json") { 181 | content, err = marshal(messages, "json") 182 | } 183 | if strings.HasSuffix(of, ".toml") { 184 | content, err = marshal(messages, "toml") 185 | } 186 | if strings.HasSuffix(of, ".yaml") { 187 | content, err = marshal(messages, "yaml") 188 | } 189 | if err != nil { 190 | return err 191 | } 192 | err = os.MkdirAll(path.Dir(outFile), os.ModePerm) 193 | if err != nil { 194 | return err 195 | } 196 | err = ioutil.WriteFile(outFile, content, os.ModePerm) 197 | if err != nil { 198 | return err 199 | } 200 | fmt.Printf("Extract to %v ...\n", outFile) 201 | return nil 202 | } 203 | 204 | func i18nPackageName(file *ast.File) string { 205 | for _, i := range file.Imports { 206 | if i.Path.Kind == token.STRING && i.Path.Value == `"github.com/mylukin/easy-i18n/i18n"` { 207 | if i.Name == nil { 208 | return "i18n" 209 | } 210 | return i.Name.Name 211 | } 212 | } 213 | return "" 214 | } 215 | 216 | func trim(text string) string { 217 | if len(text) > 2 && 218 | (text[0] == '"' && text[len(text)-1] == '"') || 219 | (text[0] == '`' && text[len(text)-1] == '`') { 220 | return text[1 : len(text)-1] 221 | } 222 | return text 223 | } 224 | 225 | -------------------------------------------------------------------------------- /i18n/generate.go: -------------------------------------------------------------------------------- 1 | package i18n 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "path" 8 | "path/filepath" 9 | "strconv" 10 | "strings" 11 | "text/template" 12 | ) 13 | 14 | // Generate catalog.go 15 | func Generate(pkgName string, paths []string, outFile string) error { 16 | if len(paths) == 0 { 17 | paths = []string{"."} 18 | } 19 | 20 | if err := os.MkdirAll(path.Dir(outFile), os.ModePerm); err != nil { 21 | return err 22 | } 23 | 24 | goFile, err := os.Create(outFile) 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | 29 | data := map[string]*Message{} 30 | for _, path := range paths { 31 | if err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error { 32 | if err != nil { 33 | return err 34 | } 35 | if info.IsDir() { 36 | return nil 37 | } 38 | 39 | messages, err := unmarshal(path) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | lang := info.Name()[0 : len(info.Name())-5] 45 | data[lang] = messages 46 | fmt.Printf("Generate %+v ...\n", path) 47 | 48 | return nil 49 | }); err != nil { 50 | return err 51 | } 52 | } 53 | 54 | err = i18nTmpl.Execute(goFile, struct { 55 | Data map[string]*Message 56 | BackQuote string 57 | Package string 58 | }{ 59 | data, 60 | "`", 61 | pkgName, 62 | }) 63 | 64 | return err 65 | } 66 | 67 | var funcs = template.FuncMap{ 68 | "funcName": func(lang string) string { 69 | lang = strings.ReplaceAll(lang, "_", "") 70 | lang = strings.ReplaceAll(lang, "-", "") 71 | lang = strings.ToUpper(lang[:1]) + lang[1:] 72 | return lang 73 | }, 74 | "quote": func(text string) string { 75 | return strconv.Quote(text) 76 | }, 77 | } 78 | 79 | var i18nTmpl = template.Must(template.New("i18n").Funcs(funcs).Parse(`package {{.Package}} 80 | 81 | import ( 82 | "golang.org/x/text/language" 83 | "golang.org/x/text/message" 84 | ) 85 | 86 | // init 87 | func init() { 88 | {{- range $k, $v := .Data }} 89 | init{{ funcName $k }}(language.Make("{{ $k }}")) 90 | {{- end }} 91 | } 92 | 93 | {{- range $k, $v := .Data }} 94 | // init{{ funcName $k }} will init {{ $k }} support. 95 | func init{{ funcName $k }}(tag language.Tag) { 96 | {{- range $k, $v := $v }} 97 | message.SetString(tag, {{quote $k}}, {{quote $v}}) 98 | {{- end }} 99 | } 100 | {{- end }} 101 | `)) 102 | -------------------------------------------------------------------------------- /i18n/i18n.go: -------------------------------------------------------------------------------- 1 | package i18n 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/BurntSushi/toml" 14 | "golang.org/x/text/language" 15 | "gopkg.in/yaml.v2" 16 | ) 17 | 18 | var p *Printer 19 | 20 | // init 21 | func init() { 22 | // default language english 23 | p = NewPrinter(language.English) 24 | } 25 | 26 | // SetLang set language 27 | func SetLang(lang interface{}) { 28 | p = NewPrinter(lang) 29 | } 30 | 31 | // GetPrinter 32 | func GetPrinter() *Printer { 33 | return p 34 | } 35 | 36 | // Printf is like fmt.Printf, but using language-specific formatting. 37 | func Printf(format string, args ...interface{}) { 38 | p.Printf(format, args...) 39 | } 40 | 41 | // Sprintf is like fmt.Sprintf, but using language-specific formatting. 42 | func Sprintf(format string, args ...interface{}) string { 43 | return p.Sprintf(format, args...) 44 | } 45 | 46 | // Fprintf is like fmt.Fprintf, but using language-specific formatting. 47 | func Fprintf(w io.Writer, key string, args ...interface{}) (n int, err error) { 48 | return p.Fprintf(w, key, args...) 49 | } 50 | 51 | func unmarshal(path string) (*Message, error) { 52 | result := &Message{} 53 | 54 | _, err := os.Stat(path) 55 | if err != nil { 56 | if !os.IsExist(err) { 57 | return result, nil 58 | } 59 | } 60 | 61 | fileExt := strings.ToLower(filepath.Ext(path)) 62 | if fileExt != ".toml" && fileExt != ".json" && fileExt != ".yaml" { 63 | return result, fmt.Errorf(Sprintf("File type not supported")) 64 | } 65 | 66 | buf, err := ioutil.ReadFile(path) 67 | if err != nil { 68 | return result, err 69 | } 70 | 71 | if strings.HasSuffix(fileExt, ".json") { 72 | err := json.Unmarshal(buf, result) 73 | if err != nil { 74 | return result, err 75 | } 76 | } 77 | 78 | if strings.HasSuffix(fileExt, ".yaml") { 79 | err := yaml.Unmarshal(buf, result) 80 | if err != nil { 81 | return result, err 82 | } 83 | } 84 | 85 | if strings.HasSuffix(fileExt, ".toml") { 86 | _, err := toml.Decode(string(buf), result) 87 | if err != nil { 88 | return result, err 89 | } 90 | } 91 | return result, nil 92 | 93 | } 94 | 95 | func marshal(v interface{}, format string) ([]byte, error) { 96 | switch format { 97 | case "json": 98 | buffer := &bytes.Buffer{} 99 | encoder := json.NewEncoder(buffer) 100 | encoder.SetEscapeHTML(false) 101 | encoder.SetIndent("", " ") 102 | err := encoder.Encode(v) 103 | return buffer.Bytes(), err 104 | case "toml": 105 | var buf bytes.Buffer 106 | enc := toml.NewEncoder(&buf) 107 | enc.Indent = "" 108 | err := enc.Encode(v) 109 | return buf.Bytes(), err 110 | case "yaml": 111 | return yaml.Marshal(v) 112 | } 113 | return nil, fmt.Errorf("unsupported format: %s", format) 114 | } 115 | -------------------------------------------------------------------------------- /i18n/printer.go: -------------------------------------------------------------------------------- 1 | package i18n 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | 10 | "golang.org/x/text/language" 11 | "golang.org/x/text/message" 12 | ) 13 | 14 | // Printer is printer 15 | type Printer struct { 16 | lang string 17 | pt *message.Printer 18 | } 19 | 20 | // PluralRule is Plural rule 21 | type PluralRule struct { 22 | Pos int 23 | Expr string 24 | Value int 25 | Text string 26 | } 27 | 28 | // Domain is Domain 29 | type Domain struct { 30 | K string 31 | } 32 | 33 | // Message is translation message 34 | type Message map[string]string 35 | 36 | // NewPrinter is new printer 37 | func NewPrinter(lang interface{}) *Printer { 38 | var langTag language.Tag 39 | switch _lang := lang.(type) { 40 | case language.Tag: 41 | langTag = _lang 42 | case string: 43 | langTag = language.Make(_lang) 44 | } 45 | return &Printer{ 46 | lang: langTag.String(), 47 | pt: message.NewPrinter(langTag), 48 | } 49 | } 50 | 51 | // Printf is like fmt.Printf, but using language-specific formatting. 52 | func (p *Printer) Printf(format string, args ...interface{}) { 53 | format, args = preArgs(format, args...) 54 | p.pt.Printf(format, args...) 55 | } 56 | 57 | // Sprintf is like fmt.Sprintf, but using language-specific formatting. 58 | func (p *Printer) Sprintf(format string, args ...interface{}) string { 59 | format, args = preArgs(format, args...) 60 | return p.pt.Sprintf(format, args...) 61 | } 62 | 63 | // Fprintf is like fmt.Fprintf, but using language-specific formatting. 64 | func (p *Printer) Fprintf(w io.Writer, key string, a ...interface{}) (n int, err error) { 65 | format, args := preArgs(key, a...) 66 | _key := message.Reference(format) 67 | return p.pt.Fprintf(w, _key, args...) 68 | } 69 | 70 | // String is lang 71 | func (p *Printer) String() string { 72 | return strings.ToLower(p.lang) 73 | } 74 | 75 | // Preprocessing parameters in plural form 76 | func preArgs(format string, args ...interface{}) (string, []interface{}) { 77 | length := len(args) 78 | if length > 0 { 79 | lastArg := args[length-1] 80 | switch v := lastArg.(type) { 81 | case []PluralRule: 82 | rules := v 83 | // parse rule 84 | for _, rule := range rules { 85 | curPosVal := args[rule.Pos-1].(int) 86 | // Support comparison expression 87 | if (rule.Expr == "=" && curPosVal == rule.Value) || (rule.Expr == ">" && curPosVal > rule.Value) { 88 | format = rule.Text 89 | break 90 | } 91 | } 92 | args = args[0:strings.Count(format, "%")] 93 | case Domain: 94 | format = fmt.Sprintf("%s.%s", v.K, format) 95 | args = args[0 : length-1] 96 | } 97 | } 98 | return format, args 99 | } 100 | 101 | // Plural is Plural function 102 | func Plural(cases ...interface{}) []PluralRule { 103 | rules := []PluralRule{} 104 | // %[1]d=1, %[1]d>1 105 | re := regexp.MustCompile(`\[(\d+)\][^=>]\s*(\=|\>)\s*(\d+)$`) 106 | for i := 0; i < len(cases); { 107 | expr := cases[i].(string) 108 | if i++; i >= len(cases) { 109 | return rules 110 | } 111 | text := cases[i].(string) 112 | // cannot match continue 113 | if !re.MatchString(expr) { 114 | continue 115 | } 116 | matches := re.FindStringSubmatch(expr) 117 | pos, _ := strconv.Atoi(matches[1]) 118 | value, _ := strconv.Atoi(matches[3]) 119 | rules = append(rules, PluralRule{ 120 | Pos: pos, 121 | Expr: matches[2], 122 | Value: value, 123 | Text: text, 124 | }) 125 | i++ 126 | } 127 | return rules 128 | } 129 | -------------------------------------------------------------------------------- /i18n/update.go: -------------------------------------------------------------------------------- 1 | package i18n 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path" 8 | "strings" 9 | ) 10 | 11 | // Update messages 12 | func Update(srcFile string, destFile string, flush bool) error { 13 | if len(srcFile) == 0 { 14 | return fmt.Errorf(Sprintf("srcFile cannot be empty")) 15 | } 16 | 17 | if len(destFile) == 0 { 18 | return fmt.Errorf(Sprintf("destFile cannot be empty")) 19 | } 20 | 21 | srcMessages, err := unmarshal(srcFile) 22 | if err != nil { 23 | return err 24 | } 25 | dstMessages, err := unmarshal(destFile) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | oldMessages := *srcMessages 31 | result := *dstMessages 32 | // Delete untranslated lines 33 | for key, value := range *dstMessages { 34 | // Delete untranslated 35 | if key == value { 36 | delete(result, key) 37 | } 38 | // Delete non-existent key 39 | if _, ok := oldMessages[key]; !ok && flush { 40 | delete(result, key) 41 | } 42 | } 43 | // Write new line 44 | for key, value := range *srcMessages { 45 | if _, ok := result[key]; !ok { 46 | result[key] = value 47 | } 48 | } 49 | 50 | var content []byte 51 | of := strings.ToLower(destFile) 52 | if strings.HasSuffix(of, ".json") { 53 | content, err = marshal(result, "json") 54 | } 55 | if strings.HasSuffix(of, ".toml") { 56 | content, err = marshal(result, "toml") 57 | } 58 | if strings.HasSuffix(of, ".yaml") { 59 | content, err = marshal(result, "yaml") 60 | } 61 | if err != nil { 62 | return err 63 | } 64 | 65 | err = os.MkdirAll(path.Dir(destFile), os.ModePerm) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | err = ioutil.WriteFile(destFile, content, os.ModePerm) 71 | if err != nil { 72 | return nil 73 | } 74 | 75 | fmt.Printf("Update %+v ...\n", destFile) 76 | 77 | return nil 78 | } 79 | --------------------------------------------------------------------------------