├── .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 |
--------------------------------------------------------------------------------