├── .github └── workflows │ ├── gitee.yaml │ └── test.yaml ├── .gitignore ├── LICENSE ├── README.md ├── cmd └── xtemplate │ ├── .gitignore │ ├── README.md │ ├── build.sh │ ├── go.mod │ ├── go.sum │ ├── internal │ ├── helper.go │ ├── keyword.go │ ├── param.go │ ├── param_test.go │ ├── run.go │ ├── run_test.go │ └── testdata │ │ ├── base.tmpl │ │ ├── index.tmpl │ │ └── pot_test.pot │ ├── lang │ └── zh_CN.po │ └── main.go ├── context.go ├── context_test.go ├── errors └── errors.go ├── example_test.go ├── f ├── format.go └── format_test.go ├── fs.go ├── fs_test.go ├── gettext.go ├── go.mod ├── go.sum ├── go.work ├── go.work.sum ├── http.go ├── http_test.go ├── init.go ├── locale ├── detect.go └── detect_test.go ├── mark.go ├── matcher.go ├── matcher_test.go ├── plurals ├── common.go ├── debug.go ├── err.go ├── err_test.go ├── exp.go ├── exp_test.go ├── parser │ ├── plural_base_listener.go │ ├── plural_lexer.go │ ├── plural_listener.go │ └── plural_parser.go ├── plural.g4 └── stack.go ├── tag.go ├── tag_test.go ├── testdata ├── messages.new.pot ├── messages.pot ├── zh_CN.mo ├── zh_CN.po └── zh_CN_save.mo ├── translations.go ├── translaton.go ├── translator.go ├── translator ├── a_test.go ├── entry.go ├── file.go ├── file_test.go ├── mo.go ├── mo_test.go ├── plural.go ├── po.go ├── po_test.go ├── pot.go ├── pot_test.go ├── reader.go ├── reader_test.go ├── translator.go ├── util.go ├── wrap.go └── wrap_test.go └── unittest.sh /.github/workflows/gitee.yaml: -------------------------------------------------------------------------------- 1 | # # https://github.com/Yikun/hub-mirror-action 2 | name: 同步到 Gitee 3 | on: push 4 | jobs: 5 | run: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout source codes 9 | uses: actions/checkout@v2 10 | - name: sync to gitee 11 | uses: Yikun/hub-mirror-action@master 12 | with: 13 | src: github/youthLin 14 | dst: gitee/youthlin 15 | dst_key: ${{ secrets.dst_key }} 16 | dst_token: ${{ secrets.dst_token }} 17 | static_list: 't' 18 | mappings: "t=>gottext" 19 | debug: true 20 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test and coverage 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | with: 11 | fetch-depth: 2 12 | - uses: actions/setup-go@v2 13 | with: 14 | go-version: "1.23" 15 | - name: Run coverage 16 | run: go test -race -coverprofile=coverage.txt -covermode=atomic ./... 17 | - name: Upload coverage to Codecov 18 | run: bash <(curl -s https://codecov.io/bash) 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | output/ 3 | .antlr/ 4 | *.interp 5 | *.tokens 6 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Youth.霖 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # t 2 | t: a translation util for go, inspired by GNU gettext 3 | t: GNU gettext 的 Go 语言实现,Go 程序的国际化工具 4 | [![sync-to-gitee](https://github.com/youthlin/t/actions/workflows/gitee.yaml/badge.svg)](https://github.com/youthlin/t/actions/workflows/gitee.yaml) 5 | [![test](https://github.com/youthlin/t/actions/workflows/test.yaml/badge.svg)](https://github.com/youthlin/t/actions/workflows/test.yaml) 6 | [![codecov](https://codecov.io/gh/youthlin/t/branch/main/graph/badge.svg?token=6RyU5nb3YT)](https://codecov.io/gh/youthlin/t) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/youthlin/t)](https://goreportcard.com/report/github.com/youthlin/t) 8 | [![Go Reference](https://pkg.go.dev/badge/github.com/youthlin/t.svg)](https://pkg.go.dev/github.com/youthlin/t) 9 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fyouthlin%2Ft.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fyouthlin%2Ft?ref=badge_shield) 10 | 11 | 12 | ## Install 安装 13 | 14 | ```bash 15 | go get -u github.com/youthlin/t 16 | ``` 17 | 18 | go.mod 19 | ```go 20 | require ( 21 | github.com/youthlin/t latest 22 | ) 23 | ``` 24 | 25 | Gitee 镜像:[gitee.com/youthlin/gottext](gitee.com/youthlin/gottext) (gottext: go + gettext) 26 | > 鸣谢仓库同步工具:https://github.com/Yikun/hub-mirror-action 27 | ``` 28 | // 使用 gitee 镜像 29 | // go.mod: 30 | replace github.com/youthlin/t latest => gitee.com/youthlin/gottext latest 31 | ``` 32 | 33 | 34 | ## Usage 使用 35 | ```go 36 | path := "path/to/filename.po" // .po, .mo file 37 | path = "path/to/po_mo/dir" // or dir. 38 | // (mo po 同名的话,po 后加载,会覆盖 mo 文件,因为 po 是文本文件,方便修改生效) 39 | // 1 bind domain 绑定翻译文件 40 | t.Load(path) // 默认绑定在 default 域 会自动搜索路径下的文件,读取 po/mo 里的语言标签进行注册 41 | t.Bind("my-domain", path) // 或者指定Ø文本域 42 | // 2 set current domain 设置使用的文本域 43 | t.SetDomain("my-domain") 44 | // 3 set user language 设置用户语言 45 | // t.SetLocale("zh_CN") 46 | t.SetLocale(t.MostMatchLocale()) // empty to use system default 47 | // 4 use the gettext api 使用 gettext 翻译接口 48 | fmt.Println(t.T("Hello, world")) 49 | fmt.Println(t.T("Hello, %v", "Tom")) 50 | fmt.Println(t.N("One apple", "%d apples", 1)) // One apple 51 | fmt.Println(t.N("One apple", "%d apples", 2)) // %d apples 52 | // t.N(single, plural, n, args...) 53 | // n => used to choose single or plural 54 | // args => to format 55 | // args... supported, used to format string 56 | // 支持传入 args... 参数用于格式化输出 57 | fmt.Println(t.N("One apple", "%d apples", 2, 2)) // 2 apples 58 | fmt.Println(t.N("%[2]s has one apple", "%[2]s has %[1]d apples", 2, 200, "Bob")) 59 | // Bob has 200 apples 60 | t.X("msg_context_text", "msg_id") 61 | t.X("msg_context_text", "msg_id") 62 | t.XN("msg_context_text", "msg_id", "msg_plural", n) 63 | ``` 64 | 65 | ## API 66 | ```go 67 | T(msgID, args...) 68 | N(msgID, msgIDPlural, n, args...) // and N64 69 | X(msgCTxt, msgID, args...) 70 | XN(msgCTxt, msgID, msgIDPlural, n, args...) // and XN64 71 | D(domain) 72 | L(locale) 73 | // T: gettext 74 | // N: ngettext 75 | // X: pgettext 76 | // XN: npgettext 77 | // D: with domain 78 | // L: with locale(language) 79 | ``` 80 | 81 | ## Domain 文本域 82 | ```go 83 | t.Bind(domain1, path1) 84 | t.Bind(domain2, path2) 85 | t.SetLocale("zh_CN") 86 | 87 | t.T("msg_id") // use default domain 88 | 89 | t.SetDomain(domain1) 90 | t.T("msg_id") // use domain1 91 | t.D(domain2).T("msg_id") // use domain2 92 | t.D("unknown-domain").T("msg_id") // return "msg_id" directly 93 | 94 | ``` 95 | 96 | ## Language 指定语言 97 | If you are building a web application, you may want each request use diffenrent language, the code below may help you: 98 | 如果你写的是 web 应用而不是 cli 工具,你可能想让每个 request 使用不同的语言,请看: 99 | 100 | ```go 101 | t.Load(path) 102 | 103 | // a) Specify a language 可以指定语言 104 | t.L("zh_CN").T("msg_id") 105 | 106 | // b) every one use his own language 每个用户使用他接受的语言 107 | // b.1) server supports 第一步,服务器支持的语言 108 | langs := t.Locales() 109 | // golang.org/x/text/language 110 | // EN: https://blog.golang.org/matchlang 111 | // 中文: https://learnku.com/docs/go-blog/matchlang/6525 112 | var supported []language.Tag 113 | for _, lang =range langs{ 114 | supported = append(supported, language.Make(lang)) 115 | } 116 | matcher := language.NewMatcher(supported) 117 | // b.2) user accept 第二步,用户接受的语言 118 | // Judging by the browser header(Accept-Language) 119 | // 根据浏览器标头获取用户语言 120 | // or: userAccept := []language.Tag{ language.Make("lang-code-from-cookie") } 121 | // 或从 cookie 中获取用户语言偏好 122 | userAccept, q, err :=language.ParseAcceptLanguage("zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6") 123 | // b.3) match 第三步,匹配出最合适的 124 | matchedTag, index, confidence := matcher.Match(userAccept...) 125 | // confidence may language.No, language.Low, language.High, language.Exact 126 | // 这里 confidence 是指匹配度,可以根据你的实际需求决定是否使用匹配的语言。 127 | // 如服务器支持 英文、简体中文,如果用户是繁体中文,那匹配度就不是 Exact, 128 | // 这时根据实际需求决定是使用英文,还是简体中文。 129 | userLang := langs[index] 130 | t.L(userLang).T("msg_id") 131 | 132 | // with domain, language 同时指定文本域、用户语言 133 | t.D(domain).L(userLang).T("msg_id") 134 | ``` 135 | 136 | > more examples can be find at: [example_test.go](example_test.go) 137 | 138 | ## How to extract string 提取翻译文本 139 | ```bash 140 | # if you use PoEdit, add a extractor 141 | # 如果你使用 PoEdit,在设置-提取器中新增一个提取器 142 | # Language: Go, *.go 语言填 Go 扩展名填 *.go 提取翻译的命令填写 143 | # xgettext -C --add-comments=TRANSLATORS: --force-po -o %o %C %K %F 144 | # 最后的三个输入框分别填写 145 | # -k%k 146 | # %f 147 | # --from-code=%c 148 | # keywords: 关键字这样设置: 149 | # T:1;N:1,2;N64:1,2;X:2,1c;XN:2,3,1c;XN64:2,3,1c 150 | xgettext -C --add-comments=TRANSLATORS: --force-po -kT -kN:1,2 -kX:2,1c -kXN:2,3,1c *.go 151 | ``` 152 | 153 | ## Done 已完成 154 | - ✅ mo file 支持 mo 二进制文件 155 | - ✅ extract from html templates 从模板文件中提取: [xtemplate](cmd/xtemplate/) 156 | ```bash 157 | go install github.com/youthlin/t/cmd/xtemplate@latest 158 | ``` 159 | 160 | ## Links 链接 161 | - https://www.gnu.org/software/gettext/manual/html_node/index.html 162 | - https://github.com/search?l=Go&q=gettext&type=Repositories 163 | - https://github.com/antlr/antlr4/ 164 | - https://blog.gopheracademy.com/advent-2017/parsing-with-antlr4-and-go/ 165 | - https://xuanwo.io/2019/12/11/golang-i18n/ (中文) 166 | 167 | 168 | 169 | ## License 170 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fyouthlin%2Ft.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fyouthlin%2Ft?ref=badge_large) -------------------------------------------------------------------------------- /cmd/xtemplate/.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | output/ 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /cmd/xtemplate/README.md: -------------------------------------------------------------------------------- 1 | # xtemplate 2 | 从 go 模板文件中提取翻译文本,并保存为 pot 文件。 3 | extract msgid from go template file and save to a pot file. 4 | 5 | 6 | ```bash 7 | go install github.com/youthlin/t/cmd/xtemplate@latest 8 | xtemplate -i -k keywords 9 | 10 | # e.g. 11 | # help 12 | xtemplate -h 13 | # extract and save (with keywords(-k), function names(-f)) 14 | xtemplate -i "path/to/**/*.tmpl" -k "T;N:1,2;N64:1,2;X:1c,2;XN:1c,2,3;XN64:1c,2,3" -f FunName,Fun2 -o path/to/save/name.pot 15 | ``` 16 | 17 | 18 | ```bash 19 | ## usage: 20 | xtemplate -i input-pattern -k keywords [-f functions] [-o output] 21 | -d debug mode 22 | -f string 23 | function names of template 24 | -h show help message 25 | -i string 26 | input file pattern 27 | -k string 28 | keywords e.g.: gettext;T:1;N1,2;X:1c,2;XN:1c,2,3 29 | -left string 30 | left delim (default "{{") 31 | -o string 32 | output file, - is stdout 33 | -right string 34 | right delim (default "}}") 35 | -v show version 36 | 37 | ## 用法 38 | 39 | xtemplate -i 输入文件 -k 关键字 [-f 模版中函数] [-o 输出文件] 40 | -d debug 模式 41 | -f string 42 | 模板中用到的函数名 43 | -h 显示帮助信息 44 | -i string 45 | 输入文件 46 | -k string 47 | 关键字,例: gettext;T:1;N1,2;X:1c,2;XN:1c,2,3 48 | -left string 49 | 左分隔符 (default "{{") 50 | -o string 51 | 输出文件,- 表示标准输出 52 | -right string 53 | 右分隔符 (default "}}") 54 | -v 显示版本号 55 | ``` 56 | 57 | ## translations of this project 58 | 本项目的 `lang` 目录包含一个 po 文件,可以将其翻译为需要的语言, 59 | 然后设置环境变量 `LANG_PATH` 以加载翻译,默认的加载路径是 `./lang` 目录。 60 | 61 | You can find a po file in `lang` dir. 62 | set `LANG_PATH=/path/to/po/dir` to load your translations. 63 | the default dir is `./lang`. 64 | -------------------------------------------------------------------------------- /cmd/xtemplate/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | rm -rf output 3 | mkdir output 4 | cp -r lang output/ 5 | go env 6 | go mod tidy 7 | 8 | RED='\033[1;31m' #红 9 | GREEN='\033[1;32m' #绿 10 | RES='\033[0m' 11 | OUT='xtemplate' 12 | 13 | GIT_SHA=$(git rev-parse --short HEAD 2>/dev/null || echo "GitNotFoundOrNoCommitFound") 14 | DATE=$(date '+%Y%m%d%H%M%S') 15 | VERSION=${DATE}-${GIT_SHA} 16 | 17 | if [ "${ENV}" == "dev" ]; then 18 | echo -e "Compiling in ${GREEN}develop${RES} mode." 19 | go build -gcflags="all=-N -l" -ldflags "-X main.Version=${VERSION}" -o output/${OUT} 20 | else 21 | go build -ldflags "-X main.Version=${VERSION}" -o output/${OUT} 22 | fi 23 | 24 | # 打印编译结果 25 | RET=$? 26 | if [ $RET == 0 ]; then 27 | echo -e "build ${GREEN}success${RES}" 28 | else 29 | echo -e "build ${RED}failed${RES}" 30 | exit $RET 31 | fi 32 | -------------------------------------------------------------------------------- /cmd/xtemplate/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/youthlin/t/cmd/xtemplate 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/smartystreets/goconvey v1.8.1 7 | github.com/youthlin/t v0.0.9 8 | ) 9 | 10 | require ( 11 | github.com/Xuanwo/go-locale v1.1.0 // indirect 12 | github.com/antlr4-go/antlr/v4 v4.13.0 // indirect 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/gopherjs/gopherjs v1.17.2 // indirect 15 | github.com/jtolds/gls v4.20.0+incompatible // indirect 16 | github.com/smarty/assertions v1.15.0 // indirect 17 | golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect 18 | golang.org/x/sys v0.6.0 // indirect 19 | golang.org/x/text v0.14.0 // indirect 20 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /cmd/xtemplate/internal/helper.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/youthlin/t/translator" 7 | ) 8 | 9 | const eot = "\x04" 10 | 11 | func isPlural(e *translator.Entry) bool { 12 | return e.MsgID2 != "" 13 | } 14 | 15 | func key(ctxt, msgid string) string { 16 | return ctxt + eot + msgid 17 | } 18 | 19 | func isGoFormat(e *translator.Entry) bool { 20 | return strings.Contains(e.MsgID, "%") || strings.Contains(e.MsgID2, "%") 21 | } 22 | -------------------------------------------------------------------------------- /cmd/xtemplate/internal/keyword.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/youthlin/t" 9 | "github.com/youthlin/t/errors" 10 | ) 11 | 12 | // Keyword gettext keyword 13 | type Keyword struct { 14 | Name string 15 | MsgCtxt int 16 | MsgID int 17 | MsgID2 int 18 | } 19 | 20 | // ParseKeywords gettext;T:1;N:1,2;X:1c,2;XN:1c,2,3 21 | func ParseKeywords(str string) (result []Keyword, err error) { 22 | kw := strings.Split(str, ";") 23 | msg := t.T("invalid keywords: %s", str) 24 | for _, key := range kw { 25 | // T 26 | // T:1 27 | // N:1,2 28 | // X:1c,2 29 | // XN:1c,2,3 30 | nameIndex := strings.Split(key, ":") 31 | if len(nameIndex) == 1 { 32 | name := nameIndex[0] 33 | if name == "" { 34 | return nil, errors.Errorf(msg) 35 | } 36 | result = append(result, Keyword{Name: name, MsgID: 1}) 37 | continue 38 | } 39 | if len(nameIndex) != 2 { 40 | return nil, errors.Errorf(msg) 41 | } 42 | k := Keyword{ 43 | Name: nameIndex[0], 44 | } 45 | index := strings.Split(nameIndex[1], ",") 46 | switch len(index) { 47 | case 1: 48 | i, err := strconv.ParseInt(index[0], 10, 64) 49 | if err != nil { 50 | return nil, errors.Wrapf(err, msg+t.T("msg id index is not a number")) 51 | } 52 | k.MsgID = int(i) 53 | case 2: 54 | i1 := index[0] 55 | i2 := index[1] 56 | i1c := strings.HasSuffix(i1, "c") 57 | if i1c { 58 | c := i1[:len(i1)-1] 59 | cIndex, err := strconv.ParseInt(c, 10, 64) 60 | if err != nil { 61 | return nil, errors.Wrapf(err, msg+t.T("context index is not a number: %v", c)) 62 | } 63 | k.MsgCtxt = int(cIndex) 64 | 65 | index, err := strconv.ParseInt(i2, 10, 64) 66 | if err != nil { 67 | return nil, errors.Wrapf(err, msg+t.T("msg id index is not a number: %v", i2)) 68 | } 69 | k.MsgID = int(index) 70 | } else { 71 | index, err := strconv.ParseInt(i1, 10, 64) 72 | if err != nil { 73 | return nil, errors.Wrapf(err, msg+t.T("msg id index is not a number: %v", i1)) 74 | } 75 | k.MsgID = int(index) 76 | 77 | index, err = strconv.ParseInt(i2, 10, 64) 78 | if err != nil { 79 | return nil, errors.Wrapf(err, msg+t.T("msg plural index is not a number: %v", i2)) 80 | } 81 | k.MsgID2 = int(index) 82 | } 83 | case 3: 84 | i1 := index[0] 85 | i2 := index[1] 86 | i3 := index[2] 87 | if !strings.HasSuffix(i1, "c") { 88 | return nil, errors.Errorf(msg + t.T("context index must end with 'c': %v", i1)) 89 | } 90 | c := i1[:len(i1)-1] 91 | index, err := strconv.ParseInt(c, 10, 64) 92 | if err != nil { 93 | return nil, errors.Wrapf(err, msg+t.T("msg context index is not a number: %v", c)) 94 | } 95 | k.MsgCtxt = int(index) 96 | 97 | index, err = strconv.ParseInt(i2, 10, 64) 98 | if err != nil { 99 | return nil, errors.Wrapf(err, msg+t.T("msg id index is not a number: %v", i2)) 100 | } 101 | k.MsgID = int(index) 102 | 103 | index, err = strconv.ParseInt(i3, 10, 64) 104 | if err != nil { 105 | return nil, errors.Wrapf(err, msg+t.T("msg id index is not a number: %v", i3)) 106 | } 107 | k.MsgID2 = int(index) 108 | default: 109 | return nil, errors.Errorf(msg + t.T("tow much keyword index")) 110 | } 111 | result = append(result, k) 112 | } 113 | return 114 | } 115 | 116 | // Writer is fileName is empty or - use stdout, otherwise use file 117 | func Writer(fileName string) (wr *os.File, err error) { 118 | wr = os.Stdout 119 | if fileName != "" && fileName != "-" { 120 | wr, err = os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY, 0644) 121 | if err != nil { 122 | err = errors.Wrapf(err, t.T("can not open output file: %s", fileName)) 123 | } 124 | } 125 | return 126 | } 127 | -------------------------------------------------------------------------------- /cmd/xtemplate/internal/param.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | "github.com/youthlin/t" 11 | "github.com/youthlin/t/errors" 12 | "github.com/youthlin/t/translator" 13 | ) 14 | 15 | // Param 输入参数 16 | type Param struct { 17 | Input string 18 | Left string 19 | Right string 20 | Keyword string 21 | Function string 22 | OutputFile string 23 | Debug bool 24 | } 25 | 26 | // debugPrint print if is debug mode 27 | func (p *Param) debugPrint(format string, args ...interface{}) { 28 | if p.Debug { 29 | fmt.Printf(format+"\n", args...) 30 | } 31 | } 32 | 33 | // Context parameters and result 34 | type Context struct { 35 | *Param 36 | Keywords []Keyword 37 | Functions template.FuncMap 38 | Output *os.File 39 | hasPlural bool 40 | entries map[string]*translator.Entry 41 | } 42 | 43 | func newCtx(param *Param) (*Context, error) { 44 | ctx := &Context{ 45 | Param: param, 46 | Functions: make(template.FuncMap), 47 | entries: make(map[string]*translator.Entry), 48 | } 49 | kw, err := ParseKeywords(param.Keyword) 50 | if err != nil { 51 | return nil, err 52 | } 53 | ctx.Keywords = kw 54 | for _, k := range kw { 55 | ctx.Functions[k.Name] = noopFun 56 | } 57 | wr, err := Writer(param.OutputFile) 58 | if err != nil { 59 | return nil, err 60 | } 61 | ctx.Output = wr 62 | 63 | if ctx.Function != "" { 64 | fun := strings.Split(ctx.Function, ",") 65 | for _, name := range fun { 66 | ctx.Functions[name] = noopFun 67 | } 68 | } 69 | return ctx, nil 70 | } 71 | 72 | // Add add a message entry 73 | func (ctx *Context) Add(entry *translator.Entry) error { 74 | plural := isPlural(entry) 75 | key := entry.Key() 76 | pre, ok := ctx.entries[key] 77 | if ok { 78 | if isPlural(pre) != plural { 79 | return errors.Errorf(t.T("msgid '%v' is used without plural and with plural.\nLine =%v\nPrevious=%v"), 80 | entry.MsgID, entry.MsgCmts, pre.MsgCmts) 81 | } 82 | pre.MsgCmts = append(pre.MsgCmts, entry.MsgCmts...) 83 | } else { 84 | ctx.entries[key] = entry 85 | if plural { 86 | ctx.hasPlural = true 87 | } 88 | } 89 | return nil 90 | } 91 | 92 | // Write write pot file to output 93 | func (ctx *Context) Write() error { 94 | pot := ctx.pot() 95 | return pot.SaveAsPot(ctx.Output) 96 | } 97 | 98 | func (ctx *Context) pot() *translator.File { 99 | pot := new(translator.File) 100 | pot.AddEntry(ctx.header()) 101 | for _, e := range ctx.entries { 102 | pot.AddEntry(e) 103 | } 104 | return pot 105 | } 106 | 107 | func (ctx *Context) header() *translator.Entry { 108 | e := new(translator.Entry) 109 | e.MsgCmts = []string{ 110 | "# SOME DESCRIPTIVE TITLE.", 111 | "# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER", 112 | "# This file is distributed under the same license as the PACKAGE package.", 113 | "# FIRST AUTHOR , YEAR.", 114 | "#", 115 | "#, fuzzy", 116 | } 117 | headers := []string{ 118 | "Project-Id-Version: PACKAGE VERSION", 119 | "Report-Msgid-Bugs-To: ", 120 | fmt.Sprintf("POT-Creation-Date: %v", time.Now().Format("2006-01-02 15:04:05-0700")), 121 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE", 122 | "Last-Translator: FULL NAME ", 123 | "Language-Team: LANGUAGE ", 124 | "Language: ", 125 | "MIME-Version: 1.0", 126 | "Content-Type: text/plain; charset=CHARSET", 127 | "Content-Transfer-Encoding: 8bit", 128 | "X-Created-By: xtemplate(https://github.com/youthlin/t/tree/main/cmd/xtemplate)", 129 | } 130 | if ctx.hasPlural { 131 | headers = append(headers, "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;") 132 | } 133 | headers = append(headers, fmt.Sprintf("X-Xtemplate-Input: %v", ctx.Input)) 134 | headers = append(headers, fmt.Sprintf("X-Xtemplate-Left: %v", ctx.Left)) 135 | headers = append(headers, fmt.Sprintf("X-Xtemplate-Right: %v", ctx.Right)) 136 | headers = append(headers, fmt.Sprintf("X-Xtemplate-Keywords: %v", ctx.Keyword)) 137 | headers = append(headers, fmt.Sprintf("X-Xtemplate-Functions: %v", ctx.Function)) 138 | headers = append(headers, fmt.Sprintf("X-Xtemplate-Output: %v", ctx.OutputFile)) 139 | headers = append(headers, "") 140 | e.MsgStr = strings.Join(headers, "\n") 141 | return e 142 | } 143 | -------------------------------------------------------------------------------- /cmd/xtemplate/internal/param_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "os" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestParseKeywords(t *testing.T) { 10 | type args struct { 11 | str string 12 | } 13 | tests := []struct { 14 | name string 15 | args args 16 | wantResult []Keyword 17 | wantErr bool 18 | }{ 19 | {"empty", args{}, nil, true}, 20 | {"T", args{"T"}, []Keyword{{Name: "T", MsgID: 1}}, false}, 21 | {"T:1", args{"T:1"}, []Keyword{{Name: "T", MsgID: 1}}, false}, 22 | {"N:1,2", args{"N:1,2"}, []Keyword{{Name: "N", MsgID: 1, MsgID2: 2}}, false}, 23 | {"X:1c,2", args{"X:1c,2"}, []Keyword{{Name: "X", MsgCtxt: 1, MsgID: 2}}, false}, 24 | {"XN:1c,2,3", args{"XN:1c,2,3"}, []Keyword{{Name: "XN", MsgCtxt: 1, MsgID: 2, MsgID2: 3}}, false}, 25 | {"T;XN:1c,2,3", args{"T;XN:1c,2,3"}, []Keyword{ 26 | {Name: "T", MsgID: 1}, 27 | {Name: "XN", MsgCtxt: 1, MsgID: 2, MsgID2: 3}, 28 | }, false}, 29 | 30 | {"invalid-length", args{"T::"}, nil, true}, 31 | {"invalid-nan", args{"T:a"}, nil, true}, 32 | {"invalid-c-nan", args{"T:ac,2"}, nil, true}, 33 | {"invalid-2-nan", args{"T:1c,a"}, nil, true}, 34 | {"invalid-plural-1", args{"T:a,b"}, nil, true}, 35 | {"invalid-plural-2", args{"T:1,b"}, nil, true}, 36 | {"invalid-3-missing-c", args{"XN:1,2,3"}, nil, true}, 37 | {"invalid-3-c-nan", args{"XN:ac,2,3"}, nil, true}, 38 | {"invalid-3-missing-id", args{"XN:1c,,3"}, nil, true}, 39 | {"invalid-3-id", args{"XN:1c,a,3"}, nil, true}, 40 | {"invalid-3-missing-id2", args{"XN:1c,2,"}, nil, true}, 41 | {"invalid-3-id2", args{"XN:1c,2,a"}, nil, true}, 42 | 43 | {"no-arg", args{"XN:"}, nil, true}, 44 | {"much-arg", args{"XN:1,2,3,4"}, nil, true}, 45 | } 46 | for _, tt := range tests { 47 | t.Run(tt.name, func(t *testing.T) { 48 | gotResult, err := ParseKeywords(tt.args.str) 49 | if (err != nil) != tt.wantErr { 50 | t.Errorf("ParseKeywords() error = %v, wantErr %v", err, tt.wantErr) 51 | return 52 | } 53 | if !reflect.DeepEqual(gotResult, tt.wantResult) { 54 | t.Errorf("ParseKeywords() = %v, want %v", gotResult, tt.wantResult) 55 | } 56 | }) 57 | } 58 | } 59 | 60 | func TestWriter(t *testing.T) { 61 | want, err := os.OpenFile("param_test.go", os.O_CREATE|os.O_WRONLY, 0644) 62 | if err != nil { 63 | t.Log(err) 64 | } 65 | type args struct { 66 | fileName string 67 | } 68 | tests := []struct { 69 | name string 70 | args args 71 | wantWr *os.File 72 | wantErr bool 73 | }{ 74 | {"empty", args{}, os.Stdout, false}, 75 | {"-", args{"-"}, os.Stdout, false}, 76 | {"file", args{"param_test.go"}, want, false}, 77 | {"can-not-create-if-dir-not-exist", args{"no-such-dir/file.pot"}, nil, true}, 78 | } 79 | for _, tt := range tests { 80 | t.Run(tt.name, func(t *testing.T) { 81 | gotWr, err := Writer(tt.args.fileName) 82 | if (err != nil) != tt.wantErr { 83 | t.Errorf("Writer() error = %v, wantErr %v", err, tt.wantErr) 84 | return 85 | } 86 | if (tt.wantWr == nil) == (gotWr == nil) { 87 | return 88 | } 89 | if (tt.wantWr == nil) != (gotWr == nil) { 90 | t.Errorf("Writer() = %v, want %v", gotWr, tt.wantWr) 91 | return 92 | } 93 | gotF, e1 := gotWr.Stat() 94 | wantF, e2 := tt.wantWr.Stat() 95 | 96 | if e1 != nil || e2 != nil || !os.SameFile(gotF, wantF) { 97 | t.Errorf("Writer() = %v, want %v", gotWr, tt.wantWr) 98 | } 99 | }) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /cmd/xtemplate/internal/run.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | "os" 7 | "path/filepath" 8 | "text/template/parse" 9 | 10 | "github.com/youthlin/t" 11 | "github.com/youthlin/t/errors" 12 | "github.com/youthlin/t/translator" 13 | ) 14 | 15 | var noopFun = func() string { return "" } 16 | 17 | // Run 运行解析任务 18 | func Run(param *Param) error { 19 | param.debugPrint("run param=%+v", param) 20 | ctx, err := newCtx(param) 21 | if err != nil { 22 | return err 23 | } 24 | filenames, err := filepath.Glob(param.Input) 25 | param.debugPrint("Glob files=%v err=%+v", filenames, err) 26 | if err != nil { 27 | return errors.Wrapf(err, t.T("invalid input pattern")) 28 | } 29 | 30 | for _, filename := range filenames { 31 | if err := resolveOneFile(filename, ctx); err != nil { 32 | if param.Debug { 33 | printErr(t.T("failed to process file %v. error message: %+v"), filename, err) 34 | } else { 35 | printErr(t.T("failed to process file %v. error message: %v"), filename, err) 36 | } 37 | } 38 | } 39 | 40 | ctx.debugPrint("extract done, %d entries", len(ctx.entries)) 41 | 42 | if err := ctx.Write(); err != nil { 43 | return err 44 | } 45 | return ctx.Output.Close() 46 | } 47 | 48 | // printErr print message to stderr 49 | func printErr(format string, args ...interface{}) { 50 | fmt.Fprintf(os.Stderr, format+"\n", args...) 51 | } 52 | 53 | // resolveOneFile 处理每个文件 54 | func resolveOneFile(filename string, ctx *Context) error { 55 | ctx.debugPrint("resolve one file: filename=%v", filename) 56 | tmpl, err := template.New(""). 57 | Delims(ctx.Left, ctx.Right). 58 | Funcs(ctx.Functions). 59 | ParseFiles(filename) 60 | if err != nil { 61 | return errors.Wrapf(err, t.T("failed to parse file %v"), filename) 62 | } 63 | // 一个文件可能有多个模板 64 | for _, tmpl := range tmpl.Templates() { 65 | resolveTmpl(filename, ctx, tmpl) 66 | } 67 | return nil 68 | } 69 | 70 | // resolveTmpl 处理每个模板 71 | func resolveTmpl(filename string, ctx *Context, tmpl *template.Template) { 72 | ctx.debugPrint("process template: [filename=%v] [template name=%v]", filename, tmpl.Name()) 73 | if tmpl.Tree == nil || tmpl.Tree.Root == nil { 74 | ctx.debugPrint(" > filename=%v, template=%v, tree or Root is nil", filename, tmpl.Name()) 75 | return 76 | } 77 | root := tmpl.Tree.Root 78 | for _, node := range root.Nodes { 79 | ctx.debugPrint(" > node=%#v", node) 80 | // comment 会被忽略,这里拿不到注释信息 81 | switch node.Type() { 82 | case parse.NodeAction: 83 | actionNode := node.(*parse.ActionNode) 84 | resolvePipe(filename, actionNode.Line, ctx, actionNode.Pipe) 85 | case parse.NodeIf: 86 | branchNode := node.(*parse.IfNode) 87 | resolvePipe(filename, branchNode.Line, ctx, branchNode.Pipe) 88 | case parse.NodeRange: 89 | branchNode := node.(*parse.RangeNode) 90 | resolvePipe(filename, branchNode.Line, ctx, branchNode.Pipe) 91 | case parse.NodeWith: 92 | withNode := node.(*parse.WithNode) 93 | resolvePipe(filename, withNode.Line, ctx, withNode.Pipe) 94 | case parse.NodeTemplate: 95 | templateNode := node.(*parse.TemplateNode) 96 | resolvePipe(filename, templateNode.Line, ctx, templateNode.Pipe) 97 | } 98 | } 99 | } 100 | 101 | // resolvePipe 处理 action 节点中的 pipe 102 | func resolvePipe(filename string, line int, ctx *Context, pipe *parse.PipeNode) { 103 | if pipe == nil { 104 | ctx.debugPrint(" > line %v: Pipe is nil", line) 105 | return 106 | } 107 | ctx.debugPrint(" > Pipe: Line=%v", line) 108 | resolveCmds(filename, line, ctx, pipe.Cmds) 109 | } 110 | 111 | // resolveCmds 处理 Cmd 112 | func resolveCmds(filename string, line int, ctx *Context, cmds []*parse.CommandNode) { 113 | for _, cmd := range cmds { 114 | if cmd == nil { 115 | continue 116 | } 117 | ctx.debugPrint(" > > Cmd: Line=%v Pos=%v", line, cmd.Pos) 118 | argC := len(cmd.Args) 119 | for i := 0; i < argC; i++ { 120 | arg := cmd.Args[i] 121 | ctx.debugPrint(" > > > Cmd.Arg: %#v", arg) 122 | switch arg := arg.(type) { 123 | case *parse.PipeNode: 124 | resolvePipe(filename, line, ctx, arg) // 递归 125 | case *parse.IdentifierNode: 126 | filter(ctx, fmt.Sprintf("%v:%d", filename, line), arg.Ident, i, cmd.Args) 127 | case *parse.FieldNode: 128 | lastID := arg.Ident[len(arg.Ident)-1] 129 | filter(ctx, fmt.Sprintf("%v:%d", filename, line), lastID, i, cmd.Args) 130 | } 131 | } 132 | } 133 | } 134 | 135 | func filter(ctx *Context, line, name string, nameIndex int, args []parse.Node) { 136 | argLength := len(args) 137 | for _, kw := range ctx.Keywords { 138 | if kw.Name == name { 139 | argCount := 1 140 | if kw.MsgCtxt > 0 { 141 | argCount++ 142 | } 143 | if kw.MsgID2 > 0 { 144 | argCount++ 145 | } 146 | lastIndex := argCount + nameIndex 147 | if lastIndex >= argLength { 148 | ctx.debugPrint(" > > > ID=%v too few args", name) 149 | continue 150 | } 151 | argOK := true 152 | m := make(map[int]string) 153 | for i := nameIndex + 1; i <= lastIndex; i++ { 154 | arg := args[i] 155 | str, ok := arg.(*parse.StringNode) 156 | if !ok { 157 | ctx.debugPrint(" > > > ID=%v args[%d] is not string node", name, i) 158 | argOK = false 159 | break 160 | } 161 | m[i-nameIndex] = str.Text 162 | } 163 | if !argOK { 164 | continue 165 | } 166 | entry, ok := extract(ctx, line, name, kw, m) 167 | if !ok { 168 | continue 169 | } 170 | if err := ctx.Add(entry); err != nil { 171 | if ctx.Debug { 172 | printErr(t.T("Waringing: %+v"), err) 173 | } else { 174 | printErr(t.T("Waringing: %v"), err) 175 | } 176 | } 177 | } 178 | } 179 | } 180 | 181 | func extract(ctx *Context, line, name string, kw Keyword, m map[int]string) (*translator.Entry, bool) { 182 | entry := new(translator.Entry) 183 | entry.MsgCmts = append(entry.MsgCmts, fmt.Sprintf("#: %v", line)) 184 | if kw.MsgCtxt > 0 { 185 | txt, ok := m[kw.MsgCtxt] 186 | if !ok { 187 | ctx.debugPrint(" > > > ID=%v missing ctxt", name) 188 | return nil, false 189 | } 190 | entry.MsgCtxt = txt 191 | } 192 | txt, ok := m[kw.MsgID] 193 | if !ok { 194 | ctx.debugPrint(" > > > ID=%v missing msg id", name) 195 | return nil, false 196 | 197 | } 198 | entry.MsgID = txt 199 | 200 | if kw.MsgID2 > 0 { 201 | txt, ok := m[kw.MsgID2] 202 | if !ok { 203 | ctx.debugPrint(" > > > ID=%v missing msg plural", name) 204 | return nil, false 205 | 206 | } 207 | entry.MsgID2 = txt 208 | } 209 | if isGoFormat(entry) { 210 | entry.MsgCmts = append(entry.MsgCmts, "#, go-format") 211 | } 212 | ctx.debugPrint("【ok】 > > ID=%v entry=%+v", name, entry) 213 | return entry, true 214 | } 215 | -------------------------------------------------------------------------------- /cmd/xtemplate/internal/run_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "html/template" 5 | "path/filepath" 6 | "testing" 7 | 8 | . "github.com/smartystreets/goconvey/convey" 9 | "github.com/youthlin/t/translator" 10 | ) 11 | 12 | func TestGlob(t *testing.T) { 13 | Convey("glob", t, func() { 14 | filenames, err := filepath.Glob("testdata/*.tmpl") 15 | So(err, ShouldBeNil) 16 | t.Logf("files: %v", filenames) 17 | }) 18 | } 19 | 20 | func TestFile(t *testing.T) { 21 | Convey("resolveOneFile", t, func() { 22 | resolveOneFile("testdata/index.tmpl", &Context{ 23 | Param: &Param{ 24 | Left: "{{", 25 | Right: "}}", 26 | Debug: true, 27 | }, 28 | Keywords: []Keyword{ 29 | {Name: "T", MsgID: 1}, 30 | {Name: "N", MsgID: 1, MsgID2: 2}, 31 | {Name: "X", MsgCtxt: 1, MsgID: 2}, 32 | {Name: "XN", MsgCtxt: 1, MsgID: 2, MsgID2: 3}, 33 | }, 34 | Functions: template.FuncMap{"T": noopFun, "X": noopFun}, 35 | entries: make(map[string]*translator.Entry), 36 | }) 37 | }) 38 | } 39 | 40 | func Test_run(t *testing.T) { 41 | Convey("run", t, func() { 42 | Run(&Param{ 43 | Input: "testdata/*.tmpl", 44 | Left: "{{", 45 | Right: "}}", 46 | Keyword: "T;X:1c,2;N:1,2;XN:1c,2,3", 47 | Function: "T", 48 | OutputFile: "-", 49 | Debug: true, 50 | }) 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /cmd/xtemplate/internal/testdata/base.tmpl: -------------------------------------------------------------------------------- 1 | {{- define "header" -}} 2 | 3 | 4 | 5 | 6 | {{- end -}} 7 | 8 | {{- define "title" -}} 9 | {{.}} 10 | 11 | 12 | {{- end -}} 13 | 14 | {{ "id2" | .XN "ctxt" "id" }}// Cmd | Cmd // StringNode | FieldNode=[XN], StringNode="ctxt", StringNOde="id" 15 | {{ .args | .XN "ctxt" "id" "id2" }}// FieldNode=[args] | FieldNode=[XN], StringNode=ctxt, id, id2 16 | {{ .XN "ctxt" "id" `id2` }}// FieldNode=[XN], StringNode=ctxt, id, id2 17 | {{- /* comment */ -}} 18 | {{ T `haha` }}// IdentifierNode=T, StringNode='haha' 19 | {{ `arg` | X "ctxt" "id" }}// StringNode | Ident=X, StringNode=ctxt, id 20 | {{ call T "haha" }}// Ident=call, Ident=T, String=haha 21 | {{ "haha" | T }}// String | Ident 22 | {{ "haha" | .t.T }}// String | Field=[t, T] 23 | 24 | {{ $arg := .arg | T "id" }} 25 | {{ $arg = T "id" }} 26 | {{ T ("id") }}// Ident PipeNode=() 不支持 27 | {{ or (T "id") (T "id") }}// Ident=or PipeNode=() PipeNode=() 28 | 29 | 30 | 1. PipeNode 31 | 2. Cmd.Args FieldNode[last] > StringNode 32 | 3. Cmd.Args Ident, Ident > String 33 | 4. Cmd.Args has PipeNode -> 1. 34 | 35 | {{- define "footer" -}} 36 | 37 | 38 | 39 | {{- end -}} -------------------------------------------------------------------------------- /cmd/xtemplate/internal/testdata/index.tmpl: -------------------------------------------------------------------------------- 1 | {{- template "header" . -}} 2 | {{- template "title" .T "Title" -}} 3 | {{- /*Comments*/ -}} 4 | 5 | {{with $foo := .T "foo"}} 6 | {{$foo}} 7 | {{end}} 8 | {{if .T "ok"}} 9 | {{end}} 10 | {{range .T "range"}} 11 |
  • {{.}}
  • 12 | {{end}} 13 | 14 | {{ .T "Hello, World" }} 15 | {{ .T "Hello, %v" }} 16 | {{ .t.X "ctx" "Hello, World" }} 17 | {{ .X "ctxt" "One apple" }} 18 | {{ .X "ctxt" "One apple" }} 19 | {{ .t.XN "ctxt" "One apple" "%v apples" }} 20 | {{ .t.N "id1" 21 | "id2" }} 22 | 23 | {{- template "footer" . -}} -------------------------------------------------------------------------------- /cmd/xtemplate/internal/testdata/pot_test.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2021-08-24 20:12:35+0800\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=CHARSET\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "X-Created-By: xtemplate(https://github.com/youthlin/t/tree/main/cmd/xtemplate)\n" 20 | "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" 21 | "X-Xtemplate-Input: internal/testdata/*.tmpl\n" 22 | "X-Xtemplate-Left: {{\n" 23 | "X-Xtemplate-Right: }}\n" 24 | "X-Xtemplate-Keywords: T;X:1c,2;N:1,2;XN:1c,2,3\n" 25 | "X-Xtemplate-Functions: \n" 26 | "X-Xtemplate-Output: internal/testdata/pot_test.pot\n" 27 | 28 | #: internal/testdata/index.tmpl:4 29 | #, go-format 30 | msgid "Hello, %v" 31 | msgstr "" 32 | 33 | #: internal/testdata/base.tmpl:6 34 | #: internal/testdata/index.tmpl:3 35 | msgid "Hello, World" 36 | msgstr "" 37 | 38 | #: internal/testdata/base.tmpl:14 39 | #: internal/testdata/base.tmpl:16 40 | msgid "haha" 41 | msgstr "" 42 | 43 | #: internal/testdata/base.tmpl:20 44 | #: internal/testdata/base.tmpl:21 45 | #: internal/testdata/base.tmpl:23 46 | #: internal/testdata/base.tmpl:23 47 | msgid "id" 48 | msgstr "" 49 | 50 | #: internal/testdata/index.tmpl:9 51 | msgid "id1" 52 | msgid_plural "id2" 53 | msgstr[0] "" 54 | msgstr[1] "" 55 | 56 | #: internal/testdata/index.tmpl:5 57 | msgctxt "ctx" 58 | msgid "Hello, World" 59 | msgstr "" 60 | 61 | #: internal/testdata/index.tmpl:6 62 | #: internal/testdata/index.tmpl:7 63 | msgctxt "ctxt" 64 | msgid "One apple" 65 | msgstr "" 66 | 67 | #: internal/testdata/base.tmpl:11 68 | #: internal/testdata/base.tmpl:12 69 | msgctxt "ctxt" 70 | msgid "id" 71 | msgid_plural "id2" 72 | msgstr[0] "" 73 | msgstr[1] "" 74 | 75 | -------------------------------------------------------------------------------- /cmd/xtemplate/lang/zh_CN.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: xtemplate\n" 4 | "POT-Creation-Date: 2021-08-24 17:47+0800\n" 5 | "PO-Revision-Date: 2021-08-24 17:48+0800\n" 6 | "Last-Translator: Lin \n" 7 | "Language-Team: https://youthlin.com\n" 8 | "Language: zh_CN\n" 9 | "MIME-Version: 1.0\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "X-Generator: Poedit 2.4.2\n" 13 | "X-Poedit-Basepath: ..\n" 14 | "Plural-Forms: nplurals=1; plural=0;\n" 15 | "X-Poedit-SourceCharset: UTF-8\n" 16 | "X-Poedit-KeywordsList: T;N:1,2;X:1c,2;XN:1c,2,3\n" 17 | "X-Poedit-SearchPath-0: .\n" 18 | "X-Poedit-SearchPathExcluded-0: *_test.go\n" 19 | 20 | #: internal/keyword.go:23 21 | #, c-format 22 | msgid "invalid keywords: %s" 23 | msgstr "非法的关键字:%s" 24 | 25 | #: internal/keyword.go:50 26 | msgid "msg id index is not a number" 27 | msgstr "待翻译原文的位置应该是数字" 28 | 29 | #: internal/keyword.go:61 30 | msgid "context index is not a number: %v" 31 | msgstr "翻译上下文的位置应该是数字" 32 | 33 | #: internal/keyword.go:67 internal/keyword.go:73 internal/keyword.go:99 34 | #: internal/keyword.go:105 35 | msgid "msg id index is not a number: %v" 36 | msgstr "待翻译原文的位置应该是数字:%v" 37 | 38 | #: internal/keyword.go:79 39 | msgid "msg plural index is not a number: %v" 40 | msgstr "待翻译复数内容的位置应该是数字:%v" 41 | 42 | #: internal/keyword.go:88 43 | msgid "context index must end with 'c': %v" 44 | msgstr "翻译上下文的位置必须以 ‘c’ 结尾" 45 | 46 | #: internal/keyword.go:93 47 | msgid "msg context index is not a number: %v" 48 | msgstr "翻译上下文的位置应该是数字:%v" 49 | 50 | #: internal/keyword.go:109 51 | msgid "tow much keyword index" 52 | msgstr "关键字指示的位置太多" 53 | 54 | #: internal/keyword.go:122 55 | #, c-format 56 | msgid "can not open output file: %s" 57 | msgstr "无法打开输出文件:%s" 58 | 59 | #: internal/param.go:77 60 | msgid "" 61 | "msgid '%v' is used without plural and with plural.\n" 62 | "Line =%v\n" 63 | "Previous=%v" 64 | msgstr "" 65 | "待翻译原文 ‘%v’ 即用于单数也用于复数。\n" 66 | "当前行=%v\n" 67 | "已扫描=%v" 68 | 69 | #: internal/run.go:27 70 | msgid "invalid input pattern" 71 | msgstr "输入文件不合法" 72 | 73 | #: internal/run.go:33 74 | msgid "failed to process file %v. error message: %+v" 75 | msgstr "处理文件 %v 失败。错误消息: %+v" 76 | 77 | #: internal/run.go:35 78 | msgid "failed to process file %v. error message: %v" 79 | msgstr "处理文件 %v 失败。错误消息: %v" 80 | 81 | #: internal/run.go:61 82 | msgid "failed to parse file %v" 83 | msgstr "处理文件 %v 失败" 84 | 85 | #: internal/run.go:160 86 | msgid "Waringing: %+v" 87 | msgstr "警告:%+v" 88 | 89 | #: internal/run.go:162 90 | msgid "Waringing: %v" 91 | msgstr "警告:%v" 92 | 93 | #: main.go:32 94 | msgid "unexpected error: %+v\n" 95 | msgstr "非预期错误:%+v\n" 96 | 97 | #: main.go:40 98 | msgid "run error: %+v" 99 | msgstr "运行失败:%+v" 100 | 101 | #: main.go:42 102 | msgid "run error: %v" 103 | msgstr "运行失败:%v" 104 | 105 | #: main.go:51 106 | msgid "input file pattern" 107 | msgstr "输入文件" 108 | 109 | #: main.go:52 110 | msgid "left delim" 111 | msgstr "左分隔符" 112 | 113 | #: main.go:53 114 | msgid "right delim" 115 | msgstr "右分隔符" 116 | 117 | #: main.go:54 118 | msgid "keywords e.g.: gettext;T:1;N1,2;X:1c,2;XN:1c,2,3" 119 | msgstr "关键字,例: gettext;T:1;N1,2;X:1c,2;XN:1c,2,3" 120 | 121 | #: main.go:55 122 | msgid "function names of template" 123 | msgstr "模板中用到的函数名" 124 | 125 | #: main.go:56 126 | msgid "output file, - is stdout" 127 | msgstr "输出文件,- 表示标准输出" 128 | 129 | #: main.go:57 130 | msgid "debug mode" 131 | msgstr "debug 模式" 132 | 133 | #: main.go:58 134 | msgid "show version" 135 | msgstr "显示版本号" 136 | 137 | #: main.go:59 138 | msgid "show help message" 139 | msgstr "显示帮助信息" 140 | 141 | #: main.go:64 142 | #, c-format 143 | msgid "" 144 | "Usage of %s:\n" 145 | "xtemplate -i input-pattern -k keywords [-f functions] [-o output]\n" 146 | msgstr "" 147 | "%s 的用法:\n" 148 | "xtemplate -i 输入文件 -k 关键字 [-f 模版中函数] [-o 输出文件]\n" 149 | -------------------------------------------------------------------------------- /cmd/xtemplate/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | "flag" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/youthlin/t" 10 | "github.com/youthlin/t/cmd/xtemplate/internal" 11 | ) 12 | 13 | // Version the version 14 | var Version string = "v0.0.9" 15 | 16 | //go:embed lang 17 | var embedLangs embed.FS 18 | 19 | // initTranslation init i18n 20 | func initTranslation() { 21 | path, ok := os.LookupEnv("LANG_PATH") 22 | if ok { 23 | t.Load(path) 24 | } else { 25 | t.LoadFS(embedLangs) 26 | } 27 | t.SetLocale("") 28 | } 29 | 30 | func main() { 31 | defer func() { 32 | if e := recover(); e != nil { 33 | fmt.Fprintf(os.Stderr, t.T("unexpected error: %+v\n"), e) 34 | } 35 | }() 36 | initTranslation() 37 | param := buildParam() 38 | err := internal.Run(param) 39 | if err != nil { 40 | if param.Debug { 41 | fmt.Fprintf(os.Stderr, t.T("run error: %+v"), err) 42 | } else { 43 | fmt.Fprintf(os.Stderr, t.T("run error: %v"), err) 44 | } 45 | os.Exit(2) 46 | } 47 | } 48 | 49 | // buildCtx parse os.Args 50 | func buildParam() *internal.Param { 51 | var ( 52 | input = flag.String("i", "", t.T("input file pattern")) 53 | left = flag.String("left", "{{", t.T("left delim")) 54 | right = flag.String("right", "}}", t.T("right delim")) 55 | keywords = flag.String("k", "", t.T("keywords e.g.: gettext;T:1;N:1,2;X:1c,2;XN:1c,2,3")) 56 | fun = flag.String("f", "", t.T("function names of template")) 57 | output = flag.String("o", "", t.T("output file, - is stdout")) 58 | debug = flag.Bool("d", false, t.T("debug mode")) 59 | version = flag.Bool("v", false, t.T("show version")) 60 | help = flag.Bool("h", false, t.T("show help message")) 61 | ) 62 | flag.Usage = func() { 63 | fmt.Fprintf( 64 | flag.CommandLine.Output(), 65 | t.T("Usage of %s:\nxtemplate -i input-pattern -k keywords [-f functions] [-o output]\n"), 66 | os.Args[0], 67 | ) 68 | flag.PrintDefaults() 69 | } 70 | flag.Parse() 71 | if *version { 72 | fmt.Fprintf(os.Stdout, `xtemplate 73 | https://github.com/youthlin/t/tree/main/cmd/xtemplate 74 | by Youth.霖(https://youthlin.com) 75 | version: %v 76 | `, 77 | Version) 78 | os.Exit(0) 79 | } 80 | if *help || len(os.Args) < 5 { // 必填参数: [xtemplate -i xxx -k xxx] 81 | flag.Usage() 82 | os.Exit(0) 83 | } 84 | return &internal.Param{ 85 | Input: *input, 86 | Left: *left, 87 | Right: *right, 88 | Keyword: *keywords, 89 | Function: *fun, 90 | OutputFile: *output, 91 | Debug: *debug, 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package t 2 | 3 | import "context" 4 | 5 | type ( 6 | typeLang struct{} 7 | typeDomain struct{} 8 | ) 9 | 10 | var ( 11 | keyLang = typeLang{} 12 | keyDomain = typeDomain{} 13 | ) 14 | 15 | func SetCtxLocale(ctx context.Context, lang string) context.Context { 16 | return context.WithValue(ctx, keyLang, lang) 17 | } 18 | 19 | func SetCtxDomain(ctx context.Context, domain string) context.Context { 20 | return context.WithValue(ctx, keyDomain, domain) 21 | } 22 | 23 | func GetCtxLocale(ctx context.Context) (string, bool) { 24 | v := ctx.Value(keyLang) 25 | lang, ok := v.(string) 26 | return lang, ok 27 | } 28 | 29 | func GetCtxDomain(ctx context.Context) (string, bool) { 30 | v := ctx.Value(keyDomain) 31 | domain, ok := v.(string) 32 | return domain, ok 33 | } 34 | 35 | func WithContext(ctx context.Context) *Translations { 36 | t := Global() 37 | if lang, ok := GetCtxLocale(ctx); ok { 38 | t = t.L(lang) 39 | } 40 | if domain, ok := GetCtxDomain(ctx); ok { 41 | t = t.D(domain) 42 | } 43 | return t 44 | } 45 | -------------------------------------------------------------------------------- /context_test.go: -------------------------------------------------------------------------------- 1 | package t 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestWithContext(t *testing.T) { 10 | ctx := context.Background() 11 | type args struct { 12 | ctx context.Context 13 | } 14 | tests := []struct { 15 | name string 16 | args args 17 | want *Translations 18 | }{ 19 | {name: "init", args: args{ctx: ctx}, want: NewTranslations()}, 20 | {name: "locale", args: args{ctx: SetCtxLocale(ctx, "zh_CN")}, want: NewTranslations().L("zh_CN")}, 21 | {name: "domain", args: args{ctx: SetCtxDomain(ctx, "my-domain")}, want: NewTranslations().D("my-domain")}, 22 | } 23 | for _, tt := range tests { 24 | t.Run(tt.name, func(t *testing.T) { 25 | if got := WithContext(tt.args.ctx); !reflect.DeepEqual(got, tt.want) { 26 | t.Errorf("WithContext() = %v, want %v", got, tt.want) 27 | } 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /errors/errors.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | func Errorf(format string, args ...interface{}) error { 9 | return fmt.Errorf(format, args...) 10 | } 11 | 12 | func Wrapf(err error, format string, args ...interface{}) error { 13 | args = append(args, err) 14 | return fmt.Errorf(format+": %w", args...) 15 | } 16 | 17 | func WithSecondaryError(err error, additionalErr error) error { 18 | return fmt.Errorf("%w: %w", err, additionalErr) 19 | } 20 | 21 | func Is(err, target error) bool { 22 | return errors.Is(err, target) 23 | } 24 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package t_test 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | "sync" 7 | 8 | "github.com/youthlin/t" 9 | ) 10 | 11 | var mu sync.Mutex 12 | 13 | func markSeq() func() { 14 | mu.Lock() // 顺序执行 15 | t.SetGlobal(t.NewTranslations()) 16 | return func() { mu.Unlock() } 17 | } 18 | 19 | // Example_init_path Load 绑定 path 到默认文本域 20 | func Example_init_path() { 21 | defer markSeq()() 22 | t.Load("testdata") 23 | // empty means system default locale 24 | // 设置为使用系统语言这一步骤可以省略,因为初始化时就是使用系统语言 25 | t.SetLocale("") 26 | // 为了能在其他环境测试通过,所以指定 zh_CN 27 | t.SetLocale("zh_CN") 28 | fmt.Println(t.T("Hello, World")) 29 | // Output: 30 | // 你好,世界 31 | } 32 | 33 | // Example_init_file Load 绑定 path 到默认文本域, path 可以是单个文件 34 | func Example_init_file() { 35 | defer markSeq()() 36 | t.Load("testdata/zh_CN.po") 37 | // will normalize to ll_CC form => zh_CN 38 | // 会格式化为 ll_CC 的形式 39 | t.SetLocale("zh_hans") 40 | fmt.Println(t.T("Hello, World")) 41 | // Output: 42 | // 你好,世界 43 | } 44 | 45 | //go:embed testdata 46 | var pathFS embed.FS 47 | 48 | //go:embed testdata/zh_CN.po 49 | var fileFS embed.FS 50 | 51 | // Example_initFS LoadFS 绑定文件系统到默认文本域 52 | func Example_initFS() { 53 | defer markSeq()() 54 | t.LoadFS(pathFS) 55 | t.SetLocale("zh") 56 | fmt.Println(t.T("Hello, World")) 57 | // Output: 58 | // 你好,世界 59 | } 60 | 61 | // Example_init_fileFS embed.FS 可以是单个文件或文件夹 62 | func Example_init_fileFS() { 63 | defer markSeq()() 64 | t.LoadFS(fileFS) 65 | t.SetLocale("zh_hans") 66 | fmt.Println("Current locale =", t.Locale()) // zh_CN 67 | fmt.Println(t.T("Hello, World")) 68 | 69 | // 设置不支持的语言,会原样返回 70 | t.SetLocale("zh_hant") 71 | fmt.Println("Current locale =", t.Locale()) // zh_TW 72 | fmt.Println(t.T("Hello, World")) 73 | // Output: 74 | // Current locale = zh_CN 75 | // 你好,世界 76 | // Current locale = zh_TW 77 | // Hello, World 78 | } 79 | 80 | // Example_locale 语言设置示例 81 | func Example_locale() { 82 | defer markSeq()() 83 | t.LoadFS(pathFS) 84 | t.SetLocale("zh") 85 | fmt.Println("UsedLocale =", t.UsedLocale()) // zh_CN 86 | t.SetLocale("zh_TW") 87 | fmt.Println("UsedLocale =", t.UsedLocale()) // en_US 88 | fmt.Println("SourceCodeLocale =", t.SourceCodeLocale()) // en_US 89 | 90 | ts := t.NewTranslations() 91 | ts.SetSourceCodeLocale("zh_CN") 92 | ts.SetLocale("en_US") 93 | fmt.Println("empty ts SourceCodeLocale =", ts.SourceCodeLocale()) // zh_CN 94 | fmt.Println("empty ts Locale =", ts.Locale()) // en_US 95 | fmt.Println("empty ts UsedLocale =", ts.UsedLocale()) // zh_CN 96 | fmt.Println(ts.T("你好,世界")) 97 | // Output: 98 | // UsedLocale = zh_CN 99 | // UsedLocale = en_US 100 | // SourceCodeLocale = en_US 101 | // empty ts SourceCodeLocale = zh_CN 102 | // empty ts Locale = en_US 103 | // empty ts UsedLocale = zh_CN 104 | // 你好,世界 105 | } 106 | 107 | // Example_bindDomain 绑定到指定文本域 108 | func Example_bindDomain() { 109 | defer markSeq()() 110 | t.SetLocale("zh") 111 | t.Bind("main", "testdata/zh_CN.po") 112 | fmt.Println("HasDomain(main) =", t.HasDomain("main")) 113 | fmt.Println("HasDomain(no) =", t.HasDomain("no")) 114 | fmt.Println("Domains =", t.Domains()) 115 | fmt.Println(t.T("Hello, World")) 116 | t.SetDomain("main") 117 | fmt.Println(t.T("Hello, World")) 118 | // Output: 119 | // HasDomain(main) = true 120 | // HasDomain(no) = false 121 | // Domains = [main] 122 | // Hello, World 123 | // 你好,世界 124 | } 125 | 126 | func Example_gettext() { 127 | defer markSeq()() 128 | t.Load("testdata") 129 | t.SetLocale("zh_CN") 130 | fmt.Println(t.T("Hello, World")) // 你好,世界 131 | fmt.Println(t.T("Hello, %s", "Tom")) // 你好,世界 132 | fmt.Println(t.N("One apple", "%d apples", 1)) // %d 个苹果 133 | fmt.Println(t.N("One apple", "%d apples", 1, 1)) // 1 个苹果 134 | fmt.Println(t.N("One apple", "%d apples", 2)) // %d 个苹果 135 | fmt.Println(t.N("One apple", "%d apples", 2, 2)) // 2 个苹果 136 | fmt.Println(t.N64("One apple", "%d apples", 200, 200)) // 200 个苹果 137 | fmt.Println(t.X("File|", "Open")) // 打开文件 138 | fmt.Println(t.X("Project|", "Open")) // 打开工程 139 | fmt.Println(t.XN("File|", "Open One", "Open %d", 1, 1)) // 打开 1 个文件 140 | fmt.Println(t.XN("Project|", "Open One", "Open %d", 1)) // 打开 %d 个工程 141 | fmt.Println(t.XN("Project|", "Open One", "Open %d", 1, 1)) // 打开 1 个工程 142 | fmt.Println(t.XN64("Project|", "Open One", "Open %d", 100, 100)) // 打开 100 个工程 143 | // Output: 144 | // 你好,世界 145 | // 你好,Tom 146 | // %d 个苹果 147 | // 1 个苹果 148 | // %d 个苹果 149 | // 2 个苹果 150 | // 200 个苹果 151 | // 打开文件 152 | // 打开工程 153 | // 打开 1 个文件 154 | // 打开 %d 个工程 155 | // 打开 1 个工程 156 | // 打开 100 个工程 157 | } 158 | 159 | func Example_with() { 160 | defer markSeq()() 161 | t.Bind("main", "testdata") 162 | l := t.L("zh_CN") 163 | fmt.Println(l.T("Hello, World")) // Hello, World 164 | 165 | d := t.D("main").L("zh_CN") 166 | fmt.Println(d.T("Hello, World")) // 你好,世界 167 | // Output: 168 | // Hello, World 169 | // 你好,世界 170 | } 171 | 172 | func Example_locales() { 173 | defer markSeq()() 174 | fmt.Println(t.Locales()) // [en_US] // SourceCodeLocale 175 | t.Load("testdata") 176 | fmt.Println(t.Locales()) // [en_US zh_CN] 177 | 178 | t.Bind("main", "testdata") 179 | fmt.Println(t.D("main").Locales()) // [en_US zh_CN] 180 | fmt.Println(t.D("no-such-domain").Locales()) // [en_US] 181 | // Output: 182 | // [en_US] 183 | // [en_US zh_CN] 184 | // [en_US zh_CN] 185 | // [en_US] 186 | } 187 | -------------------------------------------------------------------------------- /f/format.go: -------------------------------------------------------------------------------- 1 | package f 2 | 3 | import "fmt" 4 | 5 | // Format format a string with args, like fmt.Sprintf, 6 | // but if args tow many, not prints %!(EXTRA type=value); 7 | // and if no args, will return original string, 8 | // even it contains some verb(like %v/%d), it would not prints MISSING error. 9 | // 格式化字符串,功能同 fmt.Sprintf, 但是当参数多于占位符时, 10 | // 不会输出额外的 %!(EXTRA type=value); 11 | // 当 args 为空时直接返回原字符串(若包含格式化动词也原样返回而不会打印 MISSING 错误) 12 | func Format(format string, args ...interface{}) string { 13 | var length = len(args) 14 | if length == 0 { 15 | return format 16 | } 17 | // 原理:使用索引指定参数位置,在 args 后拼接一个空白字符串, 18 | // 然后在格式化字符串上使用 %[n]s 输出拼接的空白字符串,这样就没有多余的参数了 19 | // see fmt 包注释,或中文文档 https://studygolang.com/static/pkgdoc/pkg/fmt.htm 20 | args = append(args, "") 21 | format = fmt.Sprintf("%s%%[%d]s", format, length+1) 22 | return fmt.Sprintf(format, args...) 23 | } 24 | 25 | // DefaultPlural if n == 1 return singular form, else return plural form 26 | func DefaultPlural(msgID, msgIDPlural string, n int64, args ...interface{}) string { 27 | if n != 1 { 28 | return Format(msgIDPlural, args...) 29 | } 30 | return Format(msgID, args...) 31 | } 32 | -------------------------------------------------------------------------------- /f/format_test.go: -------------------------------------------------------------------------------- 1 | package f_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/youthlin/t/f" 8 | ) 9 | 10 | func TestFormat(t *testing.T) { 11 | got := fmt.Sprintf("%d 个苹果%[2]s", 1, "") 12 | if got != "1 个苹果" { 13 | t.Errorf("unexpected: %v", got) 14 | } 15 | type args struct { 16 | format string 17 | args []interface{} 18 | } 19 | tests := []struct { 20 | name string 21 | args args 22 | want string 23 | }{ 24 | {"empty-nil", args{"", nil}, ""}, 25 | {"empty-empty", args{"", []interface{}{}}, ""}, 26 | {"nonempty-empty", args{"hello", []interface{}{}}, "hello"}, 27 | {"one-apple", args{"one apple", []interface{}{1}}, "one apple"}, 28 | {"2-apples", args{"%d apples", []interface{}{2}}, "2 apples"}, 29 | {"verb-but-no-args", args{"%d apples", []interface{}{}}, "%d apples"}, 30 | {"args-too-few-1", args{"%s have %[1]d apples", []interface{}{1}}, "%!s(int=1) have 1 apples"}, 31 | {"args-too-few-2", args{"%s have %d apples", []interface{}{"Tom"}}, "Tom have %!d(string=) apples"}, 32 | {"position-index", args{"%[2]s have %[1]d apples", []interface{}{2, "Tom"}}, "Tom have 2 apples"}, 33 | 34 | {"", args{"%d 个苹果", []interface{}{1}}, "1 个苹果"}, 35 | } 36 | for _, tt := range tests { 37 | t.Run(tt.name, func(t *testing.T) { 38 | if got := f.Format(tt.args.format, tt.args.args...); got != tt.want { 39 | t.Errorf("Format() = %v, want %v", got, tt.want) 40 | } 41 | }) 42 | } 43 | } 44 | 45 | func TestDefaultPlural(t *testing.T) { 46 | type args struct { 47 | msgID string 48 | msgIDPlural string 49 | n int64 50 | args []interface{} 51 | } 52 | tests := []struct { 53 | name string 54 | args args 55 | want string 56 | }{ 57 | {"en-1-is-singular", args{"singular", "plural", 1, nil}, "singular"}, 58 | {"en-other-is-plural-0", args{"singular", "plural", 0, nil}, "plural"}, 59 | {"en-other-is-plural-n", args{"singular", "plural", 2, nil}, "plural"}, 60 | {"format", args{"one apple", "%d apples", 1, []interface{}{1}}, "one apple"}, 61 | {"format2", args{"one apple", "%d apples", 2, []interface{}{2}}, "2 apples"}, 62 | } 63 | for _, tt := range tests { 64 | t.Run(tt.name, func(t *testing.T) { 65 | if got := f.DefaultPlural(tt.args.msgID, tt.args.msgIDPlural, tt.args.n, tt.args.args...); got != tt.want { 66 | t.Errorf("defaultPlural() = %v, want %v", got, tt.want) 67 | } 68 | }) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /fs.go: -------------------------------------------------------------------------------- 1 | package t 2 | 3 | import ( 4 | "io/fs" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | // asFS path as FS. if path is dir return os.DirFS, or return singleFileFS 10 | func asFS(path string) fs.FS { 11 | return pathFS(path) 12 | } 13 | 14 | type pathFS string 15 | 16 | func (f pathFS) Open(name string) (fs.File, error) { 17 | path := string(f) 18 | // os.DirFS(path): Open(path + "/" + name) 19 | return os.Open(filepath.Join(path, name)) 20 | } 21 | -------------------------------------------------------------------------------- /fs_test.go: -------------------------------------------------------------------------------- 1 | package t 2 | 3 | import ( 4 | "io/fs" 5 | "os" 6 | "testing" 7 | "path/filepath" 8 | 9 | . "github.com/smartystreets/goconvey/convey" 10 | ) 11 | 12 | func Test_asFS(t *testing.T) { 13 | Convey("asFS", t, func() { 14 | Convey("dir", func() { 15 | fsys := asFS("testdata") 16 | dir, err := fsys.Open(".") 17 | So(err, ShouldBeNil) 18 | So(dir, ShouldNotBeNil) 19 | fi, err := dir.Stat() 20 | So(err, ShouldBeNil) 21 | So(fi.IsDir(), ShouldBeTrue) 22 | 23 | entry, err := fs.ReadDir(fsys, ".") 24 | So(err, ShouldBeNil) 25 | So(len(entry) > 0, ShouldBeTrue) 26 | 27 | fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error { 28 | t.Logf("dir as FS: path=%v, err=%v entry: name=%v, isDir=%v\n", path, err, d.Name(), d.IsDir()) 29 | return err 30 | }) 31 | }) 32 | Convey("file", func() { 33 | fsys := asFS("testdata/zh_CN.po") 34 | file, err := fsys.Open(".") 35 | So(err, ShouldBeNil) 36 | So(file, ShouldNotBeNil) 37 | 38 | bytes, err := fs.ReadFile(fsys, "") 39 | So(err, ShouldBeNil) 40 | So(len(bytes) > 0, ShouldBeTrue) 41 | 42 | fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error { 43 | t.Logf("file as FS: path=%v, err=%v entry: name=%v, isDir=%v\n", path, err, d.Name(), d.IsDir()) 44 | return err 45 | }) 46 | }) 47 | }) 48 | 49 | // Join 会去除后面的点 50 | t.Logf("%v", filepath.Join("testdata/zh_CN.mo", ".")) 51 | 52 | // os.DirFS Open 时,是直接用 / 连接的 53 | f := os.DirFS("testdata/zh_CN.mo") 54 | fs.WalkDir(f, ".", func(path string, d fs.DirEntry, err error) error { 55 | // path=. | d= | err=stat testdata/zh_CN.mo/.: not a directory 56 | t.Logf("path=%v | d= %v | err=%v", path, d, err) 57 | return err 58 | }) 59 | // adFS Open 时,用的 Join 60 | f = asFS("testdata/zh_CN.mo") 61 | fs.WalkDir(f, ".", func(path string, d fs.DirEntry, err error) error { 62 | // path=. | d= zh_CN.mo | err= 63 | t.Logf("path=%v | d= %v | err=%v", path, d.Name(), err) 64 | return err 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /gettext.go: -------------------------------------------------------------------------------- 1 | package t 2 | 3 | // D return a new Translations with domain 4 | func D(domain string) *Translations { return global.D(domain) } 5 | 6 | // L return a new Translations with locale 7 | func L(locale string) *Translations { return global.L(locale) } 8 | 9 | // T is a short name of gettext 10 | func T(msgID string, args ...interface{}) string { 11 | return global.X("", msgID, args...) 12 | } 13 | 14 | // N is a short name of ngettext 15 | func N(msgID, msgIDPlural string, n int, args ...interface{}) string { 16 | return global.XN64("", msgID, msgIDPlural, int64(n), args...) 17 | } 18 | 19 | // N64 is a short name of ngettext 20 | func N64(msgID, msgIDPlural string, n int64, args ...interface{}) string { 21 | return global.XN64("", msgID, msgIDPlural, n, args...) 22 | } 23 | 24 | // X is a short name of pgettext 25 | func X(msgCtxt, msgID string, args ...interface{}) string { 26 | return global.X(msgCtxt, msgID, args...) 27 | } 28 | 29 | // XN is a short name of npgettext 30 | func XN(msgCtxt, msgID, msgIDPlural string, n int, args ...interface{}) string { 31 | return global.XN64(msgCtxt, msgID, msgIDPlural, int64(n), args...) 32 | } 33 | 34 | // XN64 is a short name of npgettext 35 | func XN64(msgCtxt, msgID, msgIDPlural string, n int64, args ...interface{}) string { 36 | return global.XN64(msgCtxt, msgID, msgIDPlural, n, args...) 37 | } 38 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/youthlin/t 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/Xuanwo/go-locale v1.1.0 7 | github.com/antlr4-go/antlr/v4 v4.13.0 8 | github.com/smartystreets/goconvey v1.8.1 9 | golang.org/x/text v0.14.0 10 | ) 11 | 12 | require ( 13 | github.com/gopherjs/gopherjs v1.17.2 // indirect 14 | github.com/jtolds/gls v4.20.0+incompatible // indirect 15 | github.com/smarty/assertions v1.15.0 // indirect 16 | golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect 17 | golang.org/x/sys v0.6.0 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Xuanwo/go-locale v1.1.0 h1:51gUxhxl66oXAjI9uPGb2O0qwPECpriKQb2hl35mQkg= 2 | github.com/Xuanwo/go-locale v1.1.0/go.mod h1:UKrHoZB3FPIk9wIG2/tVSobnHgNnceGSH3Y8DY5cASs= 3 | github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= 4 | github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= 5 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 8 | github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= 9 | github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= 10 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 11 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 12 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 13 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 14 | github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= 15 | github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= 16 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 17 | github.com/smartystreets/goconvey v1.6.7/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 18 | github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= 19 | github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= 20 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 21 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 22 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 23 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 24 | golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU= 25 | golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= 26 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 27 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 28 | golang.org/x/sys v0.0.0-20211023085530-d6a326fbbf70/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 29 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 30 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 31 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 32 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 33 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 34 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 35 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 36 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 37 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 38 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 39 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 40 | -------------------------------------------------------------------------------- /go.work: -------------------------------------------------------------------------------- 1 | go 1.23.5 2 | 3 | use ( 4 | . 5 | ./cmd/xtemplate 6 | ) 7 | -------------------------------------------------------------------------------- /go.work.sum: -------------------------------------------------------------------------------- 1 | github.com/youthlin/t v0.0.9/go.mod h1:rW0l6z4hnkQSWZ2MYatEDwE3XEsQ2eep8yCD1umwvWg= 2 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | package t 2 | 3 | import ( 4 | "net/http" 5 | 6 | "golang.org/x/text/language" 7 | ) 8 | 9 | type httpConfig struct { 10 | cookieName string 11 | } 12 | type getUserLangOpt func(*httpConfig) 13 | 14 | func WithCookieName(cookieName string) getUserLangOpt { 15 | return func(hc *httpConfig) { hc.cookieName = cookieName } 16 | } 17 | 18 | const HTTPHeaderAcceptLanguage = "Accept-Language" 19 | 20 | func GetUserLang(request *http.Request, opts ...getUserLangOpt) string { 21 | cfg := &httpConfig{cookieName: "lang"} 22 | for _, opt := range opts { 23 | opt(cfg) 24 | } 25 | if cookie, err := request.Cookie(cfg.cookieName); err == nil { 26 | return cookie.Value 27 | } 28 | 29 | langs := Locales() 30 | var supported []language.Tag // 转换为 Tag 31 | for _, lang := range langs { 32 | supported = append(supported, language.Make(lang)) 33 | } 34 | matcher := language.NewMatcher(supported) // 匹配器 35 | acceptLangs := request.Header.Get(HTTPHeaderAcceptLanguage) // 用户支持的语言 36 | userAccept, _, _ := language.ParseAcceptLanguage(acceptLangs) // 转为 Tag 37 | _, index, _ := matcher.Match(userAccept...) // 找到最匹配的语言 38 | return langs[index] 39 | } 40 | -------------------------------------------------------------------------------- /http_test.go: -------------------------------------------------------------------------------- 1 | package t 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | ) 7 | 8 | func TestGetUserLang(t *testing.T) { 9 | r, _ := http.NewRequest("GET", "/", nil) 10 | r.AddCookie(&http.Cookie{Name: "language", Value: "zh_CN"}) 11 | r.Header.Add(HTTPHeaderAcceptLanguage, "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6") 12 | type args struct { 13 | request *http.Request 14 | opts []getUserLangOpt 15 | } 16 | tests := []struct { 17 | name string 18 | args args 19 | want string 20 | }{ 21 | {name: "cookie", args: args{request: r, opts: []getUserLangOpt{WithCookieName("language")}}, want: "zh_CN"}, 22 | {name: "header", args: args{request: r}, want: "en_US"}, 23 | } 24 | for _, tt := range tests { 25 | t.Run(tt.name, func(t *testing.T) { 26 | if got := GetUserLang(tt.args.request, tt.args.opts...); got != tt.want { 27 | t.Errorf("GetUserLang() = %v, want %v", got, tt.want) 28 | } 29 | }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /init.go: -------------------------------------------------------------------------------- 1 | package t 2 | 3 | import ( 4 | "io/fs" 5 | ) 6 | 7 | var global = NewTranslations() 8 | 9 | // Global return the global Translations instance 10 | func Global() *Translations { 11 | return global.clone() 12 | } 13 | 14 | // SetGlobal set the global Translations instance 15 | func SetGlobal(g *Translations) { 16 | global = g 17 | } 18 | 19 | // Load load translation from path to current domain 20 | func Load(path string) { 21 | LoadFS(asFS(path)) 22 | } 23 | 24 | // LoadFS load translation from file system to current domain 25 | func LoadFS(fsys fs.FS) { 26 | BindFS(Domain(), fsys) 27 | } 28 | 29 | // Bind bind translation from path to specified domain 30 | func Bind(domain, path string) { 31 | BindFS(domain, asFS(path)) 32 | } 33 | 34 | // BindFS bind translation from file system to specified domain 35 | func BindFS(domain string, fsys fs.FS) { 36 | global.BindFS(domain, fsys) 37 | } 38 | 39 | // Locale return current locale(it may not be used locale) 40 | func Locale() string { 41 | return global.Locale() 42 | } 43 | 44 | // MostMatchLocale return the most match language 返回最匹配的语言 45 | func MostMatchLocale() string { 46 | return global.MostMatchLocale() 47 | } 48 | 49 | // SetLocale set user language 50 | func SetLocale(locale string) { 51 | global.SetLocale(locale) 52 | } 53 | 54 | // SourceCodeLocale 返回源代码中使用的语言 55 | func SourceCodeLocale() string { 56 | return global.SourceCodeLocale() 57 | } 58 | 59 | // SetSourceCodeLocale 设置源代码的语言 60 | func SetSourceCodeLocale(locale string) { 61 | global.SetSourceCodeLocale(locale) 62 | } 63 | 64 | // UsedLocale return the actually used locale 65 | func UsedLocale() string { 66 | return global.UsedLocale() 67 | } 68 | 69 | // Locales return all supported locales of current used domain 70 | // 返回当前文本域中支持的所有语言 71 | func Locales() []string { 72 | return global.Locales() 73 | } 74 | 75 | // Domain return current domain 76 | func Domain() string { 77 | return global.Domain() 78 | } 79 | 80 | // SetDomain set current domain 81 | func SetDomain(domain string) { 82 | global.SetDomain(domain) 83 | } 84 | 85 | // HasDomain return if we have loaded the specified domain 86 | func HasDomain(domain string) bool { 87 | return global.HasDomain(domain) 88 | } 89 | 90 | // Domains return all loaded domains 91 | func Domains() []string { 92 | return global.Domains() 93 | } 94 | -------------------------------------------------------------------------------- /locale/detect.go: -------------------------------------------------------------------------------- 1 | package locale 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/Xuanwo/go-locale" 7 | "golang.org/x/text/language" 8 | ) 9 | 10 | // GetDefault get system default language in the form of: 11 | // ll or ll_CC, where 'll' an ISO 639 two-letter language code (lowercase) 12 | // and ‘CC’ is an ISO 3166 two-letter country code (uppercase) 13 | // https://www.gnu.org/software/gettext/manual/html_node/Header-Entry.html 14 | // 获取系统默认语言,返回格式是 ll 或 ll_CC,其中 ll 是小写语言二字码,CC 是大写地区二字码 15 | func GetDefault() string { 16 | tag, _ := locale.Detect() 17 | return NormalizeTag(tag) 18 | } 19 | 20 | // Normalize return ll_CC code 21 | func Normalize(lang string) string { 22 | return NormalizeTag(language.Make(lang)) 23 | } 24 | 25 | // NormalizeTag return ll_CC code 26 | func NormalizeTag(tag language.Tag) string { 27 | base, _ := tag.Base() 28 | region, _ := tag.Region() 29 | return fmt.Sprintf("%v_%v", base, region) 30 | } 31 | -------------------------------------------------------------------------------- /locale/detect_test.go: -------------------------------------------------------------------------------- 1 | package locale 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/smartystreets/goconvey/convey" 7 | "golang.org/x/text/language" 8 | ) 9 | 10 | func TestGetDefault(t *testing.T) { 11 | Convey("GetDefault", t, func() { 12 | locale := GetDefault() 13 | t.Logf("Default locale is: %v", locale) 14 | So(locale, ShouldContainSubstring, "_") 15 | So(locale, ShouldNotBeEmpty) 16 | So(language.Make(locale), ShouldNotResemble, language.Und) 17 | }) 18 | } 19 | 20 | func TestNormalize(t *testing.T) { 21 | type args struct { 22 | lang string 23 | } 24 | tests := []struct { 25 | name string 26 | args args 27 | want string 28 | }{ 29 | {"", args{""}, "en_US"}, 30 | {"", args{"en_US"}, "en_US"}, 31 | {"", args{"en-US"}, "en_US"}, 32 | {"", args{"en"}, "en_US"}, 33 | {"", args{"en_GB"}, "en_GB"}, 34 | {"", args{"en-GB"}, "en_GB"}, 35 | {"", args{"_"}, "en_US"}, 36 | 37 | {"", args{"zh"}, "zh_CN"}, 38 | {"", args{"zh_CN"}, "zh_CN"}, 39 | {"", args{"zh_CN.UTF-8"}, "zh_CN"}, 40 | {"", args{"zh_CN.UTF8"}, "zh_CN"}, 41 | {"", args{"zh-CN"}, "zh_CN"}, 42 | {"", args{"zh-Hans"}, "zh_CN"}, 43 | } 44 | for _, tt := range tests { 45 | t.Run(tt.name, func(t *testing.T) { 46 | if got := Normalize(tt.args.lang); got != tt.want { 47 | t.Errorf("Normalize() = %v, want %v", got, tt.want) 48 | } 49 | }) 50 | } 51 | } 52 | 53 | func TestNormalizeTag(t *testing.T) { 54 | type args struct { 55 | tag language.Tag 56 | } 57 | tests := []struct { 58 | name string 59 | args args 60 | want string 61 | }{ 62 | {"", args{language.English}, "en_US"}, 63 | {"", args{language.AmericanEnglish}, "en_US"}, 64 | {"", args{language.BritishEnglish}, "en_GB"}, 65 | 66 | {"", args{language.Chinese}, "zh_CN"}, 67 | {"", args{language.SimplifiedChinese}, "zh_CN"}, 68 | {"", args{language.TraditionalChinese}, "zh_TW"}, 69 | {"", args{language.Make("zh")}, "zh_CN"}, 70 | {"", args{language.Make("zh-Hans")}, "zh_CN"}, 71 | {"", args{language.Make("zh-Hant")}, "zh_TW"}, 72 | {"", args{language.Make("zh-Hant-TW")}, "zh_TW"}, 73 | {"", args{language.Make("zh-Hant-HK")}, "zh_HK"}, 74 | {"", args{language.Make("zh-Hans-HK")}, "zh_HK"}, // 香港简体 怪怪的 75 | {"", args{language.Make("zh_MO")}, "zh_MO"}, 76 | {"", args{language.Make("zh-TW")}, "zh_TW"}, 77 | {"", args{language.Make("zh_HK")}, "zh_HK"}, 78 | {"", args{language.Make("zh-SG")}, "zh_SG"}, 79 | 80 | {"", args{language.Und}, "en_US"}, 81 | {"", args{language.Make("CN")}, "en_US"}, 82 | {"", args{language.Make("")}, "en_US"}, 83 | } 84 | for _, tt := range tests { 85 | t.Run(tt.name, func(t *testing.T) { 86 | if got := NormalizeTag(tt.args.tag); got != tt.want { 87 | t.Errorf("Normalize() = %v, want %v", got, tt.want) 88 | } 89 | }) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /mark.go: -------------------------------------------------------------------------------- 1 | package t 2 | 3 | // Mark is used to mark translation texts 4 | var Mark = noop(0) 5 | 6 | // noop return msg directly, used to mark string which should be translated 7 | // so that xgettext tool can extract those strings. 8 | type noop int 9 | 10 | func (noop) T(msgID string) string { return msgID } 11 | func (noop) N(msgID string, msgIDPlural string) (string, string) { 12 | return msgID, msgIDPlural 13 | } 14 | func (noop) X(msgCtxt string, msgID string) (string, string) { 15 | return msgCtxt, msgID 16 | } 17 | func (noop) XN(msgCtxt string, msgID string, msgIDPlural string) (string, string, string) { 18 | return msgCtxt, msgID, msgIDPlural 19 | } 20 | -------------------------------------------------------------------------------- /matcher.go: -------------------------------------------------------------------------------- 1 | package t 2 | 3 | import "golang.org/x/text/language" 4 | 5 | func Match(supported []string, userAccept []string) (tag language.Tag, index int, c language.Confidence) { 6 | return MatchTag(Tags(supported), Tags(userAccept)) 7 | } 8 | 9 | func MatchTag(supportedTags []language.Tag, userAcceptTags []language.Tag) (tag language.Tag, index int, c language.Confidence) { 10 | matcher := language.NewMatcher(supportedTags) 11 | return matcher.Match(userAcceptTags...) 12 | } 13 | -------------------------------------------------------------------------------- /matcher_test.go: -------------------------------------------------------------------------------- 1 | package t 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "golang.org/x/text/language" 8 | ) 9 | 10 | func TestMatch(t *testing.T) { 11 | type args struct { 12 | supported []string 13 | userAccept []string 14 | } 15 | tests := []struct { 16 | name string 17 | args args 18 | wantTag language.Tag 19 | wantIndex int 20 | wantC language.Confidence 21 | }{ 22 | { 23 | name: "exact", // 精确匹配 24 | args: args{supported: []string{"zh-CN"}, userAccept: []string{"zh-CN"}}, 25 | wantTag: language.Make("zh-CN"), 26 | wantIndex: 0, 27 | wantC: language.Exact, 28 | }, 29 | { 30 | name: "exact2", // 精确匹配 31 | args: args{supported: []string{"en-US", "zh-CN"}, userAccept: []string{"zh-CN"}}, 32 | wantTag: language.Make("zh-CN"), 33 | wantIndex: 1, 34 | wantC: language.Exact, 35 | }, 36 | { 37 | name: "exact3", // 精确匹配 zh 默认就是指 zh-CN 38 | args: args{supported: []string{"zh"}, userAccept: []string{"zh-CN"}}, 39 | wantTag: language.Make("zh-u-rg-cnzzzz"), // 但返回的 tag 和 support / userAccept 都不同(带 -u 后缀指定了地区 rg=region?) 40 | wantIndex: 0, // 使用 supported 的第 0 个,即 zh 41 | wantC: language.Exact, 42 | }, 43 | { 44 | name: "exact4", // 精确匹配 zh-Hant 繁体中文默认就是指 中国台湾的繁体中文 45 | args: args{supported: []string{"zh-Hant"}, userAccept: []string{"zh-TW"}}, 46 | wantTag: language.Make("zh-Hant-u-rg-twzzzz"), 47 | wantIndex: 0, 48 | wantC: language.Exact, 49 | }, 50 | { 51 | name: "exact5", // 精确匹配 zh-HK 中国香港 默认就是 Hant 繁体 52 | args: args{supported: []string{"zh-Hant-HK"}, userAccept: []string{"zh-HK"}}, 53 | wantTag: language.Make("zh-Hant-HK"), 54 | wantIndex: 0, 55 | wantC: language.Exact, 56 | }, 57 | { 58 | name: "exact6", // 精确匹配 en 默认就是指美国英语 59 | args: args{supported: []string{"en"}, userAccept: []string{"en-US"}}, 60 | wantTag: language.Make("en-u-rg-uszzzz"), 61 | wantIndex: 0, 62 | wantC: language.Exact, 63 | }, 64 | { 65 | name: "exact7", // 精确匹配 66 | args: args{supported: []string{"en"}, userAccept: []string{"en"}}, 67 | wantTag: language.Make("en"), 68 | wantIndex: 0, 69 | wantC: language.Exact, 70 | }, 71 | 72 | { 73 | name: "high", // 高度匹配 zh-HK中国香港也使用 zh-Hant 繁体中文(未指明地区时其实默认是指中国台湾) 74 | args: args{supported: []string{"zh-Hant"}, userAccept: []string{"zh-HK"}}, 75 | wantTag: language.Make("zh-Hant-u-rg-hkzzzz"), 76 | wantIndex: 0, 77 | wantC: language.High, 78 | }, 79 | { 80 | name: "high2", // 高度匹配 提供了 zh-CN中国大陆,zh-MO中国澳门, zh-HK中国香港 更匹配同样使用 Hant繁体 的澳门 81 | args: args{supported: []string{"zh-CN", "zh-MO"}, userAccept: []string{"zh-HK"}}, 82 | wantTag: language.Make("zh-MO-u-rg-hkzzzz"), 83 | wantIndex: 1, 84 | wantC: language.High, 85 | }, 86 | { 87 | name: "high3", // 高度匹配 en 默认指美国英语 en-GB 英国也是英语 88 | args: args{supported: []string{"en"}, userAccept: []string{"en-GB"}}, 89 | wantTag: language.Make("en-u-rg-gbzzzz"), 90 | wantIndex: 0, 91 | wantC: language.High, 92 | }, 93 | 94 | { 95 | name: "low", // 也算找到了最接近的 96 | args: args{supported: []string{"en", "zh"}, userAccept: []string{"zh-HK"}}, 97 | wantTag: language.Make("zh-u-rg-hkzzzz"), 98 | wantIndex: 1, 99 | wantC: language.Low, 100 | }, 101 | { 102 | name: "low2", // 也算找到了最接近的 zh-CN中国大陆,从 英文,繁体中文(中国台湾) 中选择 繁体中文 最接近 103 | args: args{supported: []string{"en", "zh-TW"}, userAccept: []string{"zh-CN"}}, 104 | wantTag: language.Make("zh-TW-u-rg-cnzzzz"), 105 | wantIndex: 1, 106 | wantC: language.Low, 107 | }, 108 | 109 | { 110 | name: "no", // 不匹配 111 | args: args{supported: []string{"en"}, userAccept: []string{"zh-HK"}}, 112 | wantTag: language.Make("en-u-rg-hkzzzz"), 113 | wantIndex: 0, 114 | wantC: language.No, 115 | }, 116 | { 117 | name: "no1", // 不匹配 118 | args: args{supported: []string{"en-US"}, userAccept: []string{"zh-HK"}}, 119 | wantTag: language.Make("en-US-u-rg-hkzzzz"), 120 | wantIndex: 0, 121 | wantC: language.No, 122 | }, 123 | { 124 | name: "no2", // 不匹配 125 | args: args{supported: []string{"en"}, userAccept: []string{"zh"}}, 126 | wantTag: language.Make("en"), 127 | wantIndex: 0, 128 | wantC: language.No, 129 | }, 130 | { 131 | name: "no3", // 不匹配 132 | args: args{supported: []string{"en"}, userAccept: []string{"zh-Hans"}}, 133 | wantTag: language.Make("en"), 134 | wantIndex: 0, 135 | wantC: language.No, 136 | }, 137 | { 138 | name: "no4", // 不匹配 139 | args: args{supported: []string{"ja"}, userAccept: []string{"zh-CN"}}, 140 | wantTag: language.Make("ja-u-rg-cnzzzz"), 141 | wantIndex: 0, 142 | wantC: language.No, 143 | }, 144 | { 145 | name: "no5", // 不匹配 146 | args: args{supported: []string{"ja", "en"}, userAccept: []string{"zh"}}, 147 | wantTag: language.Make("ja"), // 不匹配时都是返回第 0 个 supported 148 | wantIndex: 0, 149 | wantC: language.No, 150 | }, 151 | { 152 | name: "no6", // 不匹配 153 | args: args{supported: []string{"en", "jp"}, userAccept: []string{"zh"}}, 154 | wantTag: language.Make("en"), 155 | wantIndex: 0, 156 | wantC: language.No, 157 | }, 158 | } 159 | 160 | for _, tt := range tests { 161 | t.Run(tt.name, func(t *testing.T) { 162 | gotTag, gotIndex, gotC := Match(tt.args.supported, tt.args.userAccept) 163 | if !reflect.DeepEqual(gotTag, tt.wantTag) { 164 | t.Errorf("Match() gotTag = %v, want %v", gotTag, tt.wantTag) 165 | } 166 | if gotIndex != tt.wantIndex { 167 | t.Errorf("Match() gotIndex = %v, want %v", gotIndex, tt.wantIndex) 168 | } 169 | if !reflect.DeepEqual(gotC, tt.wantC) { 170 | t.Errorf("Match() gotC = %v, want %v", gotC, tt.wantC) 171 | } 172 | }) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /plurals/common.go: -------------------------------------------------------------------------------- 1 | package plurals 2 | 3 | func i(b bool) int64 { 4 | if b { 5 | return 1 6 | } 7 | return 0 8 | } 9 | func _if(b bool, t, f int64) int64 { 10 | if b { 11 | return t 12 | } 13 | return f 14 | } 15 | 16 | // commons holds some commonly used expression from gnu site 17 | // https://www.gnu.org/software/gettext/manual/html_node/Plural-forms.html#index-plural_002c-in-a-PO-file-header 18 | var commons = map[string]func(n int64) int64{ 19 | // Asian family: Japanese, Vietnamese, Korean 20 | // Tai-Kadai family: Thai 21 | "0": func(n int64) int64 { return 0 }, 22 | // Germanic family: English, German, Dutch, Swedish, Danish, Norwegian, Faroese 23 | // Romanic family: Spanish, Portuguese, Italian 24 | // Latin/Greek family: Greek 25 | // Slavic family: Bulgarian 26 | // Finno-Ugric family: Finnish, Estonian 27 | // Semitic family: Hebrew 28 | // Austronesian family: Bahasa Indonesian 29 | // Artificial: Esperanto 30 | "n!=1": func(n int64) int64 { return i(n != 1) }, 31 | // Romanic family: Brazilian Portuguese, French 32 | "n>1": func(n int64) int64 { return i(n > 1) }, 33 | // Baltic family: Latvian 34 | "n%10==1&&n%100!=11?0:n!=0?1:2": func(n int64) int64 { 35 | return _if(n%10 == 1 && n%100 != 11, 0, _if(n != 0, 1, 2)) 36 | }, 37 | // Celtic: Gaeilge (Irish) 38 | "n==1?0:n==2?1:2": func(n int64) int64 { return _if(n == 1, 0, _if(n == 2, 1, 2)) }, 39 | // Romanic family: Romanian 40 | "n==1?0:(n==0||(n%100>0&&n%100<20))?1:2": func(n int64) int64 { 41 | return _if(n == 1, 0, _if(n == 0 || (n%100 > 0 && n%100 < 20), 1, 2)) 42 | }, 43 | // Baltic family: Lithuanian 44 | "n%10==1&&n%100!=11?0:n%10>=2&&(n%100<10||n%100>=20)?1:2": func(n int64) int64 { 45 | return _if(n%10 == 1 && n%100 != 11, 0, _if(n%10 >= 2 && (n%100 < 10 || n%100 >= 20), 1, 2)) 46 | }, 47 | // Slavic family: Russian, Ukrainian, Belarusian, Serbian, Croatian 48 | "n%10==1&&n%100!=11?0:n%10>=2&&n%10<=4&&(n%100<10||n%100>=20)?1:2": func(n int64) int64 { 49 | return _if(n%10 == 1 && n%100 != 11, 0, _if(n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20), 1, 2)) 50 | }, 51 | // Slavic family: Czech, Slovak 52 | "(n==1)?0:(n>=2&&n<=4)?1:2": func(n int64) int64 { 53 | return _if(n == 1, 0, _if(n >= 2 && n <= 4, 1, 2)) 54 | }, 55 | // Slavic family: Polish 56 | "n==1?0:n%10>=2&&n%10<=4&&(n%100<10||n%100>=20)?1:2": func(n int64) int64 { 57 | return _if(n == 1, 0, _if(n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20), 1, 2)) 58 | }, 59 | // Slavic family: Slovenian 60 | "n%100==1?0:n%100==2?1:n%100==3||n%100==4?2:3": func(n int64) int64 { 61 | return _if(n%100 == 1, 0, _if(n%100 == 2, 1, _if(n%100 == 3 || n%100 == 4, 2, 3))) 62 | }, 63 | // Afroasiatic family: Arabic 64 | "n==0?0:n==1?1:n==2?2:n%100>=3&&n%100<=10?3:n%100>=11?4:5": func(n int64) int64 { 65 | return _if(n == 0, 0, 66 | _if(n == 1, 1, 67 | _if(n == 2, 2, 68 | _if(n%100 >= 3 && n%100 <= 10, 3, 69 | _if(n%100 >= 11, 4, 5))))) 70 | }, 71 | } 72 | -------------------------------------------------------------------------------- /plurals/debug.go: -------------------------------------------------------------------------------- 1 | package plurals 2 | 3 | import "context" 4 | 5 | // ctxKey custom type using by context.WithValue as a key 6 | type ctxKey string 7 | 8 | // ctxKeyDebug a debug key instace 9 | var ctxKeyDebug = ctxKey("debug") 10 | 11 | // DebugCtx return a ctx that with a debug key, which may print how the exp calculates. 12 | func DebugCtx(ctx context.Context) context.Context { 13 | return context.WithValue(ctx, ctxKeyDebug, true) 14 | } 15 | -------------------------------------------------------------------------------- /plurals/err.go: -------------------------------------------------------------------------------- 1 | package plurals 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/antlr4-go/antlr/v4" 7 | "github.com/youthlin/t/errors" 8 | ) 9 | 10 | // ErrAntlr is a error when antlr process fails 11 | var ErrAntlr = fmt.Errorf("err antlr") 12 | 13 | // errorListener handler lexer/parser errors 14 | type errorListener struct { 15 | *antlr.DefaultErrorListener 16 | err error 17 | } 18 | 19 | func (d *errorListener) SyntaxError(recognizer antlr.Recognizer, offendingSymbol interface{}, line, column int, msg string, e antlr.RecognitionException) { 20 | d.addError(errors.Errorf("SyntaxError(line %v:%v): %v", line, column, msg)) // SyntaxError 语法错误 21 | } 22 | 23 | func (d *errorListener) ReportAmbiguity(recognizer antlr.Parser, dfa *antlr.DFA, startIndex, stopIndex int, exact bool, ambigAlts *antlr.BitSet, configs *antlr.ATNConfigSet) { 24 | d.addError(errors.Errorf("ReportAmbiguity")) // Ambiguity 歧义 25 | } 26 | 27 | func (d *errorListener) ReportAttemptingFullContext(recognizer antlr.Parser, dfa *antlr.DFA, startIndex, stopIndex int, conflictingAlts *antlr.BitSet, configs *antlr.ATNConfigSet) { 28 | d.addError(errors.Errorf("ReportAttemptingFullContext")) // SLL 冲突 29 | } 30 | 31 | func (d *errorListener) ReportContextSensitivity(recognizer antlr.Parser, dfa *antlr.DFA, startIndex, stopIndex, prediction int, configs *antlr.ATNConfigSet) { 32 | d.addError(errors.Errorf("ReportContextSensitivity")) // 上下文相关 33 | } 34 | 35 | func (d *errorListener) addError(err error) { 36 | if d.err == nil { 37 | d.err = ErrAntlr 38 | } 39 | d.err = errors.WithSecondaryError(d.err, err) 40 | } 41 | -------------------------------------------------------------------------------- /plurals/err_test.go: -------------------------------------------------------------------------------- 1 | package plurals 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | . "github.com/smartystreets/goconvey/convey" 8 | "github.com/youthlin/t/errors" 9 | ) 10 | 11 | func Test_errorListener_addError(t *testing.T) { 12 | Convey("add-err", t, func() { 13 | var err = errors.Errorf("abc=%v", 1) 14 | e := new(errorListener) 15 | e.addError(fmt.Errorf("fmt-error")) 16 | e.addError(errors.Errorf("errors-new")) 17 | e.addError(errors.Wrapf(err, "wrap message")) 18 | t.Logf("err=%+v", e.err) 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /plurals/exp.go: -------------------------------------------------------------------------------- 1 | package plurals 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/antlr4-go/antlr/v4" 10 | "github.com/youthlin/t/errors" 11 | "github.com/youthlin/t/plurals/parser" 12 | ) 13 | 14 | // Eval 传入复数表达式,返回 n 应该使用哪种复数形式 15 | func Eval(ctx context.Context, exp string, n int64) (result int64, err error) { 16 | defer func() { 17 | if e := recover(); e != nil { 18 | err = errors.Errorf("unexpected error: %v", e) 19 | } 20 | }() 21 | 22 | // 1 常见表达式直接走函数 23 | commonExp := strings.ReplaceAll(exp, " ", "") 24 | fun, ok := commons[commonExp] 25 | if ok { 26 | return fun(n), nil 27 | } 28 | 29 | // 2 词法解析 30 | input := antlr.NewInputStream(exp) 31 | lexer := parser.NewpluralLexer(input) 32 | errListener := new(errorListener) 33 | lexer.RemoveErrorListeners() 34 | lexer.AddErrorListener(errListener) 35 | 36 | // 3 语法树生成 37 | stream := antlr.NewCommonTokenStream(lexer, antlr.TokenDefaultChannel) 38 | p := parser.NewpluralParser(stream) 39 | p.RemoveErrorListeners() 40 | p.AddErrorListener(errListener) 41 | 42 | // 4 遍历语法树计算表达式 43 | l := newListener(ctx, n) 44 | tree := p.Start_() 45 | antlr.ParseTreeWalkerDefault.Walk(l, tree) 46 | return l.result, errListener.err 47 | } 48 | 49 | // c syntax accept int(0) as false, none-zero as true 50 | const ( 51 | _TRUE = 1 52 | _FALSE = 0 53 | ) 54 | 55 | // myListener is a listener when walk the ast can be do something 56 | // see https://blog.gopheracademy.com/advent-2017/parsing-with-antlr4-and-go/ 57 | // 遍历语法树时操作一个栈来计算表达式 58 | type myListener struct { 59 | // 实现这个父类上感兴趣的方法(相当于一个 adaptor) 60 | *parser.BasepluralListener 61 | stack Int64Stack // 操作数栈 62 | ctx context.Context // 外部传入的 ctx, 用于 debug 63 | n int64 // 参数 n 64 | result int64 // 结果 65 | } 66 | 67 | func newListener(ctx context.Context, n int64) *myListener { 68 | return &myListener{ 69 | ctx: ctx, 70 | n: n, 71 | stack: make(Int64Stack, 0), 72 | } 73 | } 74 | 75 | // ExitStart override 76 | func (s *myListener) ExitStart(ctx *parser.StartContext) { 77 | s.result, _ = s.stack.Pop() 78 | } 79 | 80 | // ExitExp override 81 | func (s *myListener) ExitExp(ctx *parser.ExpContext) { 82 | var ( 83 | prefix = ctx.GetPrefix() 84 | bop = ctx.GetBop() 85 | postfix = ctx.GetPostfix() 86 | ) 87 | s.handleExpPrefix(prefix) 88 | s.handleExpBop(bop) 89 | s.handleExpPostfix(postfix) 90 | // prefix, bop, postfix all nil, then it's a primary rule 91 | } 92 | 93 | func (s *myListener) handleExpPrefix(prefix antlr.Token) { 94 | if prefix == nil { 95 | return 96 | } 97 | prefixText := prefix.GetText() 98 | s.debug("prefix: %v stack=%v", prefixText, s.stack) 99 | switch prefixText { 100 | case "+": // no-op 101 | case "-": 102 | num, _ := s.stack.Pop() 103 | s.stack.Push(-num) 104 | case "++": 105 | num, _ := s.stack.Pop() 106 | s.stack.Push(num + 1) 107 | case "--": 108 | num, _ := s.stack.Pop() 109 | s.stack.Push(num - 1) 110 | case "~": 111 | num, _ := s.stack.Pop() 112 | s.stack.Push(^num) // go 使用 ^ 表示按位取反 113 | case "!": 114 | num, _ := s.stack.Pop() 115 | if num == _TRUE { 116 | s.stack.Push(_FALSE) 117 | } else { 118 | s.stack.Push(_TRUE) 119 | } 120 | default: 121 | panic("assert error: unexpected text(prefix): " + prefixText) 122 | } 123 | } 124 | 125 | func (s *myListener) handleExpBop(bop antlr.Token) { 126 | if bop == nil { 127 | return 128 | } 129 | bopText := bop.GetText() 130 | s.debug("bop : %v stack=%v", bopText, s.stack) 131 | switch bopText { 132 | case "*": 133 | var ( 134 | right, _ = s.stack.Pop() 135 | left, _ = s.stack.Pop() 136 | ) 137 | s.stack.Push(left * right) 138 | case "/": 139 | var ( 140 | right, _ = s.stack.Pop() 141 | left, _ = s.stack.Pop() 142 | ) 143 | s.stack.Push(left / right) 144 | case "%": 145 | var ( 146 | right, _ = s.stack.Pop() 147 | left, _ = s.stack.Pop() 148 | ) 149 | s.stack.Push(left % right) 150 | case "+": 151 | var ( 152 | right, _ = s.stack.Pop() 153 | left, _ = s.stack.Pop() 154 | ) 155 | s.stack.Push(left + right) 156 | case "-": 157 | var ( 158 | right, _ = s.stack.Pop() 159 | left, _ = s.stack.Pop() 160 | ) 161 | s.stack.Push(left - right) 162 | case ">>": 163 | var ( 164 | right, _ = s.stack.Pop() 165 | left, _ = s.stack.Pop() 166 | ) 167 | s.stack.Push(left >> right) 168 | case "<<": 169 | var ( 170 | right, _ = s.stack.Pop() 171 | left, _ = s.stack.Pop() 172 | ) 173 | s.stack.Push(left << right) 174 | case ">": 175 | var ( 176 | right, _ = s.stack.Pop() 177 | left, _ = s.stack.Pop() 178 | ) 179 | if left > right { 180 | s.stack.Push(_TRUE) 181 | } else { 182 | s.stack.Push(_FALSE) 183 | } 184 | case "<": 185 | var ( 186 | right, _ = s.stack.Pop() 187 | left, _ = s.stack.Pop() 188 | ) 189 | if left < right { 190 | s.stack.Push(_TRUE) 191 | } else { 192 | s.stack.Push(_FALSE) 193 | } 194 | case ">=": 195 | var ( 196 | right, _ = s.stack.Pop() 197 | left, _ = s.stack.Pop() 198 | ) 199 | if left >= right { 200 | s.stack.Push(_TRUE) 201 | } else { 202 | s.stack.Push(_FALSE) 203 | } 204 | case "<=": 205 | var ( 206 | right, _ = s.stack.Pop() 207 | left, _ = s.stack.Pop() 208 | ) 209 | if left <= right { 210 | s.stack.Push(_TRUE) 211 | } else { 212 | s.stack.Push(_FALSE) 213 | } 214 | case "==": 215 | var ( 216 | right, _ = s.stack.Pop() 217 | left, _ = s.stack.Pop() 218 | ) 219 | if left == right { 220 | s.stack.Push(_TRUE) 221 | } else { 222 | s.stack.Push(_FALSE) 223 | } 224 | case "!=": 225 | var ( 226 | right, _ = s.stack.Pop() 227 | left, _ = s.stack.Pop() 228 | ) 229 | if left != right { 230 | s.stack.Push(_TRUE) 231 | } else { 232 | s.stack.Push(_FALSE) 233 | } 234 | case "&": 235 | var ( 236 | right, _ = s.stack.Pop() 237 | left, _ = s.stack.Pop() 238 | ) 239 | s.stack.Push(left & right) 240 | case "|": 241 | var ( 242 | right, _ = s.stack.Pop() 243 | left, _ = s.stack.Pop() 244 | ) 245 | s.stack.Push(left | right) 246 | case "^": 247 | var ( 248 | right, _ = s.stack.Pop() 249 | left, _ = s.stack.Pop() 250 | ) 251 | s.stack.Push(left & right) // 作为二元操作符,是异或 252 | case "&&": 253 | var ( 254 | right, _ = s.stack.Pop() 255 | left, _ = s.stack.Pop() 256 | ) 257 | if left == _TRUE && right == _TRUE { 258 | s.stack.Push(_TRUE) 259 | } else { 260 | s.stack.Push(_FALSE) 261 | } 262 | case "||": 263 | var ( 264 | right, _ = s.stack.Pop() 265 | left, _ = s.stack.Pop() 266 | ) 267 | if left == _TRUE || right == _TRUE { 268 | s.stack.Push(_TRUE) 269 | } else { 270 | s.stack.Push(_FALSE) 271 | } 272 | case "?": 273 | s.debug("%v", s.stack) 274 | var ( 275 | right, _ = s.stack.Pop() 276 | left, _ = s.stack.Pop() 277 | cond, _ = s.stack.Pop() 278 | ) 279 | if cond == _TRUE { 280 | s.stack.Push(left) 281 | } else { 282 | s.stack.Push(right) 283 | } 284 | default: 285 | panic("assert error: unexpected text(bop): " + bopText) 286 | } 287 | s.debug("bop done stack=%v", s.stack) 288 | } 289 | 290 | func (s *myListener) handleExpPostfix(postfix antlr.Token) { 291 | if postfix == nil { 292 | return 293 | } 294 | postText := postfix.GetText() 295 | s.debug("postfix: %v stack=%v", postText, s.stack) 296 | switch postText { 297 | case "++": 298 | num, _ := s.stack.Pop() 299 | s.stack.Push(num + 1) 300 | case "--": 301 | num, _ := s.stack.Pop() 302 | s.stack.Push(num - 1) 303 | default: 304 | panic("assert error: unexpected text(postfix): " + postText) 305 | } 306 | } 307 | 308 | // ExitPrimary override 309 | func (s *myListener) ExitPrimary(ctx *parser.PrimaryContext) { 310 | // primary: '(' exp ')' | 'n' | INT; 311 | start := ctx.GetStart() 312 | switch start.GetText() { 313 | case "n": 314 | s.stack.Push(s.n) // the only variable n 315 | case "(": 316 | // 不用出入栈 317 | s.debug("primary: (exp) stack=%v", s.stack) 318 | default: 319 | num := ctx.GetText() 320 | iNum, err := strconv.ParseInt(num, 10, 64) 321 | if err != nil { 322 | panic("assert error: not a number: " + num) 323 | } 324 | s.stack.Push(iNum) 325 | } 326 | s.debug("primary: %v stack=%v", ctx.GetText(), s.stack) 327 | } 328 | 329 | // debug if ctx has a debug key, then print msg. 330 | // see DebugContext 331 | func (s *myListener) debug(format string, args ...interface{}) { 332 | if s.ctx.Value(ctxKeyDebug) != nil { 333 | fmt.Printf(format+"\n", args...) 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /plurals/exp_test.go: -------------------------------------------------------------------------------- 1 | package plurals 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | . "github.com/smartystreets/goconvey/convey" 9 | ) 10 | 11 | var ctx = context.Background() 12 | 13 | type args struct { 14 | ctx context.Context 15 | exp string 16 | n int64 17 | } 18 | type tCase struct { 19 | name string 20 | args args 21 | want int64 22 | wantErr bool 23 | } 24 | 25 | func okCase() []tCase { 26 | tests := []tCase{ 27 | // https://www.gnu.org/software/gettext/manual/html_node/Plural-forms.html#index-plural-form-formulas 28 | 29 | // 中日韩越泰 等 30 | {"#0-only-one-form: 亚洲语言", args{ctx, "0", 0}, 0, false}, 31 | {"#1-only-one-form: 亚洲语言", args{ctx, "0", 1}, 0, false}, 32 | {"#2-only-one-form: 亚洲语言", args{ctx, "0", 2}, 0, false}, 33 | // 英/德/丹麦/西班牙/意大利 等 34 | {"#0-two-forms: 单数只有1", args{ctx, "n != 1", 0}, 1, false}, 35 | {"#1-two-forms: 单数只有1", args{ctx, "n != 1", 1}, 0, false}, 36 | {"#2-two-forms: 单数只有1", args{ctx, "n != 1", 2}, 1, false}, 37 | // 法 38 | {"#1-two-forms: 01是单数", args{ctx, "n > 1", 1}, 0, false}, 39 | {"#2-two-forms: 01是单数", args{ctx, "n > 1", 2}, 1, false}, 40 | {"#0-two-forms: 01是单数", args{ctx, "n > 1", 0}, 0, false}, 41 | // 三种复数,1、2特殊 Gaeilge (Irish) 爱尔兰语 42 | {"#0-three-forms: 12特殊", args{ctx, "n==1 ? 0 : n==2 ? 1 : 2", 0}, 2, false}, 43 | {"#1-three-forms: 12特殊", args{ctx, "n==1 ? 0 : n==2 ? 1 : 2", 1}, 0, false}, 44 | {"#2-three-forms: 12特殊", args{ctx, "n==1 ? 0 : n==2 ? 1 : 2", 2}, 1, false}, 45 | {"#3-three-forms: 12特殊", args{ctx, "n==1 ? 0 : n==2 ? 1 : 2", 3}, 2, false}, 46 | } 47 | 48 | // 拉脱维亚语 Latvian 三种复数,0特殊 49 | for i := int64(0); i < 215; i++ { 50 | tests = append(tests, 51 | tCase{fmt.Sprintf("test-case-3-#%v", i), 52 | args{ctx, "n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2", i}, 53 | func(n int64) int64 { 54 | // n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2 55 | if n%10 == 1 && n%100 != 11 { 56 | return 0 57 | } 58 | if n != 0 { 59 | return 1 60 | } 61 | return 2 62 | }(i), false}, 63 | ) 64 | } 65 | // 三种复数,00 [2-9][0-9] Romanian 罗马尼亚 66 | for i := int64(0); i < 215; i++ { 67 | tests = append(tests, 68 | tCase{fmt.Sprintf("test-case-4-#%v", i), 69 | args{ctx, "n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2", i}, 70 | func(n int64) int64 { 71 | // n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2 72 | if n == 1 { 73 | return 0 74 | } 75 | if n == 0 || (n%100 > 0 && n%100 < 20) { 76 | return 1 77 | } 78 | return 2 79 | }(i), false}, 80 | ) 81 | } 82 | // 三种复数,1[2-9] 立陶宛 Lithuanian 83 | for i := int64(0); i < 215; i++ { 84 | tests = append(tests, 85 | tCase{fmt.Sprintf("test-case-5-#%v", i), 86 | args{ctx, "n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2", i}, 87 | func(n int64) int64 { 88 | // n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2 89 | if n%10 == 1 && n%100 != 11 { 90 | return 0 91 | } 92 | if n%10 >= 2 && (n%100 < 10 || n%100 >= 20) { 93 | return 1 94 | } 95 | return 2 96 | }(i), false}, 97 | ) 98 | } 99 | // 三种复数,1234结尾的特殊,但不是 1[1-4] Russian, Ukrainian, Belarusian, Serbian, Croatian 100 | for i := int64(0); i < 215; i++ { 101 | tests = append(tests, 102 | tCase{fmt.Sprintf("test-case-6-#%v", i), 103 | args{ctx, "n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2", i}, 104 | func(n int64) int64 { 105 | // n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2 106 | if n%10 == 1 && n%100 != 11 { 107 | return 0 108 | } 109 | if n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20) { 110 | return 1 111 | } 112 | return 2 113 | }(i), false}, 114 | ) 115 | } 116 | // 三种复数,1234结尾的特殊 Czech, Slovak 117 | for i := int64(0); i < 215; i++ { 118 | tests = append(tests, 119 | tCase{fmt.Sprintf("test-case-7-#%v", i), 120 | args{ctx, "(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2", i}, 121 | func(n int64) int64 { 122 | // (n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2 123 | if n == 1 { 124 | return 0 125 | } 126 | if n >= 2 && n <= 4 { 127 | return 1 128 | } 129 | return 2 130 | }(i), false}, 131 | ) 132 | } 133 | // 三种复数,Three forms, special case for one and some numbers ending in 2, 3, or 4 134 | // Polish 135 | for i := int64(0); i < 215; i++ { 136 | tests = append(tests, 137 | tCase{fmt.Sprintf("test-case-8-#%v", i), 138 | args{ctx, "n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2", i}, 139 | func(n int64) int64 { 140 | // n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2 141 | if n == 1 { 142 | return 0 143 | } 144 | if n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20) { 145 | return 1 146 | } 147 | return 2 148 | }(i), false}, 149 | ) 150 | } 151 | // Four forms, special case for one and all numbers ending in 02, 03, or 04 152 | // Slovenian 153 | for i := int64(0); i < 215; i++ { 154 | tests = append(tests, 155 | tCase{fmt.Sprintf("test-case-9-#%v", i), 156 | args{ctx, "n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3", i}, 157 | func(n int64) int64 { 158 | //n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3 159 | if n%100 == 1 { 160 | return 0 161 | } 162 | if n%100 == 2 { 163 | return 1 164 | } 165 | if n%100 == 3 || n%100 == 4 { 166 | return 2 167 | } 168 | return 3 169 | }(i), false}, 170 | ) 171 | } 172 | // Six forms, special cases for one, two, all numbers ending in 02, 03, … 10, all numbers ending in 11 … 99, and others 173 | // Arabic 174 | for i := int64(0); i < 215; i++ { 175 | tests = append(tests, 176 | tCase{fmt.Sprintf("test-case-10-#%v", i), 177 | args{ctx, " n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5", i}, 178 | func(n int64) int64 { 179 | if n == 0 { 180 | return 0 181 | } 182 | if n == 1 { 183 | return 1 184 | } 185 | if n == 2 { 186 | return 2 187 | } 188 | if n%100 >= 3 && n%100 <= 10 { 189 | return 3 190 | } 191 | if n%100 >= 11 { 192 | return 4 193 | } 194 | return 5 195 | }(i), false}, 196 | ) 197 | } 198 | return tests 199 | } 200 | 201 | func myCase() []tCase { 202 | return []tCase{ 203 | {"custom-case0", args{ctx, "n++", 0}, 1, false}, 204 | {"custom-case1", args{ctx, "n++", 1}, 2, false}, 205 | {"custom-case2", args{ctx, "n--", 1}, 0, false}, 206 | {"custom-case3", args{ctx, "+n", 1}, 1, false}, 207 | {"custom-case4", args{ctx, "-n", 1}, -1, false}, 208 | {"custom-case5", args{ctx, "++n", 1}, 2, false}, 209 | {"custom-case6", args{ctx, "--n", 1}, 0, false}, 210 | {"custom-case7", args{ctx, "~n", 1}, -2, false}, 211 | {"custom-case8", args{ctx, "!n", 1}, 0, false}, 212 | {"custom-case9", args{ctx, "n*1", 1}, 1, false}, 213 | {"custom-case10", args{ctx, "n/1", 1}, 1, false}, 214 | {"custom-case11", args{ctx, "n%1", 1}, 0, false}, 215 | {"custom-case12", args{DebugCtx(ctx), "n+1", 0}, 1, false}, 216 | {"custom-case13", args{ctx, "n-1", 1}, 0, false}, 217 | {"custom-case14", args{ctx, "n>>1", 1}, 0, false}, 218 | {"custom-case15", args{ctx, "n<<1", 1}, 2, false}, 219 | {"custom-case16", args{ctx, "n<1", 1}, 0, false}, 220 | {"custom-case17", args{ctx, "n>1", 1}, 0, false}, 221 | {"custom-case18", args{ctx, "n>=1", 1}, 1, false}, 222 | {"custom-case19", args{ctx, "n<=1", 1}, 1, false}, 223 | {"custom-case20", args{ctx, "n==1", 1}, 1, false}, 224 | {"custom-case21", args{ctx, "n!=1", 1}, 0, false}, 225 | {"custom-case22", args{ctx, "n&1", 1}, 1, false}, 226 | {"custom-case23", args{ctx, "n|1", 1}, 1, false}, 227 | {"custom-case24", args{ctx, "n^1", 1}, 1, false}, 228 | {"custom-case25", args{ctx, "n||1", 0}, 1, false}, 229 | {"custom-case26", args{ctx, "n&&1", 0}, 0, false}, 230 | {"custom-case27", args{ctx, "n&&1", 1}, 1, false}, 231 | {"custom-case28", args{ctx, "n==1?1:0", 1}, 1, false}, 232 | {"custom-case29", args{ctx, "n==1?1:0", 2}, 0, false}, 233 | {"custom-case30", args{ctx, "0", 100}, 0, false}, 234 | {"custom-case31", args{ctx, "n", 100}, 100, false}, 235 | {"custom-case32", args{ctx, "(n)", 100}, 100, false}, 236 | {"custom-case33", args{ctx, "!1", 100}, 0, false}, 237 | } 238 | } 239 | func errCase() []tCase { 240 | return []tCase{ 241 | {"err0", args{}, 0, true}, // SyntaxError: EOF 242 | {"err1", args{ctx, `nn`, 1}, 1, false}, // todo why 这个不太符合预期。应该返回错误才对 243 | } 244 | } 245 | 246 | func TestEval(t *testing.T) { 247 | tests := []tCase{} 248 | tests = append(tests, okCase()...) 249 | tests = append(tests, myCase()...) 250 | tests = append(tests, errCase()...) 251 | for _, tt := range tests { 252 | t.Run(tt.name, func(t *testing.T) { 253 | got, err := Eval(tt.args.ctx, tt.args.exp, tt.args.n) 254 | // if err != nil { 255 | // t.Logf("err=%+v", err) 256 | // } 257 | if (err != nil) != tt.wantErr { 258 | t.Errorf("Eval() error = %v, wantErr %v", err, tt.wantErr) 259 | return 260 | } 261 | if got != tt.want { 262 | t.Errorf("Eval() = %v, want %v", got, tt.want) 263 | } 264 | }) 265 | } 266 | } 267 | 268 | func TestEval2(t *testing.T) { 269 | Convey("Eval", t, func() { 270 | Convey("empty-exp", func() { 271 | _, err := Eval(DebugCtx(ctx), ``, 0) 272 | t.Logf("%v", err) 273 | So(err, ShouldNotBeNil) 274 | }) 275 | }) 276 | } 277 | -------------------------------------------------------------------------------- /plurals/parser/plural_base_listener.go: -------------------------------------------------------------------------------- 1 | // Code generated from plural.g4 by ANTLR 4.13.1. DO NOT EDIT. 2 | 3 | package parser // plural 4 | 5 | import "github.com/antlr4-go/antlr/v4" 6 | 7 | // BasepluralListener is a complete listener for a parse tree produced by pluralParser. 8 | type BasepluralListener struct{} 9 | 10 | var _ pluralListener = &BasepluralListener{} 11 | 12 | // VisitTerminal is called when a terminal node is visited. 13 | func (s *BasepluralListener) VisitTerminal(node antlr.TerminalNode) {} 14 | 15 | // VisitErrorNode is called when an error node is visited. 16 | func (s *BasepluralListener) VisitErrorNode(node antlr.ErrorNode) {} 17 | 18 | // EnterEveryRule is called when any rule is entered. 19 | func (s *BasepluralListener) EnterEveryRule(ctx antlr.ParserRuleContext) {} 20 | 21 | // ExitEveryRule is called when any rule is exited. 22 | func (s *BasepluralListener) ExitEveryRule(ctx antlr.ParserRuleContext) {} 23 | 24 | // EnterStart is called when production start is entered. 25 | func (s *BasepluralListener) EnterStart(ctx *StartContext) {} 26 | 27 | // ExitStart is called when production start is exited. 28 | func (s *BasepluralListener) ExitStart(ctx *StartContext) {} 29 | 30 | // EnterExp is called when production exp is entered. 31 | func (s *BasepluralListener) EnterExp(ctx *ExpContext) {} 32 | 33 | // ExitExp is called when production exp is exited. 34 | func (s *BasepluralListener) ExitExp(ctx *ExpContext) {} 35 | 36 | // EnterPrimary is called when production primary is entered. 37 | func (s *BasepluralListener) EnterPrimary(ctx *PrimaryContext) {} 38 | 39 | // ExitPrimary is called when production primary is exited. 40 | func (s *BasepluralListener) ExitPrimary(ctx *PrimaryContext) {} 41 | -------------------------------------------------------------------------------- /plurals/parser/plural_lexer.go: -------------------------------------------------------------------------------- 1 | // Code generated from plural.g4 by ANTLR 4.13.1. DO NOT EDIT. 2 | 3 | package parser 4 | 5 | import ( 6 | "fmt" 7 | "github.com/antlr4-go/antlr/v4" 8 | "sync" 9 | "unicode" 10 | ) 11 | 12 | // Suppress unused import error 13 | var _ = fmt.Printf 14 | var _ = sync.Once{} 15 | var _ = unicode.IsLetter 16 | 17 | type pluralLexer struct { 18 | *antlr.BaseLexer 19 | channelNames []string 20 | modeNames []string 21 | // TODO: EOF string 22 | } 23 | 24 | var PluralLexerLexerStaticData struct { 25 | once sync.Once 26 | serializedATN []int32 27 | ChannelNames []string 28 | ModeNames []string 29 | LiteralNames []string 30 | SymbolicNames []string 31 | RuleNames []string 32 | PredictionContextCache *antlr.PredictionContextCache 33 | atn *antlr.ATN 34 | decisionToDFA []*antlr.DFA 35 | } 36 | 37 | func plurallexerLexerInit() { 38 | staticData := &PluralLexerLexerStaticData 39 | staticData.ChannelNames = []string{ 40 | "DEFAULT_TOKEN_CHANNEL", "HIDDEN", 41 | } 42 | staticData.ModeNames = []string{ 43 | "DEFAULT_MODE", 44 | } 45 | staticData.LiteralNames = []string{ 46 | "", "'++'", "'--'", "'+'", "'-'", "'~'", "'!'", "'*'", "'/'", "'%'", 47 | "'>>'", "'<<'", "'>'", "'<'", "'>='", "'<='", "'=='", "'!='", "'&'", 48 | "'^'", "'|'", "'&&'", "'||'", "'?'", "':'", "'('", "')'", "'n'", 49 | } 50 | staticData.SymbolicNames = []string{ 51 | "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 52 | "", "", "", "", "", "", "", "", "", "", "", "INT", "WS", 53 | } 54 | staticData.RuleNames = []string{ 55 | "T__0", "T__1", "T__2", "T__3", "T__4", "T__5", "T__6", "T__7", "T__8", 56 | "T__9", "T__10", "T__11", "T__12", "T__13", "T__14", "T__15", "T__16", 57 | "T__17", "T__18", "T__19", "T__20", "T__21", "T__22", "T__23", "T__24", 58 | "T__25", "T__26", "INT", "WS", 59 | } 60 | staticData.PredictionContextCache = antlr.NewPredictionContextCache() 61 | staticData.serializedATN = []int32{ 62 | 4, 0, 29, 135, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 63 | 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 64 | 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 65 | 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 66 | 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 67 | 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 68 | 1, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1, 4, 1, 4, 1, 5, 1, 5, 1, 6, 1, 6, 1, 7, 69 | 1, 7, 1, 8, 1, 8, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 10, 1, 11, 1, 11, 70 | 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15, 1, 71 | 15, 1, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 18, 1, 18, 1, 19, 1, 19, 1, 20, 72 | 1, 20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 22, 1, 22, 1, 23, 1, 23, 1, 24, 1, 73 | 24, 1, 25, 1, 25, 1, 26, 1, 26, 1, 27, 4, 27, 125, 8, 27, 11, 27, 12, 27, 74 | 126, 1, 28, 4, 28, 130, 8, 28, 11, 28, 12, 28, 131, 1, 28, 1, 28, 0, 0, 75 | 29, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19, 10, 21, 76 | 11, 23, 12, 25, 13, 27, 14, 29, 15, 31, 16, 33, 17, 35, 18, 37, 19, 39, 77 | 20, 41, 21, 43, 22, 45, 23, 47, 24, 49, 25, 51, 26, 53, 27, 55, 28, 57, 78 | 29, 1, 0, 2, 1, 0, 48, 57, 2, 0, 9, 9, 32, 32, 136, 0, 1, 1, 0, 0, 0, 0, 79 | 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 80 | 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 81 | 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0, 0, 0, 25, 1, 0, 0, 82 | 0, 0, 27, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 0, 31, 1, 0, 0, 0, 0, 33, 1, 0, 83 | 0, 0, 0, 35, 1, 0, 0, 0, 0, 37, 1, 0, 0, 0, 0, 39, 1, 0, 0, 0, 0, 41, 1, 84 | 0, 0, 0, 0, 43, 1, 0, 0, 0, 0, 45, 1, 0, 0, 0, 0, 47, 1, 0, 0, 0, 0, 49, 85 | 1, 0, 0, 0, 0, 51, 1, 0, 0, 0, 0, 53, 1, 0, 0, 0, 0, 55, 1, 0, 0, 0, 0, 86 | 57, 1, 0, 0, 0, 1, 59, 1, 0, 0, 0, 3, 62, 1, 0, 0, 0, 5, 65, 1, 0, 0, 0, 87 | 7, 67, 1, 0, 0, 0, 9, 69, 1, 0, 0, 0, 11, 71, 1, 0, 0, 0, 13, 73, 1, 0, 88 | 0, 0, 15, 75, 1, 0, 0, 0, 17, 77, 1, 0, 0, 0, 19, 79, 1, 0, 0, 0, 21, 82, 89 | 1, 0, 0, 0, 23, 85, 1, 0, 0, 0, 25, 87, 1, 0, 0, 0, 27, 89, 1, 0, 0, 0, 90 | 29, 92, 1, 0, 0, 0, 31, 95, 1, 0, 0, 0, 33, 98, 1, 0, 0, 0, 35, 101, 1, 91 | 0, 0, 0, 37, 103, 1, 0, 0, 0, 39, 105, 1, 0, 0, 0, 41, 107, 1, 0, 0, 0, 92 | 43, 110, 1, 0, 0, 0, 45, 113, 1, 0, 0, 0, 47, 115, 1, 0, 0, 0, 49, 117, 93 | 1, 0, 0, 0, 51, 119, 1, 0, 0, 0, 53, 121, 1, 0, 0, 0, 55, 124, 1, 0, 0, 94 | 0, 57, 129, 1, 0, 0, 0, 59, 60, 5, 43, 0, 0, 60, 61, 5, 43, 0, 0, 61, 2, 95 | 1, 0, 0, 0, 62, 63, 5, 45, 0, 0, 63, 64, 5, 45, 0, 0, 64, 4, 1, 0, 0, 0, 96 | 65, 66, 5, 43, 0, 0, 66, 6, 1, 0, 0, 0, 67, 68, 5, 45, 0, 0, 68, 8, 1, 97 | 0, 0, 0, 69, 70, 5, 126, 0, 0, 70, 10, 1, 0, 0, 0, 71, 72, 5, 33, 0, 0, 98 | 72, 12, 1, 0, 0, 0, 73, 74, 5, 42, 0, 0, 74, 14, 1, 0, 0, 0, 75, 76, 5, 99 | 47, 0, 0, 76, 16, 1, 0, 0, 0, 77, 78, 5, 37, 0, 0, 78, 18, 1, 0, 0, 0, 100 | 79, 80, 5, 62, 0, 0, 80, 81, 5, 62, 0, 0, 81, 20, 1, 0, 0, 0, 82, 83, 5, 101 | 60, 0, 0, 83, 84, 5, 60, 0, 0, 84, 22, 1, 0, 0, 0, 85, 86, 5, 62, 0, 0, 102 | 86, 24, 1, 0, 0, 0, 87, 88, 5, 60, 0, 0, 88, 26, 1, 0, 0, 0, 89, 90, 5, 103 | 62, 0, 0, 90, 91, 5, 61, 0, 0, 91, 28, 1, 0, 0, 0, 92, 93, 5, 60, 0, 0, 104 | 93, 94, 5, 61, 0, 0, 94, 30, 1, 0, 0, 0, 95, 96, 5, 61, 0, 0, 96, 97, 5, 105 | 61, 0, 0, 97, 32, 1, 0, 0, 0, 98, 99, 5, 33, 0, 0, 99, 100, 5, 61, 0, 0, 106 | 100, 34, 1, 0, 0, 0, 101, 102, 5, 38, 0, 0, 102, 36, 1, 0, 0, 0, 103, 104, 107 | 5, 94, 0, 0, 104, 38, 1, 0, 0, 0, 105, 106, 5, 124, 0, 0, 106, 40, 1, 0, 108 | 0, 0, 107, 108, 5, 38, 0, 0, 108, 109, 5, 38, 0, 0, 109, 42, 1, 0, 0, 0, 109 | 110, 111, 5, 124, 0, 0, 111, 112, 5, 124, 0, 0, 112, 44, 1, 0, 0, 0, 113, 110 | 114, 5, 63, 0, 0, 114, 46, 1, 0, 0, 0, 115, 116, 5, 58, 0, 0, 116, 48, 111 | 1, 0, 0, 0, 117, 118, 5, 40, 0, 0, 118, 50, 1, 0, 0, 0, 119, 120, 5, 41, 112 | 0, 0, 120, 52, 1, 0, 0, 0, 121, 122, 5, 110, 0, 0, 122, 54, 1, 0, 0, 0, 113 | 123, 125, 7, 0, 0, 0, 124, 123, 1, 0, 0, 0, 125, 126, 1, 0, 0, 0, 126, 114 | 124, 1, 0, 0, 0, 126, 127, 1, 0, 0, 0, 127, 56, 1, 0, 0, 0, 128, 130, 7, 115 | 1, 0, 0, 129, 128, 1, 0, 0, 0, 130, 131, 1, 0, 0, 0, 131, 129, 1, 0, 0, 116 | 0, 131, 132, 1, 0, 0, 0, 132, 133, 1, 0, 0, 0, 133, 134, 6, 28, 0, 0, 134, 117 | 58, 1, 0, 0, 0, 3, 0, 126, 131, 1, 6, 0, 0, 118 | } 119 | deserializer := antlr.NewATNDeserializer(nil) 120 | staticData.atn = deserializer.Deserialize(staticData.serializedATN) 121 | atn := staticData.atn 122 | staticData.decisionToDFA = make([]*antlr.DFA, len(atn.DecisionToState)) 123 | decisionToDFA := staticData.decisionToDFA 124 | for index, state := range atn.DecisionToState { 125 | decisionToDFA[index] = antlr.NewDFA(state, index) 126 | } 127 | } 128 | 129 | // pluralLexerInit initializes any static state used to implement pluralLexer. By default the 130 | // static state used to implement the lexer is lazily initialized during the first call to 131 | // NewpluralLexer(). You can call this function if you wish to initialize the static state ahead 132 | // of time. 133 | func PluralLexerInit() { 134 | staticData := &PluralLexerLexerStaticData 135 | staticData.once.Do(plurallexerLexerInit) 136 | } 137 | 138 | // NewpluralLexer produces a new lexer instance for the optional input antlr.CharStream. 139 | func NewpluralLexer(input antlr.CharStream) *pluralLexer { 140 | PluralLexerInit() 141 | l := new(pluralLexer) 142 | l.BaseLexer = antlr.NewBaseLexer(input) 143 | staticData := &PluralLexerLexerStaticData 144 | l.Interpreter = antlr.NewLexerATNSimulator(l, staticData.atn, staticData.decisionToDFA, staticData.PredictionContextCache) 145 | l.channelNames = staticData.ChannelNames 146 | l.modeNames = staticData.ModeNames 147 | l.RuleNames = staticData.RuleNames 148 | l.LiteralNames = staticData.LiteralNames 149 | l.SymbolicNames = staticData.SymbolicNames 150 | l.GrammarFileName = "plural.g4" 151 | // TODO: l.EOF = antlr.TokenEOF 152 | 153 | return l 154 | } 155 | 156 | // pluralLexer tokens. 157 | const ( 158 | pluralLexerT__0 = 1 159 | pluralLexerT__1 = 2 160 | pluralLexerT__2 = 3 161 | pluralLexerT__3 = 4 162 | pluralLexerT__4 = 5 163 | pluralLexerT__5 = 6 164 | pluralLexerT__6 = 7 165 | pluralLexerT__7 = 8 166 | pluralLexerT__8 = 9 167 | pluralLexerT__9 = 10 168 | pluralLexerT__10 = 11 169 | pluralLexerT__11 = 12 170 | pluralLexerT__12 = 13 171 | pluralLexerT__13 = 14 172 | pluralLexerT__14 = 15 173 | pluralLexerT__15 = 16 174 | pluralLexerT__16 = 17 175 | pluralLexerT__17 = 18 176 | pluralLexerT__18 = 19 177 | pluralLexerT__19 = 20 178 | pluralLexerT__20 = 21 179 | pluralLexerT__21 = 22 180 | pluralLexerT__22 = 23 181 | pluralLexerT__23 = 24 182 | pluralLexerT__24 = 25 183 | pluralLexerT__25 = 26 184 | pluralLexerT__26 = 27 185 | pluralLexerINT = 28 186 | pluralLexerWS = 29 187 | ) 188 | -------------------------------------------------------------------------------- /plurals/parser/plural_listener.go: -------------------------------------------------------------------------------- 1 | // Code generated from plural.g4 by ANTLR 4.13.1. DO NOT EDIT. 2 | 3 | package parser // plural 4 | 5 | import "github.com/antlr4-go/antlr/v4" 6 | 7 | // pluralListener is a complete listener for a parse tree produced by pluralParser. 8 | type pluralListener interface { 9 | antlr.ParseTreeListener 10 | 11 | // EnterStart is called when entering the start production. 12 | EnterStart(c *StartContext) 13 | 14 | // EnterExp is called when entering the exp production. 15 | EnterExp(c *ExpContext) 16 | 17 | // EnterPrimary is called when entering the primary production. 18 | EnterPrimary(c *PrimaryContext) 19 | 20 | // ExitStart is called when exiting the start production. 21 | ExitStart(c *StartContext) 22 | 23 | // ExitExp is called when exiting the exp production. 24 | ExitExp(c *ExpContext) 25 | 26 | // ExitPrimary is called when exiting the primary production. 27 | ExitPrimary(c *PrimaryContext) 28 | } 29 | -------------------------------------------------------------------------------- /plurals/parser/plural_parser.go: -------------------------------------------------------------------------------- 1 | // Code generated from plural.g4 by ANTLR 4.13.1. DO NOT EDIT. 2 | 3 | package parser // plural 4 | 5 | import ( 6 | "fmt" 7 | "strconv" 8 | "sync" 9 | 10 | "github.com/antlr4-go/antlr/v4" 11 | ) 12 | 13 | // Suppress unused import errors 14 | var _ = fmt.Printf 15 | var _ = strconv.Itoa 16 | var _ = sync.Once{} 17 | 18 | type pluralParser struct { 19 | *antlr.BaseParser 20 | } 21 | 22 | var PluralParserStaticData struct { 23 | once sync.Once 24 | serializedATN []int32 25 | LiteralNames []string 26 | SymbolicNames []string 27 | RuleNames []string 28 | PredictionContextCache *antlr.PredictionContextCache 29 | atn *antlr.ATN 30 | decisionToDFA []*antlr.DFA 31 | } 32 | 33 | func pluralParserInit() { 34 | staticData := &PluralParserStaticData 35 | staticData.LiteralNames = []string{ 36 | "", "'++'", "'--'", "'+'", "'-'", "'~'", "'!'", "'*'", "'/'", "'%'", 37 | "'>>'", "'<<'", "'>'", "'<'", "'>='", "'<='", "'=='", "'!='", "'&'", 38 | "'^'", "'|'", "'&&'", "'||'", "'?'", "':'", "'('", "')'", "'n'", 39 | } 40 | staticData.SymbolicNames = []string{ 41 | "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 42 | "", "", "", "", "", "", "", "", "", "", "", "INT", "WS", 43 | } 44 | staticData.RuleNames = []string{ 45 | "start", "exp", "primary", 46 | } 47 | staticData.PredictionContextCache = antlr.NewPredictionContextCache() 48 | staticData.serializedATN = []int32{ 49 | 4, 1, 29, 71, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 1, 0, 1, 0, 1, 1, 1, 50 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 15, 8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 51 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 52 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 53 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 54 | 5, 1, 58, 8, 1, 10, 1, 12, 1, 61, 9, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 55 | 2, 3, 2, 69, 8, 2, 1, 2, 0, 1, 2, 3, 0, 2, 4, 0, 9, 1, 0, 1, 4, 1, 0, 5, 56 | 6, 1, 0, 7, 9, 1, 0, 3, 4, 1, 0, 10, 11, 1, 0, 12, 13, 1, 0, 14, 15, 1, 57 | 0, 16, 17, 1, 0, 1, 2, 84, 0, 6, 1, 0, 0, 0, 2, 14, 1, 0, 0, 0, 4, 68, 58 | 1, 0, 0, 0, 6, 7, 3, 2, 1, 0, 7, 1, 1, 0, 0, 0, 8, 9, 6, 1, -1, 0, 9, 15, 59 | 3, 4, 2, 0, 10, 11, 7, 0, 0, 0, 11, 15, 3, 2, 1, 14, 12, 13, 7, 1, 0, 0, 60 | 13, 15, 3, 2, 1, 13, 14, 8, 1, 0, 0, 0, 14, 10, 1, 0, 0, 0, 14, 12, 1, 61 | 0, 0, 0, 15, 59, 1, 0, 0, 0, 16, 17, 10, 12, 0, 0, 17, 18, 7, 2, 0, 0, 62 | 18, 58, 3, 2, 1, 13, 19, 20, 10, 11, 0, 0, 20, 21, 7, 3, 0, 0, 21, 58, 63 | 3, 2, 1, 12, 22, 23, 10, 10, 0, 0, 23, 24, 7, 4, 0, 0, 24, 58, 3, 2, 1, 64 | 11, 25, 26, 10, 9, 0, 0, 26, 27, 7, 5, 0, 0, 27, 58, 3, 2, 1, 10, 28, 29, 65 | 10, 8, 0, 0, 29, 30, 7, 6, 0, 0, 30, 58, 3, 2, 1, 9, 31, 32, 10, 7, 0, 66 | 0, 32, 33, 7, 7, 0, 0, 33, 58, 3, 2, 1, 8, 34, 35, 10, 6, 0, 0, 35, 36, 67 | 5, 18, 0, 0, 36, 58, 3, 2, 1, 7, 37, 38, 10, 5, 0, 0, 38, 39, 5, 19, 0, 68 | 0, 39, 58, 3, 2, 1, 6, 40, 41, 10, 4, 0, 0, 41, 42, 5, 20, 0, 0, 42, 58, 69 | 3, 2, 1, 5, 43, 44, 10, 3, 0, 0, 44, 45, 5, 21, 0, 0, 45, 58, 3, 2, 1, 70 | 4, 46, 47, 10, 2, 0, 0, 47, 48, 5, 22, 0, 0, 48, 58, 3, 2, 1, 3, 49, 50, 71 | 10, 1, 0, 0, 50, 51, 5, 23, 0, 0, 51, 52, 3, 2, 1, 0, 52, 53, 5, 24, 0, 72 | 0, 53, 54, 3, 2, 1, 1, 54, 58, 1, 0, 0, 0, 55, 56, 10, 15, 0, 0, 56, 58, 73 | 7, 8, 0, 0, 57, 16, 1, 0, 0, 0, 57, 19, 1, 0, 0, 0, 57, 22, 1, 0, 0, 0, 74 | 57, 25, 1, 0, 0, 0, 57, 28, 1, 0, 0, 0, 57, 31, 1, 0, 0, 0, 57, 34, 1, 75 | 0, 0, 0, 57, 37, 1, 0, 0, 0, 57, 40, 1, 0, 0, 0, 57, 43, 1, 0, 0, 0, 57, 76 | 46, 1, 0, 0, 0, 57, 49, 1, 0, 0, 0, 57, 55, 1, 0, 0, 0, 58, 61, 1, 0, 0, 77 | 0, 59, 57, 1, 0, 0, 0, 59, 60, 1, 0, 0, 0, 60, 3, 1, 0, 0, 0, 61, 59, 1, 78 | 0, 0, 0, 62, 63, 5, 25, 0, 0, 63, 64, 3, 2, 1, 0, 64, 65, 5, 26, 0, 0, 79 | 65, 69, 1, 0, 0, 0, 66, 69, 5, 27, 0, 0, 67, 69, 5, 28, 0, 0, 68, 62, 1, 80 | 0, 0, 0, 68, 66, 1, 0, 0, 0, 68, 67, 1, 0, 0, 0, 69, 5, 1, 0, 0, 0, 4, 81 | 14, 57, 59, 68, 82 | } 83 | deserializer := antlr.NewATNDeserializer(nil) 84 | staticData.atn = deserializer.Deserialize(staticData.serializedATN) 85 | atn := staticData.atn 86 | staticData.decisionToDFA = make([]*antlr.DFA, len(atn.DecisionToState)) 87 | decisionToDFA := staticData.decisionToDFA 88 | for index, state := range atn.DecisionToState { 89 | decisionToDFA[index] = antlr.NewDFA(state, index) 90 | } 91 | } 92 | 93 | // pluralParserInit initializes any static state used to implement pluralParser. By default the 94 | // static state used to implement the parser is lazily initialized during the first call to 95 | // NewpluralParser(). You can call this function if you wish to initialize the static state ahead 96 | // of time. 97 | func PluralParserInit() { 98 | staticData := &PluralParserStaticData 99 | staticData.once.Do(pluralParserInit) 100 | } 101 | 102 | // NewpluralParser produces a new parser instance for the optional input antlr.TokenStream. 103 | func NewpluralParser(input antlr.TokenStream) *pluralParser { 104 | PluralParserInit() 105 | this := new(pluralParser) 106 | this.BaseParser = antlr.NewBaseParser(input) 107 | staticData := &PluralParserStaticData 108 | this.Interpreter = antlr.NewParserATNSimulator(this, staticData.atn, staticData.decisionToDFA, staticData.PredictionContextCache) 109 | this.RuleNames = staticData.RuleNames 110 | this.LiteralNames = staticData.LiteralNames 111 | this.SymbolicNames = staticData.SymbolicNames 112 | this.GrammarFileName = "plural.g4" 113 | 114 | return this 115 | } 116 | 117 | // pluralParser tokens. 118 | const ( 119 | pluralParserEOF = antlr.TokenEOF 120 | pluralParserT__0 = 1 121 | pluralParserT__1 = 2 122 | pluralParserT__2 = 3 123 | pluralParserT__3 = 4 124 | pluralParserT__4 = 5 125 | pluralParserT__5 = 6 126 | pluralParserT__6 = 7 127 | pluralParserT__7 = 8 128 | pluralParserT__8 = 9 129 | pluralParserT__9 = 10 130 | pluralParserT__10 = 11 131 | pluralParserT__11 = 12 132 | pluralParserT__12 = 13 133 | pluralParserT__13 = 14 134 | pluralParserT__14 = 15 135 | pluralParserT__15 = 16 136 | pluralParserT__16 = 17 137 | pluralParserT__17 = 18 138 | pluralParserT__18 = 19 139 | pluralParserT__19 = 20 140 | pluralParserT__20 = 21 141 | pluralParserT__21 = 22 142 | pluralParserT__22 = 23 143 | pluralParserT__23 = 24 144 | pluralParserT__24 = 25 145 | pluralParserT__25 = 26 146 | pluralParserT__26 = 27 147 | pluralParserINT = 28 148 | pluralParserWS = 29 149 | ) 150 | 151 | // pluralParser rules. 152 | const ( 153 | pluralParserRULE_start = 0 154 | pluralParserRULE_exp = 1 155 | pluralParserRULE_primary = 2 156 | ) 157 | 158 | // IStartContext is an interface to support dynamic dispatch. 159 | type IStartContext interface { 160 | antlr.ParserRuleContext 161 | 162 | // GetParser returns the parser. 163 | GetParser() antlr.Parser 164 | 165 | // Getter signatures 166 | Exp() IExpContext 167 | 168 | // IsStartContext differentiates from other interfaces. 169 | IsStartContext() 170 | } 171 | 172 | type StartContext struct { 173 | antlr.BaseParserRuleContext 174 | parser antlr.Parser 175 | } 176 | 177 | func NewEmptyStartContext() *StartContext { 178 | var p = new(StartContext) 179 | antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) 180 | p.RuleIndex = pluralParserRULE_start 181 | return p 182 | } 183 | 184 | func InitEmptyStartContext(p *StartContext) { 185 | antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) 186 | p.RuleIndex = pluralParserRULE_start 187 | } 188 | 189 | func (*StartContext) IsStartContext() {} 190 | 191 | func NewStartContext(parser antlr.Parser, parent antlr.ParserRuleContext, invokingState int) *StartContext { 192 | var p = new(StartContext) 193 | 194 | antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, parent, invokingState) 195 | 196 | p.parser = parser 197 | p.RuleIndex = pluralParserRULE_start 198 | 199 | return p 200 | } 201 | 202 | func (s *StartContext) GetParser() antlr.Parser { return s.parser } 203 | 204 | func (s *StartContext) Exp() IExpContext { 205 | var t antlr.RuleContext 206 | for _, ctx := range s.GetChildren() { 207 | if _, ok := ctx.(IExpContext); ok { 208 | t = ctx.(antlr.RuleContext) 209 | break 210 | } 211 | } 212 | 213 | if t == nil { 214 | return nil 215 | } 216 | 217 | return t.(IExpContext) 218 | } 219 | 220 | func (s *StartContext) GetRuleContext() antlr.RuleContext { 221 | return s 222 | } 223 | 224 | func (s *StartContext) ToStringTree(ruleNames []string, recog antlr.Recognizer) string { 225 | return antlr.TreesStringTree(s, ruleNames, recog) 226 | } 227 | 228 | func (s *StartContext) EnterRule(listener antlr.ParseTreeListener) { 229 | if listenerT, ok := listener.(pluralListener); ok { 230 | listenerT.EnterStart(s) 231 | } 232 | } 233 | 234 | func (s *StartContext) ExitRule(listener antlr.ParseTreeListener) { 235 | if listenerT, ok := listener.(pluralListener); ok { 236 | listenerT.ExitStart(s) 237 | } 238 | } 239 | 240 | func (p *pluralParser) Start_() (localctx IStartContext) { 241 | localctx = NewStartContext(p, p.GetParserRuleContext(), p.GetState()) 242 | p.EnterRule(localctx, 0, pluralParserRULE_start) 243 | p.EnterOuterAlt(localctx, 1) 244 | { 245 | p.SetState(6) 246 | p.exp(0) 247 | } 248 | 249 | errorExit: 250 | if p.HasError() { 251 | v := p.GetError() 252 | localctx.SetException(v) 253 | p.GetErrorHandler().ReportError(p, v) 254 | p.GetErrorHandler().Recover(p, v) 255 | p.SetError(nil) 256 | } 257 | p.ExitRule() 258 | return localctx 259 | goto errorExit // Trick to prevent compiler error if the label is not used 260 | } 261 | 262 | // IExpContext is an interface to support dynamic dispatch. 263 | type IExpContext interface { 264 | antlr.ParserRuleContext 265 | 266 | // GetParser returns the parser. 267 | GetParser() antlr.Parser 268 | 269 | // GetPrefix returns the prefix token. 270 | GetPrefix() antlr.Token 271 | 272 | // GetBop returns the bop token. 273 | GetBop() antlr.Token 274 | 275 | // GetPostfix returns the postfix token. 276 | GetPostfix() antlr.Token 277 | 278 | // SetPrefix sets the prefix token. 279 | SetPrefix(antlr.Token) 280 | 281 | // SetBop sets the bop token. 282 | SetBop(antlr.Token) 283 | 284 | // SetPostfix sets the postfix token. 285 | SetPostfix(antlr.Token) 286 | 287 | // Getter signatures 288 | Primary() IPrimaryContext 289 | AllExp() []IExpContext 290 | Exp(i int) IExpContext 291 | 292 | // IsExpContext differentiates from other interfaces. 293 | IsExpContext() 294 | } 295 | 296 | type ExpContext struct { 297 | antlr.BaseParserRuleContext 298 | parser antlr.Parser 299 | prefix antlr.Token 300 | bop antlr.Token 301 | postfix antlr.Token 302 | } 303 | 304 | func NewEmptyExpContext() *ExpContext { 305 | var p = new(ExpContext) 306 | antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) 307 | p.RuleIndex = pluralParserRULE_exp 308 | return p 309 | } 310 | 311 | func InitEmptyExpContext(p *ExpContext) { 312 | antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) 313 | p.RuleIndex = pluralParserRULE_exp 314 | } 315 | 316 | func (*ExpContext) IsExpContext() {} 317 | 318 | func NewExpContext(parser antlr.Parser, parent antlr.ParserRuleContext, invokingState int) *ExpContext { 319 | var p = new(ExpContext) 320 | 321 | antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, parent, invokingState) 322 | 323 | p.parser = parser 324 | p.RuleIndex = pluralParserRULE_exp 325 | 326 | return p 327 | } 328 | 329 | func (s *ExpContext) GetParser() antlr.Parser { return s.parser } 330 | 331 | func (s *ExpContext) GetPrefix() antlr.Token { return s.prefix } 332 | 333 | func (s *ExpContext) GetBop() antlr.Token { return s.bop } 334 | 335 | func (s *ExpContext) GetPostfix() antlr.Token { return s.postfix } 336 | 337 | func (s *ExpContext) SetPrefix(v antlr.Token) { s.prefix = v } 338 | 339 | func (s *ExpContext) SetBop(v antlr.Token) { s.bop = v } 340 | 341 | func (s *ExpContext) SetPostfix(v antlr.Token) { s.postfix = v } 342 | 343 | func (s *ExpContext) Primary() IPrimaryContext { 344 | var t antlr.RuleContext 345 | for _, ctx := range s.GetChildren() { 346 | if _, ok := ctx.(IPrimaryContext); ok { 347 | t = ctx.(antlr.RuleContext) 348 | break 349 | } 350 | } 351 | 352 | if t == nil { 353 | return nil 354 | } 355 | 356 | return t.(IPrimaryContext) 357 | } 358 | 359 | func (s *ExpContext) AllExp() []IExpContext { 360 | children := s.GetChildren() 361 | len := 0 362 | for _, ctx := range children { 363 | if _, ok := ctx.(IExpContext); ok { 364 | len++ 365 | } 366 | } 367 | 368 | tst := make([]IExpContext, len) 369 | i := 0 370 | for _, ctx := range children { 371 | if t, ok := ctx.(IExpContext); ok { 372 | tst[i] = t.(IExpContext) 373 | i++ 374 | } 375 | } 376 | 377 | return tst 378 | } 379 | 380 | func (s *ExpContext) Exp(i int) IExpContext { 381 | var t antlr.RuleContext 382 | j := 0 383 | for _, ctx := range s.GetChildren() { 384 | if _, ok := ctx.(IExpContext); ok { 385 | if j == i { 386 | t = ctx.(antlr.RuleContext) 387 | break 388 | } 389 | j++ 390 | } 391 | } 392 | 393 | if t == nil { 394 | return nil 395 | } 396 | 397 | return t.(IExpContext) 398 | } 399 | 400 | func (s *ExpContext) GetRuleContext() antlr.RuleContext { 401 | return s 402 | } 403 | 404 | func (s *ExpContext) ToStringTree(ruleNames []string, recog antlr.Recognizer) string { 405 | return antlr.TreesStringTree(s, ruleNames, recog) 406 | } 407 | 408 | func (s *ExpContext) EnterRule(listener antlr.ParseTreeListener) { 409 | if listenerT, ok := listener.(pluralListener); ok { 410 | listenerT.EnterExp(s) 411 | } 412 | } 413 | 414 | func (s *ExpContext) ExitRule(listener antlr.ParseTreeListener) { 415 | if listenerT, ok := listener.(pluralListener); ok { 416 | listenerT.ExitExp(s) 417 | } 418 | } 419 | 420 | func (p *pluralParser) Exp() (localctx IExpContext) { 421 | return p.exp(0) 422 | } 423 | 424 | func (p *pluralParser) exp(_p int) (localctx IExpContext) { 425 | var _parentctx antlr.ParserRuleContext = p.GetParserRuleContext() 426 | 427 | _parentState := p.GetState() 428 | localctx = NewExpContext(p, p.GetParserRuleContext(), _parentState) 429 | var _prevctx IExpContext = localctx 430 | var _ antlr.ParserRuleContext = _prevctx // TODO: To prevent unused variable warning. 431 | _startState := 2 432 | p.EnterRecursionRule(localctx, 2, pluralParserRULE_exp, _p) 433 | var _la int 434 | 435 | var _alt int 436 | 437 | p.EnterOuterAlt(localctx, 1) 438 | p.SetState(14) 439 | p.GetErrorHandler().Sync(p) 440 | if p.HasError() { 441 | goto errorExit 442 | } 443 | 444 | switch p.GetTokenStream().LA(1) { 445 | case pluralParserT__24, pluralParserT__26, pluralParserINT: 446 | { 447 | p.SetState(9) 448 | p.Primary() 449 | } 450 | 451 | case pluralParserT__0, pluralParserT__1, pluralParserT__2, pluralParserT__3: 452 | { 453 | p.SetState(10) 454 | 455 | var _lt = p.GetTokenStream().LT(1) 456 | 457 | localctx.(*ExpContext).prefix = _lt 458 | 459 | _la = p.GetTokenStream().LA(1) 460 | 461 | if !((int64(_la) & ^0x3f) == 0 && ((int64(1)<<_la)&30) != 0) { 462 | var _ri = p.GetErrorHandler().RecoverInline(p) 463 | 464 | localctx.(*ExpContext).prefix = _ri 465 | } else { 466 | p.GetErrorHandler().ReportMatch(p) 467 | p.Consume() 468 | } 469 | } 470 | { 471 | p.SetState(11) 472 | p.exp(14) 473 | } 474 | 475 | case pluralParserT__4, pluralParserT__5: 476 | { 477 | p.SetState(12) 478 | 479 | var _lt = p.GetTokenStream().LT(1) 480 | 481 | localctx.(*ExpContext).prefix = _lt 482 | 483 | _la = p.GetTokenStream().LA(1) 484 | 485 | if !(_la == pluralParserT__4 || _la == pluralParserT__5) { 486 | var _ri = p.GetErrorHandler().RecoverInline(p) 487 | 488 | localctx.(*ExpContext).prefix = _ri 489 | } else { 490 | p.GetErrorHandler().ReportMatch(p) 491 | p.Consume() 492 | } 493 | } 494 | { 495 | p.SetState(13) 496 | p.exp(13) 497 | } 498 | 499 | default: 500 | p.SetError(antlr.NewNoViableAltException(p, nil, nil, nil, nil, nil)) 501 | goto errorExit 502 | } 503 | p.GetParserRuleContext().SetStop(p.GetTokenStream().LT(-1)) 504 | p.SetState(59) 505 | p.GetErrorHandler().Sync(p) 506 | if p.HasError() { 507 | goto errorExit 508 | } 509 | _alt = p.GetInterpreter().AdaptivePredict(p.BaseParser, p.GetTokenStream(), 2, p.GetParserRuleContext()) 510 | if p.HasError() { 511 | goto errorExit 512 | } 513 | for _alt != 2 && _alt != antlr.ATNInvalidAltNumber { 514 | if _alt == 1 { 515 | if p.GetParseListeners() != nil { 516 | p.TriggerExitRuleEvent() 517 | } 518 | _prevctx = localctx 519 | p.SetState(57) 520 | p.GetErrorHandler().Sync(p) 521 | if p.HasError() { 522 | goto errorExit 523 | } 524 | 525 | switch p.GetInterpreter().AdaptivePredict(p.BaseParser, p.GetTokenStream(), 1, p.GetParserRuleContext()) { 526 | case 1: 527 | localctx = NewExpContext(p, _parentctx, _parentState) 528 | p.PushNewRecursionContext(localctx, _startState, pluralParserRULE_exp) 529 | p.SetState(16) 530 | 531 | if !(p.Precpred(p.GetParserRuleContext(), 12)) { 532 | p.SetError(antlr.NewFailedPredicateException(p, "p.Precpred(p.GetParserRuleContext(), 12)", "")) 533 | goto errorExit 534 | } 535 | { 536 | p.SetState(17) 537 | 538 | var _lt = p.GetTokenStream().LT(1) 539 | 540 | localctx.(*ExpContext).bop = _lt 541 | 542 | _la = p.GetTokenStream().LA(1) 543 | 544 | if !((int64(_la) & ^0x3f) == 0 && ((int64(1)<<_la)&896) != 0) { 545 | var _ri = p.GetErrorHandler().RecoverInline(p) 546 | 547 | localctx.(*ExpContext).bop = _ri 548 | } else { 549 | p.GetErrorHandler().ReportMatch(p) 550 | p.Consume() 551 | } 552 | } 553 | { 554 | p.SetState(18) 555 | p.exp(13) 556 | } 557 | 558 | case 2: 559 | localctx = NewExpContext(p, _parentctx, _parentState) 560 | p.PushNewRecursionContext(localctx, _startState, pluralParserRULE_exp) 561 | p.SetState(19) 562 | 563 | if !(p.Precpred(p.GetParserRuleContext(), 11)) { 564 | p.SetError(antlr.NewFailedPredicateException(p, "p.Precpred(p.GetParserRuleContext(), 11)", "")) 565 | goto errorExit 566 | } 567 | { 568 | p.SetState(20) 569 | 570 | var _lt = p.GetTokenStream().LT(1) 571 | 572 | localctx.(*ExpContext).bop = _lt 573 | 574 | _la = p.GetTokenStream().LA(1) 575 | 576 | if !(_la == pluralParserT__2 || _la == pluralParserT__3) { 577 | var _ri = p.GetErrorHandler().RecoverInline(p) 578 | 579 | localctx.(*ExpContext).bop = _ri 580 | } else { 581 | p.GetErrorHandler().ReportMatch(p) 582 | p.Consume() 583 | } 584 | } 585 | { 586 | p.SetState(21) 587 | p.exp(12) 588 | } 589 | 590 | case 3: 591 | localctx = NewExpContext(p, _parentctx, _parentState) 592 | p.PushNewRecursionContext(localctx, _startState, pluralParserRULE_exp) 593 | p.SetState(22) 594 | 595 | if !(p.Precpred(p.GetParserRuleContext(), 10)) { 596 | p.SetError(antlr.NewFailedPredicateException(p, "p.Precpred(p.GetParserRuleContext(), 10)", "")) 597 | goto errorExit 598 | } 599 | { 600 | p.SetState(23) 601 | 602 | var _lt = p.GetTokenStream().LT(1) 603 | 604 | localctx.(*ExpContext).bop = _lt 605 | 606 | _la = p.GetTokenStream().LA(1) 607 | 608 | if !(_la == pluralParserT__9 || _la == pluralParserT__10) { 609 | var _ri = p.GetErrorHandler().RecoverInline(p) 610 | 611 | localctx.(*ExpContext).bop = _ri 612 | } else { 613 | p.GetErrorHandler().ReportMatch(p) 614 | p.Consume() 615 | } 616 | } 617 | { 618 | p.SetState(24) 619 | p.exp(11) 620 | } 621 | 622 | case 4: 623 | localctx = NewExpContext(p, _parentctx, _parentState) 624 | p.PushNewRecursionContext(localctx, _startState, pluralParserRULE_exp) 625 | p.SetState(25) 626 | 627 | if !(p.Precpred(p.GetParserRuleContext(), 9)) { 628 | p.SetError(antlr.NewFailedPredicateException(p, "p.Precpred(p.GetParserRuleContext(), 9)", "")) 629 | goto errorExit 630 | } 631 | { 632 | p.SetState(26) 633 | 634 | var _lt = p.GetTokenStream().LT(1) 635 | 636 | localctx.(*ExpContext).bop = _lt 637 | 638 | _la = p.GetTokenStream().LA(1) 639 | 640 | if !(_la == pluralParserT__11 || _la == pluralParserT__12) { 641 | var _ri = p.GetErrorHandler().RecoverInline(p) 642 | 643 | localctx.(*ExpContext).bop = _ri 644 | } else { 645 | p.GetErrorHandler().ReportMatch(p) 646 | p.Consume() 647 | } 648 | } 649 | { 650 | p.SetState(27) 651 | p.exp(10) 652 | } 653 | 654 | case 5: 655 | localctx = NewExpContext(p, _parentctx, _parentState) 656 | p.PushNewRecursionContext(localctx, _startState, pluralParserRULE_exp) 657 | p.SetState(28) 658 | 659 | if !(p.Precpred(p.GetParserRuleContext(), 8)) { 660 | p.SetError(antlr.NewFailedPredicateException(p, "p.Precpred(p.GetParserRuleContext(), 8)", "")) 661 | goto errorExit 662 | } 663 | { 664 | p.SetState(29) 665 | 666 | var _lt = p.GetTokenStream().LT(1) 667 | 668 | localctx.(*ExpContext).bop = _lt 669 | 670 | _la = p.GetTokenStream().LA(1) 671 | 672 | if !(_la == pluralParserT__13 || _la == pluralParserT__14) { 673 | var _ri = p.GetErrorHandler().RecoverInline(p) 674 | 675 | localctx.(*ExpContext).bop = _ri 676 | } else { 677 | p.GetErrorHandler().ReportMatch(p) 678 | p.Consume() 679 | } 680 | } 681 | { 682 | p.SetState(30) 683 | p.exp(9) 684 | } 685 | 686 | case 6: 687 | localctx = NewExpContext(p, _parentctx, _parentState) 688 | p.PushNewRecursionContext(localctx, _startState, pluralParserRULE_exp) 689 | p.SetState(31) 690 | 691 | if !(p.Precpred(p.GetParserRuleContext(), 7)) { 692 | p.SetError(antlr.NewFailedPredicateException(p, "p.Precpred(p.GetParserRuleContext(), 7)", "")) 693 | goto errorExit 694 | } 695 | { 696 | p.SetState(32) 697 | 698 | var _lt = p.GetTokenStream().LT(1) 699 | 700 | localctx.(*ExpContext).bop = _lt 701 | 702 | _la = p.GetTokenStream().LA(1) 703 | 704 | if !(_la == pluralParserT__15 || _la == pluralParserT__16) { 705 | var _ri = p.GetErrorHandler().RecoverInline(p) 706 | 707 | localctx.(*ExpContext).bop = _ri 708 | } else { 709 | p.GetErrorHandler().ReportMatch(p) 710 | p.Consume() 711 | } 712 | } 713 | { 714 | p.SetState(33) 715 | p.exp(8) 716 | } 717 | 718 | case 7: 719 | localctx = NewExpContext(p, _parentctx, _parentState) 720 | p.PushNewRecursionContext(localctx, _startState, pluralParserRULE_exp) 721 | p.SetState(34) 722 | 723 | if !(p.Precpred(p.GetParserRuleContext(), 6)) { 724 | p.SetError(antlr.NewFailedPredicateException(p, "p.Precpred(p.GetParserRuleContext(), 6)", "")) 725 | goto errorExit 726 | } 727 | { 728 | p.SetState(35) 729 | 730 | var _m = p.Match(pluralParserT__17) 731 | 732 | localctx.(*ExpContext).bop = _m 733 | if p.HasError() { 734 | // Recognition error - abort rule 735 | goto errorExit 736 | } 737 | } 738 | { 739 | p.SetState(36) 740 | p.exp(7) 741 | } 742 | 743 | case 8: 744 | localctx = NewExpContext(p, _parentctx, _parentState) 745 | p.PushNewRecursionContext(localctx, _startState, pluralParserRULE_exp) 746 | p.SetState(37) 747 | 748 | if !(p.Precpred(p.GetParserRuleContext(), 5)) { 749 | p.SetError(antlr.NewFailedPredicateException(p, "p.Precpred(p.GetParserRuleContext(), 5)", "")) 750 | goto errorExit 751 | } 752 | { 753 | p.SetState(38) 754 | 755 | var _m = p.Match(pluralParserT__18) 756 | 757 | localctx.(*ExpContext).bop = _m 758 | if p.HasError() { 759 | // Recognition error - abort rule 760 | goto errorExit 761 | } 762 | } 763 | { 764 | p.SetState(39) 765 | p.exp(6) 766 | } 767 | 768 | case 9: 769 | localctx = NewExpContext(p, _parentctx, _parentState) 770 | p.PushNewRecursionContext(localctx, _startState, pluralParserRULE_exp) 771 | p.SetState(40) 772 | 773 | if !(p.Precpred(p.GetParserRuleContext(), 4)) { 774 | p.SetError(antlr.NewFailedPredicateException(p, "p.Precpred(p.GetParserRuleContext(), 4)", "")) 775 | goto errorExit 776 | } 777 | { 778 | p.SetState(41) 779 | 780 | var _m = p.Match(pluralParserT__19) 781 | 782 | localctx.(*ExpContext).bop = _m 783 | if p.HasError() { 784 | // Recognition error - abort rule 785 | goto errorExit 786 | } 787 | } 788 | { 789 | p.SetState(42) 790 | p.exp(5) 791 | } 792 | 793 | case 10: 794 | localctx = NewExpContext(p, _parentctx, _parentState) 795 | p.PushNewRecursionContext(localctx, _startState, pluralParserRULE_exp) 796 | p.SetState(43) 797 | 798 | if !(p.Precpred(p.GetParserRuleContext(), 3)) { 799 | p.SetError(antlr.NewFailedPredicateException(p, "p.Precpred(p.GetParserRuleContext(), 3)", "")) 800 | goto errorExit 801 | } 802 | { 803 | p.SetState(44) 804 | 805 | var _m = p.Match(pluralParserT__20) 806 | 807 | localctx.(*ExpContext).bop = _m 808 | if p.HasError() { 809 | // Recognition error - abort rule 810 | goto errorExit 811 | } 812 | } 813 | { 814 | p.SetState(45) 815 | p.exp(4) 816 | } 817 | 818 | case 11: 819 | localctx = NewExpContext(p, _parentctx, _parentState) 820 | p.PushNewRecursionContext(localctx, _startState, pluralParserRULE_exp) 821 | p.SetState(46) 822 | 823 | if !(p.Precpred(p.GetParserRuleContext(), 2)) { 824 | p.SetError(antlr.NewFailedPredicateException(p, "p.Precpred(p.GetParserRuleContext(), 2)", "")) 825 | goto errorExit 826 | } 827 | { 828 | p.SetState(47) 829 | 830 | var _m = p.Match(pluralParserT__21) 831 | 832 | localctx.(*ExpContext).bop = _m 833 | if p.HasError() { 834 | // Recognition error - abort rule 835 | goto errorExit 836 | } 837 | } 838 | { 839 | p.SetState(48) 840 | p.exp(3) 841 | } 842 | 843 | case 12: 844 | localctx = NewExpContext(p, _parentctx, _parentState) 845 | p.PushNewRecursionContext(localctx, _startState, pluralParserRULE_exp) 846 | p.SetState(49) 847 | 848 | if !(p.Precpred(p.GetParserRuleContext(), 1)) { 849 | p.SetError(antlr.NewFailedPredicateException(p, "p.Precpred(p.GetParserRuleContext(), 1)", "")) 850 | goto errorExit 851 | } 852 | { 853 | p.SetState(50) 854 | 855 | var _m = p.Match(pluralParserT__22) 856 | 857 | localctx.(*ExpContext).bop = _m 858 | if p.HasError() { 859 | // Recognition error - abort rule 860 | goto errorExit 861 | } 862 | } 863 | { 864 | p.SetState(51) 865 | p.exp(0) 866 | } 867 | { 868 | p.SetState(52) 869 | p.Match(pluralParserT__23) 870 | if p.HasError() { 871 | // Recognition error - abort rule 872 | goto errorExit 873 | } 874 | } 875 | { 876 | p.SetState(53) 877 | p.exp(1) 878 | } 879 | 880 | case 13: 881 | localctx = NewExpContext(p, _parentctx, _parentState) 882 | p.PushNewRecursionContext(localctx, _startState, pluralParserRULE_exp) 883 | p.SetState(55) 884 | 885 | if !(p.Precpred(p.GetParserRuleContext(), 15)) { 886 | p.SetError(antlr.NewFailedPredicateException(p, "p.Precpred(p.GetParserRuleContext(), 15)", "")) 887 | goto errorExit 888 | } 889 | { 890 | p.SetState(56) 891 | 892 | var _lt = p.GetTokenStream().LT(1) 893 | 894 | localctx.(*ExpContext).postfix = _lt 895 | 896 | _la = p.GetTokenStream().LA(1) 897 | 898 | if !(_la == pluralParserT__0 || _la == pluralParserT__1) { 899 | var _ri = p.GetErrorHandler().RecoverInline(p) 900 | 901 | localctx.(*ExpContext).postfix = _ri 902 | } else { 903 | p.GetErrorHandler().ReportMatch(p) 904 | p.Consume() 905 | } 906 | } 907 | 908 | case antlr.ATNInvalidAltNumber: 909 | goto errorExit 910 | } 911 | 912 | } 913 | p.SetState(61) 914 | p.GetErrorHandler().Sync(p) 915 | if p.HasError() { 916 | goto errorExit 917 | } 918 | _alt = p.GetInterpreter().AdaptivePredict(p.BaseParser, p.GetTokenStream(), 2, p.GetParserRuleContext()) 919 | if p.HasError() { 920 | goto errorExit 921 | } 922 | } 923 | 924 | errorExit: 925 | if p.HasError() { 926 | v := p.GetError() 927 | localctx.SetException(v) 928 | p.GetErrorHandler().ReportError(p, v) 929 | p.GetErrorHandler().Recover(p, v) 930 | p.SetError(nil) 931 | } 932 | p.UnrollRecursionContexts(_parentctx) 933 | return localctx 934 | goto errorExit // Trick to prevent compiler error if the label is not used 935 | } 936 | 937 | // IPrimaryContext is an interface to support dynamic dispatch. 938 | type IPrimaryContext interface { 939 | antlr.ParserRuleContext 940 | 941 | // GetParser returns the parser. 942 | GetParser() antlr.Parser 943 | 944 | // Getter signatures 945 | Exp() IExpContext 946 | INT() antlr.TerminalNode 947 | 948 | // IsPrimaryContext differentiates from other interfaces. 949 | IsPrimaryContext() 950 | } 951 | 952 | type PrimaryContext struct { 953 | antlr.BaseParserRuleContext 954 | parser antlr.Parser 955 | } 956 | 957 | func NewEmptyPrimaryContext() *PrimaryContext { 958 | var p = new(PrimaryContext) 959 | antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) 960 | p.RuleIndex = pluralParserRULE_primary 961 | return p 962 | } 963 | 964 | func InitEmptyPrimaryContext(p *PrimaryContext) { 965 | antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, nil, -1) 966 | p.RuleIndex = pluralParserRULE_primary 967 | } 968 | 969 | func (*PrimaryContext) IsPrimaryContext() {} 970 | 971 | func NewPrimaryContext(parser antlr.Parser, parent antlr.ParserRuleContext, invokingState int) *PrimaryContext { 972 | var p = new(PrimaryContext) 973 | 974 | antlr.InitBaseParserRuleContext(&p.BaseParserRuleContext, parent, invokingState) 975 | 976 | p.parser = parser 977 | p.RuleIndex = pluralParserRULE_primary 978 | 979 | return p 980 | } 981 | 982 | func (s *PrimaryContext) GetParser() antlr.Parser { return s.parser } 983 | 984 | func (s *PrimaryContext) Exp() IExpContext { 985 | var t antlr.RuleContext 986 | for _, ctx := range s.GetChildren() { 987 | if _, ok := ctx.(IExpContext); ok { 988 | t = ctx.(antlr.RuleContext) 989 | break 990 | } 991 | } 992 | 993 | if t == nil { 994 | return nil 995 | } 996 | 997 | return t.(IExpContext) 998 | } 999 | 1000 | func (s *PrimaryContext) INT() antlr.TerminalNode { 1001 | return s.GetToken(pluralParserINT, 0) 1002 | } 1003 | 1004 | func (s *PrimaryContext) GetRuleContext() antlr.RuleContext { 1005 | return s 1006 | } 1007 | 1008 | func (s *PrimaryContext) ToStringTree(ruleNames []string, recog antlr.Recognizer) string { 1009 | return antlr.TreesStringTree(s, ruleNames, recog) 1010 | } 1011 | 1012 | func (s *PrimaryContext) EnterRule(listener antlr.ParseTreeListener) { 1013 | if listenerT, ok := listener.(pluralListener); ok { 1014 | listenerT.EnterPrimary(s) 1015 | } 1016 | } 1017 | 1018 | func (s *PrimaryContext) ExitRule(listener antlr.ParseTreeListener) { 1019 | if listenerT, ok := listener.(pluralListener); ok { 1020 | listenerT.ExitPrimary(s) 1021 | } 1022 | } 1023 | 1024 | func (p *pluralParser) Primary() (localctx IPrimaryContext) { 1025 | localctx = NewPrimaryContext(p, p.GetParserRuleContext(), p.GetState()) 1026 | p.EnterRule(localctx, 4, pluralParserRULE_primary) 1027 | p.SetState(68) 1028 | p.GetErrorHandler().Sync(p) 1029 | if p.HasError() { 1030 | goto errorExit 1031 | } 1032 | 1033 | switch p.GetTokenStream().LA(1) { 1034 | case pluralParserT__24: 1035 | p.EnterOuterAlt(localctx, 1) 1036 | { 1037 | p.SetState(62) 1038 | p.Match(pluralParserT__24) 1039 | if p.HasError() { 1040 | // Recognition error - abort rule 1041 | goto errorExit 1042 | } 1043 | } 1044 | { 1045 | p.SetState(63) 1046 | p.exp(0) 1047 | } 1048 | { 1049 | p.SetState(64) 1050 | p.Match(pluralParserT__25) 1051 | if p.HasError() { 1052 | // Recognition error - abort rule 1053 | goto errorExit 1054 | } 1055 | } 1056 | 1057 | case pluralParserT__26: 1058 | p.EnterOuterAlt(localctx, 2) 1059 | { 1060 | p.SetState(66) 1061 | p.Match(pluralParserT__26) 1062 | if p.HasError() { 1063 | // Recognition error - abort rule 1064 | goto errorExit 1065 | } 1066 | } 1067 | 1068 | case pluralParserINT: 1069 | p.EnterOuterAlt(localctx, 3) 1070 | { 1071 | p.SetState(67) 1072 | p.Match(pluralParserINT) 1073 | if p.HasError() { 1074 | // Recognition error - abort rule 1075 | goto errorExit 1076 | } 1077 | } 1078 | 1079 | default: 1080 | p.SetError(antlr.NewNoViableAltException(p, nil, nil, nil, nil, nil)) 1081 | goto errorExit 1082 | } 1083 | 1084 | errorExit: 1085 | if p.HasError() { 1086 | v := p.GetError() 1087 | localctx.SetException(v) 1088 | p.GetErrorHandler().ReportError(p, v) 1089 | p.GetErrorHandler().Recover(p, v) 1090 | p.SetError(nil) 1091 | } 1092 | p.ExitRule() 1093 | return localctx 1094 | goto errorExit // Trick to prevent compiler error if the label is not used 1095 | } 1096 | 1097 | func (p *pluralParser) Sempred(localctx antlr.RuleContext, ruleIndex, predIndex int) bool { 1098 | switch ruleIndex { 1099 | case 1: 1100 | var t *ExpContext = nil 1101 | if localctx != nil { 1102 | t = localctx.(*ExpContext) 1103 | } 1104 | return p.Exp_Sempred(t, predIndex) 1105 | 1106 | default: 1107 | panic("No predicate with index: " + fmt.Sprint(ruleIndex)) 1108 | } 1109 | } 1110 | 1111 | func (p *pluralParser) Exp_Sempred(localctx antlr.RuleContext, predIndex int) bool { 1112 | switch predIndex { 1113 | case 0: 1114 | return p.Precpred(p.GetParserRuleContext(), 12) 1115 | 1116 | case 1: 1117 | return p.Precpred(p.GetParserRuleContext(), 11) 1118 | 1119 | case 2: 1120 | return p.Precpred(p.GetParserRuleContext(), 10) 1121 | 1122 | case 3: 1123 | return p.Precpred(p.GetParserRuleContext(), 9) 1124 | 1125 | case 4: 1126 | return p.Precpred(p.GetParserRuleContext(), 8) 1127 | 1128 | case 5: 1129 | return p.Precpred(p.GetParserRuleContext(), 7) 1130 | 1131 | case 6: 1132 | return p.Precpred(p.GetParserRuleContext(), 6) 1133 | 1134 | case 7: 1135 | return p.Precpred(p.GetParserRuleContext(), 5) 1136 | 1137 | case 8: 1138 | return p.Precpred(p.GetParserRuleContext(), 4) 1139 | 1140 | case 9: 1141 | return p.Precpred(p.GetParserRuleContext(), 3) 1142 | 1143 | case 10: 1144 | return p.Precpred(p.GetParserRuleContext(), 2) 1145 | 1146 | case 11: 1147 | return p.Precpred(p.GetParserRuleContext(), 1) 1148 | 1149 | case 12: 1150 | return p.Precpred(p.GetParserRuleContext(), 15) 1151 | 1152 | default: 1153 | panic("No predicate with index: " + fmt.Sprint(predIndex)) 1154 | } 1155 | } 1156 | -------------------------------------------------------------------------------- /plurals/plural.g4: -------------------------------------------------------------------------------- 1 | grammar plural; 2 | 3 | // 关于复数表达式的官方描述: 4 | // https://www.gnu.org/software/gettext/manual/html_node/Plural-forms.html#index-specifying-plural-form-in-a-PO-file 5 | // plural 表达式是 C 语法的表达式,但不允许负数出现,而且只能出现整数,变量只允许 n。可以有空白,但不能反斜杠换行。 6 | 7 | // 生成 Go 代码的命令 (在本目录下执行): antlr4 -Dlanguage=Go plural.g4 -o parser 8 | 9 | start: exp; 10 | exp: 11 | primary 12 | | exp postfix = ('++' | '--') 13 | | prefix = ('+' | '-' | '++' | '--') exp 14 | | prefix = ('~' | '!') exp 15 | | exp bop = ('*' | '/' | '%') exp 16 | | exp bop = ('+' | '-') exp 17 | | exp bop = ('>>' | '<<') exp 18 | | exp bop = ('>' | '<') exp 19 | | exp bop = ('>=' | '<=') exp 20 | | exp bop = ('==' | '!=') exp 21 | | exp bop = '&' exp 22 | | exp bop = '^' exp 23 | | exp bop = '|' exp 24 | | exp bop = '&&' exp 25 | | exp bop = '||' exp 26 | | exp bop = '?' exp ':' exp; 27 | primary: '(' exp ')' | 'n' | INT; 28 | INT: [0-9]+; 29 | WS: [ \t]+ -> skip; 30 | -------------------------------------------------------------------------------- /plurals/stack.go: -------------------------------------------------------------------------------- 1 | package plurals 2 | 3 | import "errors" 4 | 5 | // A simple integer stack 6 | 7 | // Int64Stack see antlr.IntStack 8 | type Int64Stack []int64 9 | 10 | // ErrEmptyStack returned when call Pop on empty stack 11 | var ErrEmptyStack = errors.New("stack is empty") 12 | 13 | // Pop pop a number from stack 14 | func (s *Int64Stack) Pop() (int64, error) { 15 | l := len(*s) - 1 16 | if l < 0 { 17 | return 0, ErrEmptyStack 18 | } 19 | v := (*s)[l] 20 | *s = (*s)[0:l] 21 | return v, nil 22 | } 23 | 24 | // Push push a number to stack 25 | func (s *Int64Stack) Push(e int64) { 26 | *s = append(*s, e) 27 | } 28 | -------------------------------------------------------------------------------- /tag.go: -------------------------------------------------------------------------------- 1 | package t 2 | 3 | import ( 4 | "sync" 5 | 6 | "golang.org/x/text/language" 7 | ) 8 | 9 | var cachedTag sync.Map 10 | 11 | func Tag(locale string) language.Tag { 12 | if v, ok := cachedTag.Load(locale); ok { 13 | return v.(language.Tag) 14 | } 15 | tag := language.Make(locale) 16 | cachedTag.Store(locale, tag) 17 | return tag 18 | } 19 | 20 | func Tags(locales []string) (tags []language.Tag) { 21 | for _, locale := range locales { 22 | tags = append(tags, Tag(locale)) 23 | } 24 | return 25 | } 26 | -------------------------------------------------------------------------------- /tag_test.go: -------------------------------------------------------------------------------- 1 | package t 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "golang.org/x/text/language" 8 | ) 9 | 10 | func TestTags(t *testing.T) { 11 | type args struct { 12 | locales []string 13 | } 14 | tests := []struct { 15 | name string 16 | args args 17 | wantTags []language.Tag 18 | }{ 19 | {"tags", args{[]string{"zh", "zh"}}, []language.Tag{language.Make("zh"), language.Make("zh")}}, 20 | } 21 | for _, tt := range tests { 22 | t.Run(tt.name, func(t *testing.T) { 23 | if gotTags := Tags(tt.args.locales); !reflect.DeepEqual(gotTags, tt.wantTags) { 24 | t.Errorf("Tags() = %v, want %v", gotTags, tt.wantTags) 25 | } 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /testdata/messages.new.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2024-01-05 16:04:11+0800\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" 20 | "X-Created-By: xtpl(https://github.com/pub-go/tpl/tree/main/cmd/xtpl)\n" 21 | "X-xTpl-Path: .\n" 22 | "X-xTpl-Pattern: .*\\.html\n" 23 | "X-xTpl-Keywords: T;N:1,2;N64:1,2;X:1c,2;XN:1c,2,3;XN64:1c,2,3;__;_n:1,2;_x:1c,2;_xn:1c,2,3\n" 24 | "X-xTpl-Output: testdata/messages.pot\n" 25 | 26 | #: testdata/index.html:6:24 27 | msgid "Title" 28 | msgstr "" 29 | 30 | #: testdata/index.html:9:21 31 | msgid "How are you?" 32 | msgstr "" 33 | 34 | #: testdata/index.html:11:23 35 | msgid "And you?" 36 | msgstr "" 37 | 38 | #: testdata/index.html:12:20 39 | msgid "Hello, %s" 40 | msgstr "" 41 | 42 | #: testdata/index.html:13:32 43 | msgctxt "post new" 44 | msgid "Post" 45 | msgstr "" 46 | 47 | #: testdata/index.html:14:29 48 | msgctxt "posts" 49 | msgid "Post" 50 | msgstr "" 51 | 52 | #: testdata/index.html:15:20 53 | msgid "One Apple" 54 | msgid_plural "%d Apples" 55 | msgstr[0] "" 56 | msgstr[1] "" 57 | 58 | #: testdata/index.html:16:28 59 | msgctxt "ctx" 60 | msgid "One Book" 61 | msgid_plural "%d Books" 62 | msgstr[0] "" 63 | msgstr[1] "" 64 | 65 | -------------------------------------------------------------------------------- /testdata/messages.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2024-01-05 16:04:11+0800\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" 20 | "X-Created-By: xtpl(https://github.com/pub-go/tpl/tree/main/cmd/xtpl)\n" 21 | "X-xTpl-Path: .\n" 22 | "X-xTpl-Pattern: .*\\.html\n" 23 | "X-xTpl-Keywords: T;N:1,2;N64:1,2;X:1c,2;XN:1c,2,3;XN64:1c,2,3;__;_n:1,2;_x:1c,2;_xn:1c,2,3\n" 24 | "X-xTpl-Output: testdata/messages.pot\n" 25 | 26 | #: testdata/index.html:11:23 27 | msgid "And you?" 28 | msgstr "" 29 | 30 | #: testdata/index.html:12:20 31 | msgid "Hello, %s" 32 | msgstr "" 33 | 34 | #: testdata/index.html:9:21 35 | msgid "How are you?" 36 | msgstr "" 37 | 38 | #: testdata/index.html:15:20 39 | msgid "One Apple" 40 | msgid_plural "%d Apples" 41 | msgstr[0] "" 42 | msgstr[1] "" 43 | 44 | #: testdata/index.html:6:24 45 | msgid "Title" 46 | msgstr "" 47 | 48 | #: testdata/index.html:16:28 49 | msgctxt "ctx" 50 | msgid "One Book" 51 | msgid_plural "%d Books" 52 | msgstr[0] "" 53 | msgstr[1] "" 54 | 55 | #: testdata/index.html:13:32 56 | msgctxt "post new" 57 | msgid "Post" 58 | msgstr "" 59 | 60 | #: testdata/index.html:14:29 61 | msgctxt "posts" 62 | msgid "Post" 63 | msgstr "" 64 | 65 | -------------------------------------------------------------------------------- /testdata/zh_CN.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youthlin/t/595798ea5b90b92d8eb7aedbcf0e05c177990028/testdata/zh_CN.mo -------------------------------------------------------------------------------- /testdata/zh_CN.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: testdata of github.com/youthlin/t\n" 4 | "POT-Creation-Date: 2021-08-24 13:45+0800\n" 5 | "PO-Revision-Date: 2021-08-24 13:45+0800\n" 6 | "Last-Translator: Lin \n" 7 | "Language-Team: https://youthlin.com/\n" 8 | "Language: zh_CN\n" 9 | "MIME-Version: 1.0\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "X-Generator: Poedit 2.4.2\n" 13 | "X-Poedit-Basepath: ..\n" 14 | "Plural-Forms: nplurals=1; plural=0;\n" 15 | "X-Poedit-KeywordsList: T:1;N:1,2;N64:1,2;X:2,1c;XN:2,3,1c;XN64:2,3,1c\n" 16 | "X-Poedit-SourceCharset: UTF-8\n" 17 | "X-Poedit-SearchPath-0: .\n" 18 | "X-Poedit-SearchPathExcluded-0: cmd\n" 19 | 20 | #: example_test.go:28 example_test.go:40 example_test.go:56 example_test.go:67 21 | #: example_test.go:72 example_test.go:114 example_test.go:116 22 | #: example_test.go:128 23 | msgid "Hello, World" 24 | msgstr "你好,世界" 25 | 26 | #: example_test.go:96 27 | msgid "你好,世界" 28 | msgstr "" 29 | 30 | #: example_test.go:129 31 | #, c-format 32 | msgid "Hello, %s" 33 | msgstr "你好,%s" 34 | 35 | #: example_test.go:130 example_test.go:131 example_test.go:132 36 | #: example_test.go:133 37 | #, c-format 38 | msgid "One apple" 39 | msgid_plural "%d apples" 40 | msgstr[0] "%d 个苹果" 41 | 42 | #: example_test.go:135 43 | msgctxt "File|" 44 | msgid "Open" 45 | msgstr "打开文件" 46 | 47 | #: example_test.go:136 48 | msgctxt "Project|" 49 | msgid "Open" 50 | msgstr "打开工程" 51 | 52 | #: example_test.go:137 53 | #, c-format 54 | msgctxt "File|" 55 | msgid "Open One" 56 | msgid_plural "Open %d" 57 | msgstr[0] "打开 %d 个文件" 58 | 59 | #: example_test.go:138 example_test.go:139 60 | #, c-format 61 | msgctxt "Project|" 62 | msgid "Open One" 63 | msgid_plural "Open %d" 64 | msgstr[0] "打开 %d 个工程" 65 | -------------------------------------------------------------------------------- /testdata/zh_CN_save.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youthlin/t/595798ea5b90b92d8eb7aedbcf0e05c177990028/testdata/zh_CN_save.mo -------------------------------------------------------------------------------- /translations.go: -------------------------------------------------------------------------------- 1 | package t 2 | 3 | import ( 4 | "io/fs" 5 | "sort" 6 | 7 | "github.com/youthlin/t/locale" 8 | ) 9 | 10 | // DefaultDomain 默认的文本域 11 | const DefaultDomain = "default" 12 | 13 | // DefaultSourceCodeLocale 默认的源代码语言 14 | const DefaultSourceCodeLocale = "en_US" 15 | 16 | // Translations holds several translation domains 17 | // ts. [翻译集]包含多个翻译,每个翻译分别属于一个文本域 18 | type Translations struct { 19 | locale string 20 | domain string 21 | domains map[string]*Translation // key is domain 22 | // sourceCodeLocale 源代码中的语言, 通常应该使用英文 23 | sourceCodeLocale string 24 | } 25 | 26 | // NewTranslations create a new Translations 新建翻译集 27 | func NewTranslations() *Translations { 28 | return &Translations{ 29 | locale: locale.GetDefault(), 30 | domain: DefaultDomain, 31 | domains: make(map[string]*Translation), 32 | sourceCodeLocale: DefaultSourceCodeLocale, 33 | } 34 | } 35 | 36 | // clone clones a Translations 37 | func (ts *Translations) clone() *Translations { 38 | return &Translations{ 39 | locale: ts.locale, 40 | domain: ts.domain, 41 | domains: func() map[string]*Translation { 42 | m := make(map[string]*Translation) 43 | for d, tr := range ts.domains { 44 | m[d] = tr 45 | } 46 | return m 47 | }(), 48 | sourceCodeLocale: ts.sourceCodeLocale, 49 | } 50 | } 51 | 52 | // BindFS load a Translation form file system and bind to a domain 53 | // 从文件系统绑定翻译域 54 | func (ts *Translations) BindFS(domain string, fsys fs.FS) { 55 | tr := NewTranslation(domain) 56 | if tr.LoadFS(fsys) { 57 | ts.domains[domain] = tr 58 | } 59 | } 60 | 61 | // Domain return current domain 返回当前使用的文本域 62 | func (ts *Translations) Domain() string { 63 | return ts.domain 64 | } 65 | 66 | // SetDomain set current domain 设置要使用的文本域 67 | func (ts *Translations) SetDomain(domain string) { 68 | ts.domain = domain 69 | } 70 | 71 | // HasDomain return true if ts has the specified domain 72 | func (ts *Translations) HasDomain(domain string) bool { 73 | for d := range ts.domains { 74 | if d == domain { 75 | return true 76 | } 77 | } 78 | return false 79 | } 80 | 81 | // Domains return all domains 82 | func (ts *Translations) Domains() (domains []string) { 83 | for domain := range ts.domains { 84 | domains = append(domains, domain) 85 | } 86 | return 87 | } 88 | 89 | // Locale return current locale 返回设置的希望使用的语言 90 | func (ts *Translations) Locale() string { 91 | return ts.locale 92 | } 93 | 94 | // Locales return all supported locales of domain 返回文本域中支持的所有语言 95 | func (ts *Translations) Locales() (locales []string) { 96 | tr := ts.GetOrNoop(ts.domain) 97 | m := make(map[string]struct{}, len(tr.langs)+1) 98 | m[ts.sourceCodeLocale] = struct{}{} 99 | locales = append(locales, ts.sourceCodeLocale) 100 | for lang := range tr.langs { 101 | lang = locale.Normalize(lang) 102 | if _, ok := m[lang]; !ok { 103 | m[lang] = struct{}{} 104 | locales = append(locales, lang) 105 | } 106 | } 107 | sort.Strings(locales) 108 | return 109 | } 110 | 111 | // MostMatchLocale return the most match language 返回最匹配的语言 112 | func (ts *Translations) MostMatchLocale() string { 113 | var supported = Locales() 114 | _, index, _ := Match(supported, []string{Locale()}) 115 | return supported[index] 116 | } 117 | 118 | // UsedLocale return the locale that actually used 119 | func (ts *Translations) UsedLocale() string { 120 | tr, ok := ts.Get(ts.domain) 121 | if !ok { 122 | return ts.sourceCodeLocale 123 | } 124 | _, ok = tr.Get(ts.locale) 125 | if !ok { 126 | return ts.sourceCodeLocale 127 | } 128 | return ts.locale 129 | } 130 | 131 | // SetLocale set current locale 设置要使用的语言 132 | func (ts *Translations) SetLocale(lang string) { 133 | if lang == "" { 134 | lang = locale.GetDefault() 135 | } else { 136 | lang = locale.Normalize(lang) 137 | } 138 | ts.locale = lang 139 | } 140 | 141 | // SourceCodeLocale 设置源代码语言 142 | func (ts *Translations) SourceCodeLocale() string { return ts.sourceCodeLocale } 143 | 144 | // SetSourceCodeLocale 设置源代码语言 145 | func (ts *Translations) SetSourceCodeLocale(lang string) { 146 | lang = locale.Normalize(lang) 147 | ts.sourceCodeLocale = lang 148 | } 149 | 150 | // Get return the Translation of the specified domain 151 | // 获取指定的的翻译域 152 | func (ts *Translations) Get(domain string) (*Translation, bool) { 153 | tr, ok := ts.domains[domain] 154 | return tr, ok 155 | } 156 | 157 | // GetOrNoop return the Translation of the specified domain 158 | // 获取指定的的翻译域 159 | func (ts *Translations) GetOrNoop(domain string) *Translation { 160 | if tr, ok := ts.domains[domain]; ok { 161 | return tr 162 | } 163 | return trNoop 164 | } 165 | 166 | // D return a new Translations with domain 167 | func (ts *Translations) D(domain string) *Translations { 168 | result := ts.clone() 169 | result.SetDomain(domain) 170 | return result 171 | } 172 | 173 | // L return a new Translations with locale 174 | func (ts *Translations) L(locale string) *Translations { 175 | result := ts.clone() 176 | result.SetLocale(locale) 177 | return result 178 | } 179 | 180 | // T is a short name of gettext 181 | func (ts *Translations) T(msgID string, args ...interface{}) string { 182 | return ts.X("", msgID, args...) 183 | } 184 | 185 | // N is a short name of nettext 186 | func (ts *Translations) N(msgID, msgIDPlural string, n int, args ...interface{}) string { 187 | return ts.XN64("", msgID, msgIDPlural, int64(n), args...) 188 | } 189 | 190 | // N64 is a short name of nettext 191 | func (ts *Translations) N64(msgID, msgIDPlural string, n int64, args ...interface{}) string { 192 | return ts.XN64("", msgID, msgIDPlural, n, args...) 193 | } 194 | 195 | // X is a short name of pgettext 196 | func (ts *Translations) X(msgCtxt, msgID string, args ...interface{}) string { 197 | tr := ts.GetOrNoop(ts.domain) 198 | tor := tr.GetOrNoop(ts.locale) 199 | return tor.X(msgCtxt, msgID, args...) 200 | } 201 | 202 | // XN is a short name of npgettext 203 | func (ts *Translations) XN(msgCtxt, msgID, msgIDPlural string, n int, args ...interface{}) string { 204 | return ts.XN64(msgCtxt, msgID, msgIDPlural, int64(n), args...) 205 | } 206 | 207 | // XN64 is a short name of npgettext 208 | func (ts *Translations) XN64(msgCtxt, msgID, msgIDPlural string, n int64, args ...interface{}) string { 209 | tr := ts.GetOrNoop(ts.domain) 210 | tor := tr.GetOrNoop(ts.locale) 211 | return tor.XN64(msgCtxt, msgID, msgIDPlural, n, args...) 212 | } 213 | -------------------------------------------------------------------------------- /translaton.go: -------------------------------------------------------------------------------- 1 | package t 2 | 3 | import ( 4 | "io" 5 | "io/fs" 6 | "strings" 7 | 8 | "github.com/youthlin/t/locale" 9 | "github.com/youthlin/t/translator" 10 | ) 11 | 12 | const ( 13 | extPo = ".po" 14 | extMo = ".mo" 15 | ) 16 | 17 | // trNoop is a no-op Translation 18 | var trNoop = NewTranslation("") 19 | 20 | // Translation can provide different language translation of a domain 21 | // tr. [翻译域]包含各个语言的翻译 22 | type Translation struct { 23 | domain string 24 | langs map[string]Translator // key is language 25 | } 26 | 27 | // NewTranslation create a new Translation 28 | func NewTranslation(domain string, translators ...Translator) *Translation { 29 | tr := &Translation{domain: domain, langs: make(map[string]translator.Translator)} 30 | for _, tor := range translators { 31 | tr.AddOrReplace(tor) 32 | } 33 | return tr 34 | } 35 | 36 | // AddOrReplace add a translator and return the previous translator of this language 37 | func (tr *Translation) AddOrReplace(tor Translator) Translator { 38 | lang := tor.Lang() 39 | if lang == "" { 40 | return nil 41 | } 42 | lang = locale.Normalize(lang) 43 | if pre, ok := tr.langs[lang]; ok { 44 | tr.langs[lang] = tor 45 | return pre 46 | } 47 | tr.langs[lang] = tor 48 | return nil 49 | } 50 | 51 | // Get get the Translator of the specified lang 52 | func (tr *Translation) Get(lang string) (Translator, bool) { 53 | tor, ok := tr.langs[lang] 54 | return tor, ok 55 | } 56 | 57 | // GetOrNoop return the Translator of the specified language 58 | // 获取指定语言的翻译 59 | func (tr *Translation) GetOrNoop(lang string) Translator { 60 | if tor, ok := tr.langs[lang]; ok { 61 | return tor 62 | } 63 | return noopTranslator 64 | } 65 | 66 | // LoadFS load a translator from file system 67 | func (tr *Translation) LoadFS(f fs.FS) bool { 68 | var loaded = false 69 | fn := func(ext string) func(path string, d fs.DirEntry, err error) error { 70 | return func(path string, d fs.DirEntry, err error) error { 71 | if err != nil { 72 | return err 73 | } 74 | if d != nil && !d.IsDir() { 75 | if strings.HasSuffix(d.Name(), ext) { // 这里应该使用 d.Name; 76 | of, err := f.Open(path) // 这里应该使用 path: file asFS 时 path=. d.Name=file name 77 | if err == nil { 78 | defer of.Close() 79 | if err := tr.LoadFile(of); err == nil { 80 | loaded = true 81 | } 82 | } 83 | } 84 | } 85 | return nil 86 | } 87 | } 88 | fs.WalkDir(f, ".", fn(extMo)) 89 | fs.WalkDir(f, ".", fn(extPo)) 90 | return loaded 91 | } 92 | 93 | // LoadFile load a translator from a file 94 | func (tr *Translation) LoadFile(file fs.File) error { 95 | fi, err := file.Stat() 96 | if err != nil { 97 | return err 98 | } 99 | fileName := fi.Name() 100 | content, err := io.ReadAll(file) 101 | if err != nil { 102 | return err 103 | } 104 | if strings.HasSuffix(fileName, extPo) { 105 | err = tr.LoadPo(content) 106 | } else if strings.HasSuffix(fileName, extMo) { 107 | err = tr.LoadMo(content) 108 | } 109 | return err 110 | } 111 | 112 | // LoadPo load po file 113 | func (tr *Translation) LoadPo(content []byte) error { 114 | poFile, err := translator.ReadPo(content) 115 | if err != nil { 116 | return err 117 | } 118 | tr.AddOrReplace(poFile) 119 | return nil 120 | } 121 | 122 | // LoadMo load mo file 123 | func (tr *Translation) LoadMo(content []byte) error { 124 | moFile, err := translator.ReadMo(content) 125 | if err != nil { 126 | return err 127 | } 128 | tr.AddOrReplace(moFile) 129 | return nil 130 | } 131 | -------------------------------------------------------------------------------- /translator.go: -------------------------------------------------------------------------------- 1 | package t 2 | 3 | import ( 4 | "github.com/youthlin/t/f" 5 | "github.com/youthlin/t/translator" 6 | ) 7 | 8 | // Translator 翻译接口 9 | type Translator = translator.Translator 10 | 11 | // NoopTranslator return a no-op Translator 12 | func NoopTranslator() Translator { return noopTranslator } 13 | 14 | var noopTranslator Translator = (*nooptor)(nil) 15 | 16 | type nooptor struct{} 17 | 18 | func (tor *nooptor) Lang() string { return "" } 19 | func (tor *nooptor) X(msgCtxt, msgID string, args ...interface{}) string { 20 | return f.Format(msgID, args...) 21 | } 22 | func (tor *nooptor) XN64(msgCtxt, msgID, msgIDPlural string, n int64, args ...interface{}) string { 23 | return f.DefaultPlural(msgID, msgIDPlural, n, args...) 24 | } 25 | -------------------------------------------------------------------------------- /translator/a_test.go: -------------------------------------------------------------------------------- 1 | package translator_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/youthlin/t" 7 | ) 8 | 9 | func Test(tt *testing.T) { 10 | // 每行 77 max 11 | // xgettext -C -kT translator/a_test.go -o - 12 | tt.Logf(t.T("1234567890123456789012345678901234567890123456789012345678901234567890123456 ")) 13 | tt.Logf(t.T("1234567890123456789012345678901234567890123456789012345678901234567890123456 8")) 14 | tt.Logf(t.T("1234567890123456789012345678901234567890123456789012345678901234567890123456\n\n")) 15 | tt.Logf(t.T("1234567890123456789012345678901234567890123456789012345678901234567890123456\n")) 16 | tt.Logf(t.T("Content-Transfer-Encoding: 8bit\nPlural-Forms: nplurals=1; plural=0;\n")) 17 | } 18 | -------------------------------------------------------------------------------- /translator/entry.go: -------------------------------------------------------------------------------- 1 | package translator 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | // Entry 一个翻译条目 10 | type Entry struct { 11 | MsgCmts []string 12 | MsgCtxt string 13 | MsgID string 14 | MsgID2 string 15 | MsgStr string 16 | MsgStrN []string 17 | } 18 | 19 | // Key 一个翻译条目的 Key 20 | func (e *Entry) Key() string { 21 | return key(e.MsgCtxt, e.MsgID) 22 | } 23 | 24 | // isHeader 返回该条目是否是一个 header 条目 25 | func (e *Entry) isHeader() bool { 26 | return e.MsgID == "" 27 | } 28 | 29 | // isValid 是否合法的条目 30 | func (e *Entry) isValid() bool { 31 | // header entry: msgid == "" 32 | return e != nil && (e.MsgStr != "" || len(e.MsgStrN) > 0) 33 | } 34 | 35 | // getSortKey 排序 36 | func (e *Entry) getSortKey() string { 37 | if e.isHeader() { 38 | return "" // 排在最前 39 | } 40 | return e.getLineString() + e.Key() 41 | } 42 | 43 | // getLineString 如果有行号注释按行号排序 非行号的注释不使用 44 | func (e *Entry) getLineString() string { 45 | var ss []string 46 | for _, cmt := range e.MsgCmts { 47 | // #: testdata/index.html:16:28 testdata/index.html:18 48 | if strings.HasPrefix(cmt, "#:") { // 行号注释前缀 49 | cmt = strings.TrimPrefix(cmt, "#:") 50 | var s []string 51 | s = append(s, "#:") 52 | for _, item := range strings.Split(cmt, " ") { // 多个位置 53 | pair := strings.Split(item, ":") // 文件名:行号[:列号] 54 | result := make([]string, 0, len(pair)) 55 | for _, n := range pair { 56 | i, err := strconv.ParseInt(n, 10, 64) 57 | if err != nil { 58 | result = append(result, n) 59 | } else { // 数字添加前导 0,这样字符串比较时 012<123 60 | result = append(result, fmt.Sprintf("%10d", i)) 61 | } 62 | } 63 | s = append(s, result...) 64 | } 65 | ss = append(ss, s...) 66 | } 67 | } 68 | return strings.Join(ss, "") 69 | } 70 | -------------------------------------------------------------------------------- /translator/file.go: -------------------------------------------------------------------------------- 1 | package translator 2 | 3 | import ( 4 | "regexp" 5 | "sort" 6 | "strings" 7 | 8 | "github.com/youthlin/t/f" 9 | ) 10 | 11 | const ( 12 | // HeaderPluralForms 表明该语言的复数形式 13 | HeaderPluralForms = "Plural-Forms" 14 | // HeaderLanguage 表明该文件是什么语言 15 | HeaderLanguage = "Language" 16 | ) 17 | 18 | var _ Translator = (*File)(nil) // 触发编译检查,是否实现接口 19 | var reHeader = regexp.MustCompile(`(.*?): (.*)`) 20 | 21 | // File 一个翻译文件 22 | type File struct { 23 | entries map[string]*Entry 24 | headers map[string]string 25 | plural *plural 26 | } 27 | 28 | // Lang get this translations' language 29 | func (file *File) Lang() string { 30 | lang, _ := file.GetHeader(HeaderLanguage) 31 | return lang 32 | } 33 | 34 | // X is ashort name for pgettext 35 | func (file *File) X(msgCtxt, msgID string, args ...interface{}) string { 36 | entry, ok := file.entries[key(msgCtxt, msgID)] 37 | if !ok || entry.MsgStr == "" { 38 | return f.Format(msgID, args...) 39 | } 40 | return f.Format(entry.MsgStr, args...) 41 | } 42 | 43 | // XN64 is ashort name for npgettext 44 | func (file *File) XN64(msgCtxt, msgID, msgIDPlural string, n int64, args ...interface{}) string { 45 | entry, ok := file.entries[key(msgCtxt, msgID)] 46 | if !ok { 47 | return f.DefaultPlural(msgID, msgIDPlural, n, args...) 48 | } 49 | plural := file.getPlural() 50 | if plural.totalForms <= 0 || plural.fn == nil { 51 | return f.DefaultPlural(msgID, msgIDPlural, n, args...) 52 | } 53 | index := plural.fn(n) 54 | if index < 0 || index >= int(plural.totalForms) || index > len(entry.MsgStrN) || entry.MsgStrN[index] == "" { 55 | // 超出范围 56 | return f.DefaultPlural(msgID, msgIDPlural, n, args...) 57 | } 58 | return f.Format(entry.MsgStrN[index], args...) 59 | } 60 | 61 | // SortedEntry sort entry by key 62 | func (file *File) SortedEntry() (entries []*Entry) { 63 | for _, e := range file.entries { 64 | entries = append(entries, e) 65 | } 66 | sort.Slice(entries, func(i, j int) bool { 67 | left := entries[i] 68 | right := entries[j] 69 | return left.getSortKey() < right.getSortKey() 70 | }) 71 | return 72 | } 73 | 74 | // AddEntry adds a Entry 75 | func (file *File) AddEntry(e *Entry) { 76 | if file.entries == nil { 77 | file.entries = map[string]*Entry{} 78 | } 79 | file.entries[e.Key()] = e 80 | if e.isHeader() { 81 | file.initHeader() 82 | file.initPlural() 83 | } 84 | } 85 | 86 | // GetHeader get header value by key 87 | func (file *File) GetHeader(key string) (value string, ok bool) { 88 | file.initHeader() 89 | value, ok = file.headers[key] 90 | return 91 | } 92 | 93 | func (file *File) initHeader() { 94 | if file.headers == nil { 95 | headers := make(map[string]string) 96 | if headerEntry, ok := file.entries[key("", "")]; ok { 97 | kvs := strings.Split(headerEntry.MsgStr, "\n") 98 | for _, kv := range kvs { 99 | if kv == "" { 100 | continue 101 | } 102 | find := reHeader.FindAllStringSubmatch(kv, -1) 103 | if len(find) != 1 || len(find[0]) != 3 { 104 | continue 105 | } 106 | kv := find[0] 107 | k := strings.TrimSpace(kv[1]) 108 | v := strings.TrimSpace(kv[2]) 109 | if _, ok := headers[k]; !ok { 110 | headers[k] = v 111 | } 112 | } 113 | } 114 | file.headers = headers 115 | } 116 | } 117 | 118 | func (file *File) getPlural() *plural { 119 | file.initPlural() 120 | return file.plural 121 | } 122 | 123 | func (file *File) initPlural() { 124 | if file.plural == nil { 125 | forms, _ := file.GetHeader(HeaderPluralForms) 126 | file.plural = parsePlural(forms) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /translator/file_test.go: -------------------------------------------------------------------------------- 1 | package translator 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestFile_Lang(t *testing.T) { 9 | type fields struct { 10 | entries map[string]*Entry 11 | headers map[string]string 12 | plural *plural 13 | } 14 | tests := []struct { 15 | name string 16 | fields fields 17 | want string 18 | }{ 19 | {"empty", fields{}, ""}, 20 | {"header", fields{headers: map[string]string{HeaderLanguage: "zh_CN"}}, "zh_CN"}, 21 | {"entry", fields{entries: map[string]*Entry{ 22 | key("", ""): {MsgStr: "Language: zh_CN"}, 23 | }}, "zh_CN"}, 24 | } 25 | for _, tt := range tests { 26 | t.Run(tt.name, func(t *testing.T) { 27 | file := &File{ 28 | entries: tt.fields.entries, 29 | headers: tt.fields.headers, 30 | plural: tt.fields.plural, 31 | } 32 | if got := file.Lang(); got != tt.want { 33 | t.Errorf("File.Lang() = %v, want %v", got, tt.want) 34 | } 35 | }) 36 | } 37 | } 38 | 39 | func TestFile_T(t *testing.T) { 40 | type fields struct { 41 | entries map[string]*Entry 42 | headers map[string]string 43 | plural *plural 44 | } 45 | type args struct { 46 | msgID string 47 | args []interface{} 48 | } 49 | tests := []struct { 50 | name string 51 | fields fields 52 | args args 53 | want string 54 | }{ 55 | {"empty", fields{}, args{"hello", []interface{}{}}, "hello"}, 56 | {"empty-args", fields{}, args{"hello %s", []interface{}{"world"}}, "hello world"}, 57 | {"t", fields{ 58 | entries: map[string]*Entry{ 59 | key("", "hello"): {MsgStr: "你好"}, 60 | }, 61 | }, args{"hello", []interface{}{}}, "你好"}, 62 | {"t-args", fields{ 63 | entries: map[string]*Entry{ 64 | key("", "hello %s"): {MsgStr: "你好 %s"}, 65 | }, 66 | }, args{"hello %s", []interface{}{"world"}}, "你好 world"}, 67 | } 68 | for _, tt := range tests { 69 | t.Run(tt.name, func(t *testing.T) { 70 | file := &File{ 71 | entries: tt.fields.entries, 72 | headers: tt.fields.headers, 73 | plural: tt.fields.plural, 74 | } 75 | if got := file.X("", tt.args.msgID, tt.args.args...); got != tt.want { 76 | t.Errorf("File.T() = %v, want %v", got, tt.want) 77 | } 78 | }) 79 | } 80 | } 81 | 82 | func TestFile_N(t *testing.T) { 83 | type fields struct { 84 | entries map[string]*Entry 85 | headers map[string]string 86 | plural *plural 87 | } 88 | type args struct { 89 | msgID string 90 | msgIDPlural string 91 | n int 92 | args []interface{} 93 | } 94 | tests := []struct { 95 | name string 96 | fields fields 97 | args args 98 | want string 99 | }{ 100 | {"empty-no-arg-single", fields{}, args{ 101 | "one apple", 102 | "%d apples", 103 | 1, 104 | []interface{}{}, 105 | }, "one apple"}, 106 | {"empty-no-arg-plural", fields{}, args{ 107 | "one apple", 108 | "%d apples", 109 | 2, 110 | []interface{}{}, 111 | }, "%d apples"}, 112 | {"empty-no-arg-single-args", fields{}, args{ 113 | "one apple", 114 | "%d apples", 115 | 1, 116 | []interface{}{1}, 117 | }, "one apple"}, 118 | {"empty-no-arg-plural-args", fields{}, args{ 119 | "one apple", 120 | "%d apples", 121 | 2, 122 | []interface{}{2}, 123 | }, "2 apples"}, 124 | 125 | { 126 | "no-arg-no-plural-header", 127 | fields{ 128 | entries: map[string]*Entry{ 129 | key("", "one apple"): {MsgStrN: []string{"%d 个苹果"}}, 130 | }, 131 | }, 132 | args{ 133 | "one apple", 134 | "%d apples", 135 | 1, 136 | []interface{}{}, 137 | }, 138 | "one apple", 139 | }, 140 | { 141 | "with-arg-no-plural-header", 142 | fields{ 143 | entries: map[string]*Entry{ 144 | key("", "one apple"): {MsgStrN: []string{"%d 个苹果"}}, 145 | }, 146 | }, 147 | args{ 148 | "one apple", 149 | "%d apples", 150 | 2, 151 | []interface{}{2}, 152 | }, 153 | "2 apples", 154 | }, 155 | 156 | { 157 | "no-arg-single", 158 | fields{ 159 | entries: map[string]*Entry{ 160 | key("", "one apple"): {MsgStrN: []string{"%d 个苹果"}}, 161 | }, 162 | headers: map[string]string{HeaderPluralForms: "nplurals=1;plural=0;"}, 163 | }, 164 | args{ 165 | "one apple", 166 | "%d apples", 167 | 1, 168 | []interface{}{}, 169 | }, 170 | "%d 个苹果", 171 | }, 172 | { 173 | "no-arg-plural", 174 | fields{ 175 | entries: map[string]*Entry{ 176 | key("", "one apple"): {MsgStrN: []string{"%d 个苹果"}}, 177 | }, 178 | headers: map[string]string{HeaderPluralForms: "nplurals=1;plural=0;"}, 179 | }, 180 | args{ 181 | "one apple", 182 | "%d apples", 183 | 2, 184 | []interface{}{}, 185 | }, 186 | "%d 个苹果", 187 | }, 188 | { 189 | "with-arg", 190 | fields{ 191 | entries: map[string]*Entry{ 192 | key("", "one apple"): {MsgStrN: []string{"%d 个苹果"}}, 193 | }, 194 | headers: map[string]string{HeaderPluralForms: "nplurals=1;plural=0;"}, 195 | }, 196 | args{ 197 | "one apple", 198 | "%d apples", 199 | 1, 200 | []interface{}{1}, 201 | }, 202 | "1 个苹果", 203 | }, 204 | 205 | { 206 | "invalid-plural", 207 | fields{ 208 | entries: map[string]*Entry{ 209 | key("", "one apple"): {MsgStrN: []string{"%d 个苹果"}}, 210 | }, 211 | headers: map[string]string{HeaderPluralForms: "nplurals=1;plural=1;"}, 212 | }, 213 | args{ 214 | "one apple", 215 | "%d apples", 216 | 1, 217 | []interface{}{1}, 218 | }, 219 | "one apple", 220 | }, 221 | } 222 | for _, tt := range tests { 223 | t.Run(tt.name, func(t *testing.T) { 224 | file := &File{ 225 | entries: tt.fields.entries, 226 | headers: tt.fields.headers, 227 | plural: tt.fields.plural, 228 | } 229 | if got := file.XN64("", tt.args.msgID, tt.args.msgIDPlural, int64(tt.args.n), tt.args.args...); got != tt.want { 230 | t.Errorf("File.N() = %v, want %v", got, tt.want) 231 | } 232 | }) 233 | } 234 | } 235 | 236 | func TestSortedEntry(t *testing.T) { 237 | content, err := os.ReadFile("../testdata/messages.pot") 238 | if err != nil { 239 | t.Fatalf("read file failed: %v", err) 240 | } 241 | f, err := ReadPot(content) 242 | if err != nil { 243 | t.Fatalf("read .po failed: %v", err) 244 | } 245 | w, err := os.OpenFile("../testdata/messages.new.pot", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0777) 246 | if err != nil { 247 | t.Fatalf("open save file failed: %v", err) 248 | } 249 | err = f.SaveAsPot(w) 250 | if err != nil { 251 | t.Fatalf("save file failed: %v", err) 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /translator/mo.go: -------------------------------------------------------------------------------- 1 | package translator 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "io" 8 | "strings" 9 | 10 | "github.com/youthlin/t/errors" 11 | ) 12 | 13 | // https://www.gnu.org/software/gettext/manual/html_node/MO-Files.html#MO-Files 14 | /* 15 | byte 16 | +------------------------------------------+ 17 | 0 | magic number = 0x950412de | 18 | | | 19 | 4 | file format revision = 0 | 20 | | | 21 | 8 | number of strings | == N 22 | | | 23 | 12 | offset of table with original strings | == O 24 | | | 25 | 16 | offset of table with translation strings | == T 26 | | | 27 | 20 | size of hashing table | == S 28 | | | 29 | 24 | offset of hashing table | == H 30 | | | 31 | . . 32 | . (possibly more entries later) . 33 | . . 34 | | | 35 | O | length & offset 0th string ----------------. 36 | O + 8 | length & offset 1st string ------------------. 37 | ... ... | | 38 | O + ((N-1)*8)| length & offset (N-1)th string | | | 39 | | | | | 40 | T | length & offset 0th translation ---------------. 41 | T + 8 | length & offset 1st translation -----------------. 42 | ... ... | | | | 43 | T + ((N-1)*8)| length & offset (N-1)th translation | | | | | 44 | | | | | | | 45 | H | start hash table | | | | | 46 | ... ... | | | | 47 | H + S * 4 | end hash table | | | | | 48 | | | | | | | 49 | | NUL terminated 0th string <----------------' | | | 50 | | | | | | 51 | | NUL terminated 1st string <------------------' | | 52 | | | | | 53 | ... ... | | 54 | | | | | 55 | | NUL terminated 0th translation <---------------' | 56 | | | | 57 | | NUL terminated 1st translation <-----------------' 58 | | | 59 | ... ... 60 | | | 61 | +------------------------------------------+ 62 | */ 63 | 64 | const ( 65 | nul = "\x00" 66 | eot = "\x04" 67 | moMagic = 0x950412de 68 | moMagicBig = 0xde120495 69 | flag = "ThisFileIsGenerateBy:github.com/youthlin/t" + nul 70 | flagLen = len(flag) // 43 71 | offsetO = 28 + 4 + flagLen // 75: 28=固定header 4=uint32:flagLen 43=string:flag 72 | ) 73 | 74 | var errReadMo = fmt.Errorf("read mo") 75 | 76 | type header struct { 77 | Major uint16 // 主版本号,只能是0或1 78 | Minor uint16 // 次版本好,只能是0或1 79 | IDCount uint32 // N msgID 数量 80 | OffsetID uint32 // O msgID 从哪里开始读取 81 | OffsetStr uint32 // T msgStr 的偏移量 82 | SizeHash uint32 // S 可忽略 hash 表大小 83 | OffsetHash uint32 // H 可忽略 hash 表偏移位置 84 | } 85 | 86 | type lenOff struct { 87 | Length uint32 // 长度 88 | OffSet uint32 // 偏移位置 89 | } 90 | 91 | // ReadMo read mo from []byte content 92 | func ReadMo(content []byte) (*File, error) { 93 | file := new(File) 94 | r := bytes.NewReader(content) 95 | // 1 读取魔数 96 | var magic uint32 97 | if err := binary.Read(r, binary.LittleEndian, &magic); err != nil { 98 | return nil, errors.WithSecondaryError(errReadMo, errors.Wrapf(err, "failed to read magic number")) 99 | } 100 | var bo binary.ByteOrder 101 | switch magic { 102 | case moMagicBig: 103 | bo = binary.BigEndian 104 | case moMagic: 105 | bo = binary.LittleEndian 106 | default: 107 | return nil, errors.WithSecondaryError(errReadMo, errors.Errorf("invalid magic number: %v", magic)) 108 | } 109 | 110 | // 2 读取魔数后的固定头部字段 111 | var h header 112 | if err := binary.Read(r, bo, &h); err != nil { 113 | return nil, errors.WithSecondaryError(errReadMo, errors.Wrapf(err, "failed to read mo header")) 114 | } 115 | if h.Major != 0 && h.Major != 1 { 116 | return nil, errors.WithSecondaryError(errReadMo, errors.Errorf("unsopported major version: %v", h.Major)) 117 | } 118 | if h.Minor != 0 && h.Minor != 1 { 119 | return nil, errors.WithSecondaryError(errReadMo, errors.Errorf("unsopported minor version: %v", h.Minor)) 120 | } 121 | 122 | // 3 跳转到 O 处 读取 msgID 信息 123 | if _, err := r.Seek(int64(h.OffsetID), io.SeekStart); err != nil { 124 | return nil, errors.WithSecondaryError(errReadMo, 125 | errors.Wrapf(err, "failed to seek to O(message id table): %d", h.OffsetID)) 126 | } 127 | var n = h.IDCount // 一共有 n 条 msgID 128 | var msgIDMeta []lenOff 129 | for i := uint32(0); i < n; i++ { 130 | var lo lenOff 131 | if err := binary.Read(r, bo, &lo); err != nil { 132 | return nil, errors.WithSecondaryError(errReadMo, 133 | errors.Wrapf(err, "failed to read msg_id[%d] length & offset", i)) 134 | } 135 | msgIDMeta = append(msgIDMeta, lo) 136 | } 137 | 138 | // 4 跳转到 T 处,读取 msgStr 信息 139 | if _, err := r.Seek(int64(h.OffsetStr), io.SeekStart); err != nil { 140 | return nil, errors.WithSecondaryError(errReadMo, 141 | errors.Wrapf(err, "failed to seek to O(message string table): %d", h.OffsetStr)) 142 | } 143 | var msgStrMeta []lenOff 144 | for i := uint32(0); i < n; i++ { 145 | var lo lenOff 146 | if err := binary.Read(r, bo, &lo); err != nil { 147 | return nil, errors.WithSecondaryError(errReadMo, 148 | errors.Wrapf(err, "failed to read msg_str[%d] length & offset", i)) 149 | } 150 | msgStrMeta = append(msgStrMeta, lo) 151 | } 152 | 153 | // 5 开始读取 154 | for i := uint32(0); i < n; i++ { 155 | // 5.1 跳转到第 i 条 msg_id 偏移处 156 | if _, err := r.Seek(int64(msgIDMeta[i].OffSet), io.SeekStart); err != nil { 157 | return nil, errors.WithSecondaryError(errReadMo, 158 | errors.Wrapf(err, "failed to seek to msg_id[%d]: %d", i, msgIDMeta[i].OffSet)) 159 | } 160 | id := make([]byte, msgIDMeta[i].Length) // msg_id 的长度 161 | if err := binary.Read(r, bo, &id); err != nil { 162 | return nil, errors.WithSecondaryError(errReadMo, 163 | errors.Wrapf(err, "failed to read msg_id[%d]", i)) 164 | } 165 | 166 | // 5.2 跳转读取 msg_str 167 | if _, err := r.Seek(int64(msgStrMeta[i].OffSet), io.SeekStart); err != nil { 168 | return nil, errors.WithSecondaryError(errReadMo, 169 | errors.Wrapf(err, "failed to seek to msg_str[%d]: %d", i, msgStrMeta[i].OffSet)) 170 | } 171 | str := make([]byte, msgStrMeta[i].Length) // msg_str 的长度 172 | if err := binary.Read(r, bo, &str); err != nil { 173 | return nil, errors.WithSecondaryError(errReadMo, 174 | errors.Wrapf(err, "failed to read msg_str[%d]", i)) 175 | } 176 | 177 | // 5.3 as Entry 178 | var entry = &Entry{ 179 | MsgID: string(id), 180 | MsgStr: string(str), 181 | } 182 | // 0x04 分割 msgCtxt 和 msgId 183 | if index := strings.Index(entry.MsgID, eot); index >= 0 { 184 | entry.MsgCtxt, entry.MsgID = entry.MsgID[:index], entry.MsgID[index+1:] 185 | } 186 | // 0x00 分割 msgId 和 msgIdPlural 187 | if index := strings.Index(entry.MsgID, nul); index >= 0 { 188 | entry.MsgID, entry.MsgID2 = entry.MsgID[:index], entry.MsgID[index+1:] 189 | entry.MsgStrN = strings.Split(entry.MsgStr, nul) 190 | entry.MsgStr = "" 191 | } 192 | file.AddEntry(entry) 193 | } 194 | return file, nil 195 | } 196 | 197 | // SaveAsMo save as machine object file(.mo) 198 | // 199 | // 0: magic number = 0x950412de 200 | // 4: version = 0 201 | // 8: count = count 202 | // 203 | // 12: offset of origin string table = O = 75 204 | // 16: offset of translation string table 205 | // 20: size of hash table = 0 206 | // 24: offset of hash table = 0 207 | // 28: custom header entry: flag size = len(flag) = 43 208 | // 32: custom header entry: flag 209 | // 75: offsetO: id table: (length & offset) * count 210 | // xx: offsetT: string table. xx=75+count*8 211 | // aa: offsetID: ids. aa=75+count*8*2 212 | // bb: offsetStr: strs. 213 | func (f *File) SaveAsMo(w io.Writer) error { 214 | count := len(f.entries) 215 | var ws = new(bytes.Buffer) 216 | writeMoHeader(ws, count) 217 | 218 | // map 多次迭代顺序不定,先转为数组 219 | var entries = f.SortedEntry() 220 | 221 | // pos=O. from here is O. 222 | offsetID := offsetO + count*8*2 // 8=length(uint32)+offset(uint32) 2=id table + str table 223 | // length/offset of 0th string 224 | for _, entry := range entries { 225 | // length 226 | msgID := moMsgID(entry) 227 | msgIDLen := len(msgID) 228 | if err := binary.Write(ws, binary.LittleEndian, uint32(msgIDLen)); err != nil { 229 | return err 230 | } 231 | // offset 先占位 232 | if err := binary.Write(ws, binary.LittleEndian, uint32(offsetID)); err != nil { 233 | return err 234 | } 235 | offsetID += msgIDLen + 1 // +1: string end with null 236 | } 237 | 238 | // pos=T form here is T 239 | offsetStr := offsetID 240 | // length & offset 0th translation 241 | for _, entry := range entries { 242 | // length 243 | msgStr := moMsgStr(entry) 244 | msgStrLen := len(msgStr) 245 | if err := binary.Write(ws, binary.LittleEndian, uint32(msgStrLen)); err != nil { 246 | return err 247 | } 248 | // offset 先占位 249 | if err := binary.Write(ws, binary.LittleEndian, uint32(offsetStr)); err != nil { 250 | return err 251 | } 252 | offsetStr += msgStrLen + 1 253 | } 254 | 255 | // offsetH ignore 256 | 257 | // pos=offsetID 258 | // NUL terminated 0th string 259 | for _, entry := range entries { 260 | msgID := moMsgID(entry) 261 | if err := binary.Write(ws, binary.LittleEndian, []byte(msgID+nul)); err != nil { 262 | return err 263 | } 264 | } 265 | 266 | // pos=offsetStr 267 | // NUL terminated 0th translation 268 | for _, entry := range entries { 269 | msgStr := moMsgStr(entry) 270 | if err := binary.Write(ws, binary.LittleEndian, []byte(msgStr+nul)); err != nil { 271 | return err 272 | } 273 | } 274 | // 不必回去填充各个占位符 已经计算好了 275 | _, err := ws.WriteTo(w) 276 | return err 277 | } 278 | 279 | func writeMoHeader(ws io.Writer, count int) error { 280 | var offsetT = offsetO + count*8 // string table is after id table 281 | // pos=0. magic number 282 | if err := binary.Write(ws, binary.LittleEndian, uint32(moMagic)); err != nil { 283 | return err 284 | } 285 | // pos=4. version=0 286 | if err := binary.Write(ws, binary.LittleEndian, uint32(0)); err != nil { 287 | return err 288 | } 289 | // pos=8. N=number of strings 290 | if err := binary.Write(ws, binary.LittleEndian, uint32(count)); err != nil { 291 | return err 292 | } 293 | // pos=12. O=offset of ids table 294 | if err := binary.Write(ws, binary.LittleEndian, uint32(offsetO)); err != nil { 295 | return err 296 | } 297 | // pos=16. T=offset of translated str table 298 | if err := binary.Write(ws, binary.LittleEndian, uint32(offsetT)); err != nil { 299 | return err 300 | } 301 | // pos=20. S=0 size of hashtable 302 | if err := binary.Write(ws, binary.LittleEndian, uint32(0)); err != nil { 303 | return err 304 | } 305 | // pos=24. H=0 offset of hashtable 306 | if err := binary.Write(ws, binary.LittleEndian, uint32(0)); err != nil { 307 | return err 308 | } 309 | 310 | // 随便多填点内容 311 | if err := binary.Write(ws, binary.LittleEndian, uint32(flagLen)); err != nil { 312 | return err 313 | } 314 | return binary.Write(ws, binary.LittleEndian, []byte(flag)) 315 | } 316 | 317 | func moMsgID(entry *Entry) string { 318 | msgID := entry.MsgID 319 | if entry.MsgCtxt != "" { 320 | msgID = entry.MsgCtxt + eot + msgID 321 | } 322 | if entry.MsgID2 != "" { 323 | msgID += nul + entry.MsgID2 324 | } 325 | return msgID 326 | } 327 | 328 | func moMsgStr(entry *Entry) string { 329 | msgStr := entry.MsgStr 330 | if len(entry.MsgStrN) > 0 { 331 | msgStr = strings.Join(entry.MsgStrN, nul) 332 | } 333 | return msgStr 334 | } 335 | -------------------------------------------------------------------------------- /translator/mo_test.go: -------------------------------------------------------------------------------- 1 | package translator 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func TestReadMo(t *testing.T) { 11 | fMo, err := os.Open("../testdata/zh_CN.mo") 12 | if err != nil { 13 | t.Fatalf("can not open test mo file: err=%+v", err) 14 | } 15 | mo := read(t, fMo) 16 | t.Logf("mo=%#v", mo) 17 | w, err := os.OpenFile("../testdata/zh_CN_save.mo", os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0o666) 18 | if err != nil { 19 | t.Fatalf("can not open save file: err=%+v", err) 20 | } 21 | defer w.Close() 22 | mo.SaveAsMo(w) 23 | 24 | w.Seek(0, io.SeekStart) 25 | mo2 := read(t, w) 26 | if !reflect.DeepEqual(mo.entries, mo2.entries) { 27 | t.Errorf("entries:\n origin=%#v\n read=%#v\n", mo, mo2) 28 | } 29 | if !reflect.DeepEqual(mo.headers, mo2.headers) { 30 | t.Errorf("headers:\n origin=%#v\n read=%#v\n", mo, mo2) 31 | } 32 | if !reflect.DeepEqual(mo.plural.totalForms, mo2.plural.totalForms) { 33 | t.Errorf("plural.totalForms:\n origin=%#v\n read=%#v\n", mo, mo2) 34 | } 35 | if !reflect.DeepEqual(mo.plural.expression, mo2.plural.expression) { 36 | t.Errorf("plural.expression:\n origin=%#v\n read=%#v\n", mo, mo2) 37 | } 38 | } 39 | 40 | func read(t *testing.T, r io.Reader) *File { 41 | t.Helper() 42 | content, err := io.ReadAll(r) 43 | if err != nil { 44 | t.Fatalf("can not read test mo file: err=%+v", err) 45 | } 46 | mo, err := ReadMo(content) 47 | if err != nil { 48 | t.Fatalf("read mo fail: err=%+v", err) 49 | } 50 | return mo 51 | } 52 | -------------------------------------------------------------------------------- /translator/plural.go: -------------------------------------------------------------------------------- 1 | package translator 2 | 3 | import ( 4 | "context" 5 | "regexp" 6 | "strconv" 7 | 8 | "github.com/youthlin/t/plurals" 9 | ) 10 | 11 | // plural 复数 12 | type plural struct { 13 | totalForms int 14 | expression string 15 | fn func(int64) int 16 | } 17 | 18 | var ( 19 | rePlurals = regexp.MustCompile(`^\s*nplurals\s*=\s*(\d)\s*;\s*plural\s*=\s*(.*)\s*;$`) 20 | invalidPluralFn = func(i int64) int { return -1 } 21 | ) 22 | 23 | func parsePlural(forms string) *plural { 24 | var p plural 25 | p.fn = invalidPluralFn 26 | if forms == "" { 27 | return &p 28 | } 29 | find := rePlurals.FindAllStringSubmatch(forms, -1) 30 | if len(find) == 1 && len(find[0]) == 3 { 31 | n := find[0][1] 32 | exp := find[0][2] 33 | if total, err := strconv.ParseInt(n, 10, 64); err == nil { 34 | p.totalForms = int(total) 35 | p.expression = exp 36 | p.fn = func(i int64) int { 37 | index, err := plurals.Eval(context.Background(), exp, i) 38 | if err != nil { 39 | return -1 40 | } 41 | return int(index) 42 | } 43 | } 44 | } 45 | return &p 46 | } 47 | -------------------------------------------------------------------------------- /translator/po.go: -------------------------------------------------------------------------------- 1 | package translator 2 | 3 | // The Format of PO Files 4 | // https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "io" 10 | "strings" 11 | 12 | "github.com/youthlin/t/errors" 13 | ) 14 | 15 | var errEmptyPo = fmt.Errorf("empty po file") 16 | 17 | // ReadPo read po file 18 | func ReadPo(content []byte) (*File, error) { 19 | return readPo(content, false) 20 | } 21 | 22 | func ReadPot(content []byte) (*File, error) { 23 | return readPo(content, true) 24 | } 25 | 26 | func readPo(content []byte, pot bool) (*File, error) { 27 | if len(content) == 0 { 28 | return nil, errors.Wrapf(errEmptyPo, "read po file failed") 29 | } 30 | data := string(content) 31 | data = strings.ReplaceAll(data, "\r", "") 32 | r := newReader(strings.Split(data, "\n")) 33 | file := new(File) 34 | for { 35 | entry, err := readEntry(r) 36 | if pot || entry.isValid() { 37 | file.AddEntry(entry) 38 | } 39 | if errors.Is(err, io.EOF) { 40 | return file, nil 41 | } 42 | if err != nil { 43 | return nil, err 44 | } 45 | } 46 | } 47 | 48 | // SaveAsPo save as a po file 49 | func (f *File) SaveAsPo(w io.Writer) error { 50 | var buf = &bytes.Buffer{} 51 | for _, entry := range f.SortedEntry() { 52 | for _, comment := range entry.MsgCmts { 53 | buf.WriteString(comment) 54 | buf.WriteString("\n") 55 | } 56 | if entry.MsgCtxt != "" { 57 | writeString(buf, msgctxt, entry.MsgCtxt) 58 | } 59 | writeString(buf, msgid, entry.MsgID) 60 | if entry.MsgID2 != "" { 61 | writeString(buf, msgidPlural, entry.MsgID2) 62 | } 63 | if entry.MsgStr != "" { 64 | writeString(buf, msgstr, entry.MsgStr) 65 | } 66 | for i, str := range entry.MsgStrN { 67 | writeString(buf, fmt.Sprintf(msgstrN, i), str) 68 | } 69 | buf.WriteString("\n") 70 | } 71 | _, err := buf.WriteTo(w) 72 | return err 73 | } 74 | 75 | var errInvalidEntry = fmt.Errorf("invalid entry") 76 | 77 | // readEntry 读取翻译条目 78 | func readEntry(r *reader) (*Entry, error) { 79 | const ( // 记录上一行读取的字段 80 | stateCtxt = "ctxt" 81 | stateID2 = "msgid_plural" 82 | stateID = "msgid" 83 | stateStrN = "msgstr[]" 84 | stateStr = "msgstr" 85 | ) 86 | const ( // 各种情形的前缀 87 | prefixCmt = "#" 88 | prefixCtxt = "msgctxt " 89 | prefixID2 = "msgid_plural " 90 | prefixID = "msgid " 91 | prefixStrN = "msgstr[%d] " 92 | prefixStr = "msgstr " 93 | prefixQuote = "\"" 94 | ) 95 | var ( 96 | entry = new(Entry) 97 | // previousLineState 上一行的状态 98 | previousLineState = "" 99 | ) 100 | for { 101 | line, err := r.nextLine() 102 | if err != nil { 103 | if errors.Is(err, io.EOF) { 104 | return entry, err 105 | } 106 | return nil, errors.WithSecondaryError(errInvalidEntry, 107 | errors.Wrapf(err, "read entry failed")) 108 | } 109 | // 空白行,丢弃 110 | if strings.TrimSpace(line) == "" { 111 | continue 112 | } 113 | // 注释 114 | if strings.HasPrefix(line, prefixCmt) { 115 | if previousLineState != "" { 116 | // current entry has been parsed 117 | // this line is next entry's comment 118 | r.unGetLine() 119 | return entry, nil 120 | } 121 | entry.MsgCmts = append(entry.MsgCmts, line) 122 | continue 123 | } 124 | // ctxt 125 | if strings.HasPrefix(line, prefixCtxt) { 126 | if previousLineState != "" { 127 | r.unGetLine() 128 | return entry, nil 129 | } 130 | previousLineState = stateCtxt 131 | data, err := removePrefixAndUnquote(line, prefixCtxt) 132 | if err != nil { 133 | return nil, errors.WithSecondaryError(errInvalidEntry, 134 | errors.Wrapf(err, "unquote msgctxt failed|line %d: %s", r.lineNo, line)) 135 | } 136 | entry.MsgCtxt += data 137 | continue 138 | } 139 | // msgid_plural 140 | if strings.HasPrefix(line, prefixID2) { 141 | if equalsAny(previousLineState, stateID2, stateStr, stateStrN) { 142 | r.unGetLine() // 当前条目已经有这些字段了,说明当前行是下一个条目的 143 | return entry, nil 144 | } 145 | previousLineState = stateID2 146 | data, err := removePrefixAndUnquote(line, prefixID2) 147 | if err != nil { 148 | return nil, errors.WithSecondaryError(errInvalidEntry, 149 | errors.Wrapf(err, "unquote msgid_plural failed|line %d: %s", r.lineNo, line)) 150 | } 151 | entry.MsgID2 += data 152 | continue 153 | } 154 | // msgid 155 | if strings.HasPrefix(line, prefixID) { 156 | if equalsAny(previousLineState, stateID, stateID2, stateStr, stateStrN) { 157 | r.unGetLine() 158 | return entry, nil 159 | } 160 | previousLineState = stateID 161 | data, err := removePrefixAndUnquote(line, prefixID) 162 | if err != nil { 163 | return nil, errors.WithSecondaryError(errInvalidEntry, 164 | errors.Wrapf(err, "unquote msgid failed|line %d: %s", r.lineNo, line)) 165 | } 166 | entry.MsgID += data 167 | continue 168 | } 169 | // msgstr[0] 170 | if prefix := fmt.Sprintf(prefixStrN, len(entry.MsgStrN)); strings.HasPrefix(line, prefix) { 171 | if equalsAny(previousLineState, stateStr) { 172 | r.unGetLine() 173 | return entry, nil 174 | } 175 | previousLineState = stateStrN 176 | data, err := removePrefixAndUnquote(line, prefix) 177 | if err != nil { 178 | return nil, errors.WithSecondaryError(errInvalidEntry, 179 | errors.Wrapf(err, "unquote %s failed|line %d: %s", prefix, r.lineNo, line)) 180 | } 181 | entry.MsgStrN = append(entry.MsgStrN, data) 182 | continue 183 | } 184 | // msgstr 185 | if strings.HasPrefix(line, prefixStr) { 186 | if equalsAny(previousLineState, stateStrN) { 187 | r.unGetLine() 188 | return entry, nil 189 | } 190 | previousLineState = stateStr 191 | data, err := removePrefixAndUnquote(line, prefixStr) 192 | if err != nil { 193 | return nil, errors.WithSecondaryError(errInvalidEntry, 194 | errors.Wrapf(err, "unquote msgstr failed|line %d: %s", r.lineNo, line)) 195 | } 196 | entry.MsgStr += data 197 | continue 198 | } 199 | 200 | // msgid "previous line" 201 | // "current line" 202 | if strings.HasPrefix(line, prefixQuote) { 203 | data, err := removePrefixAndUnquote(line, "") 204 | if err != nil { 205 | return nil, errors.WithSecondaryError(errInvalidEntry, 206 | errors.Wrapf(err, "unquote %s failed|line %d: %s", previousLineState, r.lineNo, line)) 207 | } 208 | switch previousLineState { 209 | case stateCtxt: 210 | entry.MsgCtxt += data 211 | case stateID2: 212 | entry.MsgID2 += data 213 | case stateID: 214 | entry.MsgID += data 215 | case stateStrN: 216 | entry.MsgStrN[len(entry.MsgStrN)-1] += data 217 | case stateStr: 218 | entry.MsgStr += data 219 | } 220 | } else { 221 | return nil, errors.WithSecondaryError(errInvalidEntry, 222 | errors.Errorf("unexpected line %d: %s", r.lineNo, line)) 223 | } 224 | } 225 | } 226 | 227 | func equalsAny(data string, args ...string) bool { 228 | for _, arg := range args { 229 | if data == arg { 230 | return true 231 | } 232 | } 233 | return false 234 | } 235 | -------------------------------------------------------------------------------- /translator/po_test.go: -------------------------------------------------------------------------------- 1 | package translator 2 | 3 | import ( 4 | "bytes" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestFile_SaveAsPo(t *testing.T) { 10 | type fields struct { 11 | entries []*Entry 12 | } 13 | tests := []struct { 14 | name string 15 | fields fields 16 | wantW string 17 | wantErr bool 18 | }{ 19 | {"empty", fields{}, "", false}, 20 | {"header-only", fields{[]*Entry{ 21 | { 22 | MsgID: "", 23 | MsgStr: "Project-Id-Version: MyProject\n", 24 | }, 25 | }}, `msgid "" 26 | msgstr "Project-Id-Version: MyProject\n" 27 | 28 | `, false}, 29 | {"header-2", fields{[]*Entry{ 30 | { 31 | MsgID: "", 32 | MsgStr: `Project-Id-Version: MyProject 33 | Language: zh_CN 34 | Content-Type: text/plain; charset=UTF-8 35 | Content-Transfer-Encoding: 8bit 36 | Plural-Forms: nplurals=1; plural=0; 37 | `, 38 | }, 39 | }}, `msgid "" 40 | msgstr "" 41 | "Project-Id-Version: MyProject\n" 42 | "Language: zh_CN\n" 43 | "Content-Type: text/plain; charset=UTF-8\n" 44 | "Content-Transfer-Encoding: 8bit\n" 45 | "Plural-Forms: nplurals=1; plural=0;\n" 46 | 47 | `, false}, 48 | {"with-cmt", fields{[]*Entry{ 49 | { 50 | MsgCmts: []string{"# translators comment", "#: path/to/source"}, 51 | MsgID: "hello", 52 | MsgStr: "你好", 53 | }, 54 | }}, `# translators comment 55 | #: path/to/source 56 | msgid "hello" 57 | msgstr "你好" 58 | 59 | `, false}, 60 | {"cmt-ctx-plural", fields{[]*Entry{ 61 | { 62 | MsgID: "", 63 | MsgStr: "Project-Id-Version: MyProject\n", 64 | }, 65 | { 66 | MsgCmts: []string{"# translators comment", "#: path/to/source"}, 67 | MsgCtxt: "ctx", 68 | MsgID: "one apple", 69 | MsgID2: "%d apples", 70 | MsgStrN: []string{"%d 个苹果"}, 71 | }, 72 | }}, `msgid "" 73 | msgstr "Project-Id-Version: MyProject\n" 74 | 75 | # translators comment 76 | #: path/to/source 77 | msgctxt "ctx" 78 | msgid "one apple" 79 | msgid_plural "%d apples" 80 | msgstr[0] "%d 个苹果" 81 | 82 | `, false}, 83 | } 84 | for _, tt := range tests { 85 | t.Run(tt.name, func(t *testing.T) { 86 | f := &File{ 87 | entries: entriesMap(tt.fields.entries), 88 | } 89 | w := &bytes.Buffer{} 90 | if err := f.SaveAsPo(w); (err != nil) != tt.wantErr { 91 | t.Errorf("File.SaveAsPo() error = %v, wantErr %v", err, tt.wantErr) 92 | return 93 | } 94 | if gotW := w.String(); gotW != tt.wantW { 95 | t.Errorf("File.SaveAsPo() = %v, want %v", gotW, tt.wantW) 96 | } 97 | }) 98 | } 99 | } 100 | 101 | func Test_readEntry(t *testing.T) { 102 | type args struct { 103 | r *reader 104 | } 105 | tests := []struct { 106 | name string 107 | args args 108 | want *Entry 109 | wantErr bool 110 | }{ 111 | {"empty", args{newReader([]string{})}, &Entry{}, true}, // EOF 112 | {"blank", args{newReader([]string{""})}, &Entry{}, true}, // EOF 113 | 114 | {"simple-id-str", args{newReader([]string{`msgid "hello"`, `msgstr "你好"`})}, &Entry{ 115 | MsgID: "hello", 116 | MsgStr: "你好", 117 | }, true}, 118 | {"simple-id-strN", args{newReader([]string{`msgid "hello"`, `msgstr[0] "你好"`})}, &Entry{ 119 | MsgID: "hello", 120 | MsgStrN: []string{"你好"}, 121 | }, true}, 122 | 123 | {"simple-id-str-two-entry", args{newReader([]string{`msgid "hello"`, `msgstr "你好"`, `msgid "entry 2`})}, &Entry{ 124 | MsgID: "hello", 125 | MsgStr: "你好", 126 | }, false}, 127 | 128 | {"simple-cmt", args{newReader([]string{`# bla bla`, `msgstr "你好"`})}, &Entry{ 129 | MsgCmts: []string{"# bla bla"}, 130 | MsgID: "", 131 | MsgStr: "你好", 132 | }, true}, 133 | {"cmt-is-entry-start", args{newReader([]string{`# bla bla`, `msgstr "你好"`, "#abc"})}, &Entry{ 134 | MsgCmts: []string{"# bla bla"}, 135 | MsgID: "", 136 | MsgStr: "你好", 137 | }, false}, 138 | 139 | {"simple-ctxt", args{newReader([]string{`msgctxt "ctxt"`, `msgstr "你好"`})}, &Entry{ 140 | MsgCtxt: "ctxt", 141 | MsgStr: "你好", 142 | }, true}, 143 | {"unquote-ctxt", args{newReader([]string{`msgctxt ctxt`, `msgstr "你好"`})}, nil, true}, // quote err 144 | {"split-ctxt", args{newReader([]string{`msgctxt "ctxt"`, `msgctxt "你好"`})}, &Entry{ 145 | MsgCtxt: "ctxt", 146 | }, false}, 147 | 148 | {"split-msg_plural", args{newReader([]string{`msgid_plural "ctxt"`, `msgid_plural "你好"`})}, &Entry{ 149 | MsgID2: "ctxt", 150 | }, false}, 151 | {"unquote-msg_plural", args{newReader([]string{`msgid_plural id2`, `msgid_plural "你好"`})}, nil, true}, 152 | {"unquote-msg_id", args{newReader([]string{`msgid id2`, `msgid_plural "你好"`})}, nil, true}, 153 | 154 | {"msg_strN", args{newReader([]string{`msgstr[0] "str0"`, `msgstr[1] "str1"`})}, &Entry{ 155 | MsgStrN: []string{"str0", "str1"}, 156 | }, true}, 157 | {"split-msg_strN", args{newReader([]string{`msgstr[0] "str0"`, `msgstr "str"`})}, &Entry{ 158 | MsgStrN: []string{"str0"}, 159 | }, false}, 160 | 161 | {"unquote-msg_strN", args{newReader([]string{`msgstr[0] str0`})}, nil, true}, 162 | 163 | {"msg_str", args{newReader([]string{`msgstr "str"`})}, &Entry{ 164 | MsgStr: "str", 165 | }, true}, 166 | {"msg_str", args{newReader([]string{`msgstr str`})}, nil, true}, 167 | {"split-msg_str", args{newReader([]string{`msgstr "str"`, `msgstr[0] "str0"`})}, &Entry{ 168 | MsgStr: "str", 169 | }, false}, 170 | 171 | {"multi-line-ctxt", args{newReader([]string{`msgctxt "line1"`, `"line2"`})}, &Entry{ 172 | MsgCtxt: "line1line2", 173 | }, true}, 174 | {"multi-line-id", args{newReader([]string{`msgid "line1"`, `"line2"`})}, &Entry{ 175 | MsgID: "line1line2", 176 | }, true}, 177 | {"multi-line-id2", args{newReader([]string{`msgid_plural "line1"`, `"line2"`})}, &Entry{ 178 | MsgID2: "line1line2", 179 | }, true}, 180 | {"multi-line-str", args{newReader([]string{`msgstr "line1"`, `"line2"`})}, &Entry{ 181 | MsgStr: "line1line2", 182 | }, true}, 183 | {"multi-line-strN", args{newReader([]string{`msgstr[0] "line1"`, `"line2"`})}, &Entry{ 184 | MsgStrN: []string{"line1line2"}, 185 | }, true}, 186 | {"unquote-multi-line-ctxt", args{newReader([]string{`msgctxt "line1"`, `"line2`})}, nil, true}, 187 | {"unexpected-multi-line", args{newReader([]string{`msgctxt "line1"`, `line2`})}, nil, true}, 188 | 189 | {"case-1", args{newReader([]string{`msgid ""`, `msgstr ""`, `"Project-Id-Version: t\n"`})}, &Entry{ 190 | MsgStr: "Project-Id-Version: t\n", 191 | }, true}, 192 | {"case-2", args{newReader([]string{ 193 | `#, c-format`, 194 | `msgctxt "Project|"`, 195 | `msgid "Open One"`, 196 | `msgid_plural "Open %d"`, 197 | `msgstr[0] "打开 %d 个工程"`})}, &Entry{ 198 | MsgCmts: []string{"#, c-format"}, 199 | MsgCtxt: "Project|", 200 | MsgID: "Open One", 201 | MsgID2: "Open %d", 202 | MsgStrN: []string{"打开 %d 个工程"}, 203 | }, true}, 204 | {"case-3-invalid-entry", args{newReader([]string{ 205 | `#, c-format`, 206 | `msgctxt "Project|"`, 207 | `msgid "Open One"`, 208 | `msgid_plural "Open %d"`, 209 | `msgstr "打开"`, 210 | `msgstr[0] "打开 %d 个工程"`})}, &Entry{ 211 | MsgCmts: []string{"#, c-format"}, 212 | MsgCtxt: "Project|", 213 | MsgID: "Open One", 214 | MsgID2: "Open %d", 215 | MsgStr: "打开", 216 | }, false}, 217 | } 218 | for _, tt := range tests { 219 | t.Run(tt.name, func(t *testing.T) { 220 | got, err := readEntry(tt.args.r) 221 | if (err != nil) != tt.wantErr { 222 | t.Errorf("readEntry() error = %+v, wantErr %v", err, tt.wantErr) 223 | return 224 | } 225 | if !reflect.DeepEqual(got, tt.want) { 226 | t.Errorf("readEntry() = %+v, want %+v", got, tt.want) 227 | } 228 | }) 229 | } 230 | } 231 | 232 | func TestReadPo(t *testing.T) { 233 | type args struct { 234 | content []byte 235 | } 236 | tests := []struct { 237 | name string 238 | args args 239 | want *File 240 | wantErr bool 241 | }{ 242 | {"nil", args{}, nil, true}, 243 | {"empty", args{[]byte("")}, nil, true}, 244 | {"one-entry", args{[]byte(`#: lang_test.go:22 lang_test.go:23 main_test.go:37 245 | msgid "Hello, World" 246 | msgstr "你好,世界"`)}, &File{entries: map[string]*Entry{ 247 | key("", "Hello, World"): { 248 | MsgCmts: []string{"#: lang_test.go:22 lang_test.go:23 main_test.go:37"}, 249 | MsgID: "Hello, World", 250 | MsgStr: "你好,世界", 251 | }, 252 | }}, false}, 253 | {"two-entry", args{[]byte(`#: lang_test.go:22 lang_test.go:23 main_test.go:37 254 | msgid "Hello, World" 255 | msgstr "你好,世界" 256 | 257 | msgid "one apple" 258 | msgid_plural "%d apples" 259 | msgstr[0] "%d 个苹果" 260 | `)}, &File{entries: map[string]*Entry{ 261 | key("", "Hello, World"): { 262 | MsgCmts: []string{"#: lang_test.go:22 lang_test.go:23 main_test.go:37"}, 263 | MsgID: "Hello, World", 264 | MsgStr: "你好,世界", 265 | }, 266 | key("", "one apple"): { 267 | MsgID: "one apple", 268 | MsgID2: "%d apples", 269 | MsgStrN: []string{"%d 个苹果"}, 270 | }, 271 | }}, false}, 272 | {"3-entry", args{[]byte(`#: lang_test.go:22 lang_test.go:23 main_test.go:37 273 | msgid "Hello, World" 274 | msgstr "你好,世界" 275 | 276 | msgid "one apple" 277 | msgid_plural "%d apples" 278 | msgstr[0] "%d 个苹果" 279 | 280 | msgctxt "verb" 281 | msgid "Post" 282 | msgstr "发布" 283 | `)}, &File{entries: map[string]*Entry{ 284 | key("", "Hello, World"): { 285 | MsgCmts: []string{"#: lang_test.go:22 lang_test.go:23 main_test.go:37"}, 286 | MsgID: "Hello, World", 287 | MsgStr: "你好,世界", 288 | }, 289 | key("", "one apple"): { 290 | MsgID: "one apple", 291 | MsgID2: "%d apples", 292 | MsgStrN: []string{"%d 个苹果"}, 293 | }, 294 | key("verb", "Post"): { 295 | MsgCtxt: "verb", 296 | MsgID: "Post", 297 | MsgStr: "发布", 298 | }, 299 | }}, false}, 300 | {"error", args{[]byte(`msgid hallo`)}, nil, true}, 301 | } 302 | for _, tt := range tests { 303 | t.Run(tt.name, func(t *testing.T) { 304 | got, err := ReadPo(tt.args.content) 305 | if (err != nil) != tt.wantErr { 306 | t.Errorf("ReadPo() error = %v, wantErr %v", err, tt.wantErr) 307 | return 308 | } 309 | if !reflect.DeepEqual(got, tt.want) { 310 | t.Errorf("ReadPo() = %v, want %v", got, tt.want) 311 | } 312 | }) 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /translator/pot.go: -------------------------------------------------------------------------------- 1 | package translator 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | const ( 10 | msgctxt = "msgctxt" 11 | msgid = "msgid" 12 | msgidPlural = "msgid_plural" 13 | msgstr = "msgstr" 14 | msgstrN = "msgstr[%d]" 15 | ) 16 | 17 | func writeString(buf *bytes.Buffer, key, content string) { 18 | buf.WriteString(key + " ") 19 | for _, line := range split(content, lineThreshold) { 20 | buf.WriteString(fmt.Sprintf("%q\n", line)) 21 | } 22 | } 23 | 24 | // SaveAsPot save this File as pot format 25 | func (f *File) SaveAsPot(w io.Writer) error { 26 | var buf = &bytes.Buffer{} 27 | for _, entry := range f.SortedEntry() { 28 | for _, comment := range entry.MsgCmts { 29 | buf.WriteString(comment) 30 | buf.WriteString("\n") 31 | } 32 | if entry.MsgCtxt != "" { 33 | writeString(buf, msgctxt, entry.MsgCtxt) 34 | } 35 | writeString(buf, msgid, entry.MsgID) 36 | 37 | if entry.MsgID2 == "" { 38 | if entry.MsgID == "" { // header 39 | writeString(buf, msgstr, entry.MsgStr) 40 | } else { 41 | writeString(buf, msgstr, "") 42 | } 43 | } else { 44 | writeString(buf, msgidPlural, entry.MsgID2) 45 | writeString(buf, fmt.Sprintf(msgstrN, 0), "") 46 | writeString(buf, fmt.Sprintf(msgstrN, 1), "") 47 | } 48 | buf.WriteString("\n") 49 | } 50 | _, err := buf.WriteTo(w) 51 | return err 52 | } 53 | -------------------------------------------------------------------------------- /translator/pot_test.go: -------------------------------------------------------------------------------- 1 | package translator 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func entriesMap(entries []*Entry) map[string]*Entry { 9 | m := make(map[string]*Entry) 10 | for _, e := range entries { 11 | m[e.Key()] = e 12 | } 13 | return m 14 | } 15 | 16 | func TestFile_SaveAsPot(t *testing.T) { 17 | type fields struct { 18 | entries []*Entry 19 | } 20 | tests := []struct { 21 | name string 22 | fields fields 23 | wantW string 24 | wantErr bool 25 | }{ 26 | {"empty", fields{}, "", false}, 27 | {"header-2", fields{[]*Entry{ 28 | { 29 | MsgID: "", 30 | MsgStr: `Project-Id-Version: MyProject 31 | Language: zh_CN 32 | Content-Type: text/plain; charset=UTF-8 33 | Content-Transfer-Encoding: 8bit 34 | Plural-Forms: nplurals=1; plural=0; 35 | `, 36 | }, 37 | }}, `msgid "" 38 | msgstr "" 39 | "Project-Id-Version: MyProject\n" 40 | "Language: zh_CN\n" 41 | "Content-Type: text/plain; charset=UTF-8\n" 42 | "Content-Transfer-Encoding: 8bit\n" 43 | "Plural-Forms: nplurals=1; plural=0;\n" 44 | 45 | `, false}, 46 | {"with-cmt", fields{[]*Entry{ 47 | { 48 | MsgCmts: []string{"# translators comment", "#: path/to/source"}, 49 | MsgID: "hello", 50 | MsgStr: "你好", 51 | }, 52 | }}, `# translators comment 53 | #: path/to/source 54 | msgid "hello" 55 | msgstr "" 56 | 57 | `, false}, 58 | {"cmt-ctx-plural", fields{[]*Entry{ 59 | { 60 | MsgCmts: []string{"# translators comment", "#: path/to/source"}, 61 | MsgCtxt: "ctx", 62 | MsgID: "one apple", 63 | MsgID2: "%d apples", 64 | MsgStrN: []string{"%d 个苹果"}, 65 | }, 66 | }}, `# translators comment 67 | #: path/to/source 68 | msgctxt "ctx" 69 | msgid "one apple" 70 | msgid_plural "%d apples" 71 | msgstr[0] "" 72 | msgstr[1] "" 73 | 74 | `, false}, 75 | {"header-cmt-ctx-plural", fields{[]*Entry{ 76 | { 77 | MsgID: "", 78 | MsgStr: `Project-Id-Version: MyProject 79 | Language: zh_CN 80 | Content-Type: text/plain; charset=UTF-8 81 | Content-Transfer-Encoding: 8bit 82 | Plural-Forms: nplurals=1; plural=0; 83 | `, 84 | }, 85 | { 86 | MsgCmts: []string{"# translators comment", "#: path/to/source"}, 87 | MsgCtxt: "ctx", 88 | MsgID: "one apple", 89 | MsgID2: "%d apples", 90 | MsgStrN: []string{"%d 个苹果"}, 91 | }, 92 | }}, `msgid "" 93 | msgstr "" 94 | "Project-Id-Version: MyProject\n" 95 | "Language: zh_CN\n" 96 | "Content-Type: text/plain; charset=UTF-8\n" 97 | "Content-Transfer-Encoding: 8bit\n" 98 | "Plural-Forms: nplurals=1; plural=0;\n" 99 | 100 | # translators comment 101 | #: path/to/source 102 | msgctxt "ctx" 103 | msgid "one apple" 104 | msgid_plural "%d apples" 105 | msgstr[0] "" 106 | msgstr[1] "" 107 | 108 | `, false}, 109 | } 110 | for _, tt := range tests { 111 | t.Run(tt.name, func(t *testing.T) { 112 | f := &File{ 113 | entries: entriesMap(tt.fields.entries), 114 | } 115 | w := &bytes.Buffer{} 116 | if err := f.SaveAsPot(w); (err != nil) != tt.wantErr { 117 | t.Errorf("File.SaveAsPot() error = %v, wantErr %v", err, tt.wantErr) 118 | return 119 | } 120 | if gotW := w.String(); gotW != tt.wantW { 121 | t.Errorf("File.SaveAsPot() = %v, want %v", gotW, tt.wantW) 122 | } 123 | }) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /translator/reader.go: -------------------------------------------------------------------------------- 1 | package translator 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/youthlin/t/errors" 7 | ) 8 | 9 | type reader struct { 10 | lines []string 11 | lineNo int 12 | totalLine int 13 | } 14 | 15 | func newReader(lines []string) *reader { 16 | return &reader{ 17 | lines: lines, 18 | lineNo: -1, 19 | totalLine: len(lines), 20 | } 21 | } 22 | 23 | func (r *reader) currentLine() (string, error) { 24 | if r.lineNo < 0 { 25 | return "", errors.Errorf("you should call nextLine() first") 26 | } 27 | if r.lineNo >= r.totalLine { 28 | return "", io.EOF 29 | } 30 | return r.lines[r.lineNo], nil 31 | } 32 | 33 | func (r *reader) nextLine() (string, error) { 34 | r.lineNo++ 35 | return r.currentLine() 36 | } 37 | 38 | func (r *reader) unGetLine() error { 39 | if r.lineNo < 0 { 40 | return errors.Errorf("already at the beginning") 41 | } 42 | r.lineNo-- 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /translator/reader_test.go: -------------------------------------------------------------------------------- 1 | package translator 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | 7 | . "github.com/smartystreets/goconvey/convey" 8 | "github.com/youthlin/t/errors" 9 | ) 10 | 11 | func Test_newReader(t *testing.T) { 12 | Convey("newReader", t, func() { 13 | Convey("nil-input", func() { 14 | r := newReader(nil) 15 | So(r, ShouldNotBeNil) 16 | So(r.lines, ShouldEqual, []string(nil)) 17 | So(r.lineNo, ShouldEqual, -1) 18 | So(r.totalLine, ShouldEqual, 0) 19 | }) 20 | Convey("empty-input", func() { 21 | r := newReader([]string{}) 22 | So(r, ShouldNotBeNil) 23 | So(r.lines, ShouldResemble, []string{}) 24 | So(r.lineNo, ShouldEqual, -1) 25 | So(r.totalLine, ShouldEqual, 0) 26 | }) 27 | Convey("one-line", func() { 28 | r := newReader([]string{"hello"}) 29 | So(r, ShouldNotBeNil) 30 | So(r.lines, ShouldResemble, []string{"hello"}) 31 | So(r.lineNo, ShouldEqual, -1) 32 | So(r.totalLine, ShouldEqual, 1) 33 | }) 34 | }) 35 | } 36 | 37 | func Test_reader_currentLine(t *testing.T) { 38 | Convey("currentLine", t, func() { 39 | Convey("new-and-currentLine", func() { 40 | r := newReader([]string{"hello"}) 41 | _, err := r.currentLine() 42 | So(err, ShouldNotBeNil) 43 | }) 44 | Convey("next-and-currentLine", func() { 45 | r := newReader([]string{"hello"}) 46 | line, err := r.nextLine() 47 | So(err, ShouldBeNil) 48 | So(line, ShouldEqual, "hello") 49 | str, err := r.currentLine() 50 | So(err, ShouldBeNil) 51 | So(str, ShouldEqual, line) 52 | }) 53 | }) 54 | } 55 | func Test_reader_nextLine(t *testing.T) { 56 | Convey("nextLine", t, func() { 57 | Convey("nil", func() { 58 | r := newReader(nil) 59 | _, err := r.nextLine() 60 | So(err, ShouldNotBeNil) 61 | So(errors.Is(err, io.EOF), ShouldBeTrue) 62 | }) 63 | Convey("not-empty", func() { 64 | r := newReader([]string{"hello"}) 65 | line, err := r.nextLine() 66 | So(err, ShouldBeNil) 67 | So(line, ShouldEqual, "hello") 68 | line, err = r.nextLine() 69 | So(line, ShouldEqual, "") 70 | So(errors.Is(err, io.EOF), ShouldBeTrue) 71 | }) 72 | }) 73 | } 74 | func Test_reader_unGetLine(t *testing.T) { 75 | Convey("unget", t, func() { 76 | Convey("nil", func() { 77 | r := newReader(nil) 78 | err := r.unGetLine() 79 | So(err, ShouldNotBeNil) 80 | }) 81 | Convey("non-empty", func() { 82 | r := newReader([]string{"hello"}) 83 | line, err := r.nextLine() 84 | So(err, ShouldBeNil) 85 | err = r.unGetLine() 86 | So(err, ShouldBeNil) 87 | line2, err := r.nextLine() 88 | So(err, ShouldBeNil) 89 | So(line, ShouldEqual, line2) 90 | }) 91 | }) 92 | } 93 | -------------------------------------------------------------------------------- /translator/translator.go: -------------------------------------------------------------------------------- 1 | package translator 2 | 3 | // Translator 翻译家接口 4 | type Translator interface { 5 | // Lang return the language 返回翻译之后的语言 6 | Lang() string 7 | // X short name of pgettext. msgCtxt can be empty. 单数翻译接口 8 | X(msgCtxt, msgID string, args ...interface{}) string 9 | // XN64 short name of npgettext. msgCtxt can be empty. 复数翻译接口 10 | XN64(msgCtxt, msgID, msgIDPlural string, n int64, args ...interface{}) string 11 | } 12 | -------------------------------------------------------------------------------- /translator/util.go: -------------------------------------------------------------------------------- 1 | package translator 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | // key helper function, return message key 9 | func key(ctxt, msgid string) string { 10 | return ctxt + eot + msgid 11 | } 12 | 13 | // removePrefixAndUnquote 去掉前缀并返回引号中的内容 14 | func removePrefixAndUnquote(line, prefix string) (string, error) { 15 | line = strings.TrimPrefix(line, prefix) 16 | line = strings.TrimSpace(line) 17 | return strconv.Unquote(line) 18 | } 19 | -------------------------------------------------------------------------------- /translator/wrap.go: -------------------------------------------------------------------------------- 1 | package translator 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | const lineThreshold = 77 8 | 9 | // split split long text to small parts by blank or newline(\n) 10 | func split(long string, threshold int) []string { 11 | return split0(long, threshold, 0) 12 | } 13 | 14 | // split split long text to small parts by blank or newline(\n) 15 | func split0(long string, threshold int, depth int) []string { 16 | length := len(long) 17 | if index := strings.Index(long, "\n"); index >= 0 && index < length-1 { 18 | left := long[:index+1] 19 | right := long[index+1:] 20 | prefix := []string{left} 21 | if depth == 0 { 22 | prefix = []string{"", left} 23 | } 24 | return append(prefix, split0(right, threshold, depth+1)...) 25 | } 26 | if length <= threshold { 27 | return []string{long} 28 | } 29 | if index := strings.LastIndex(long, " "); index >= 0 && index < length-1 { 30 | left := long[:index+1] 31 | right := long[index+1:] 32 | return append(split0(left, threshold, depth+1), right) 33 | } 34 | return []string{long} 35 | } 36 | -------------------------------------------------------------------------------- /translator/wrap_test.go: -------------------------------------------------------------------------------- 1 | package translator 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func Test_split(t *testing.T) { 9 | type args struct { 10 | long string 11 | threshold int 12 | } 13 | tests := []struct { 14 | name string 15 | args args 16 | want []string 17 | }{ 18 | {"empty", args{"", 10}, []string{""}}, 19 | {"no-need-split", args{"0123456789", 10}, []string{"0123456789"}}, 20 | {"can-not-split", args{"0123456789", 5}, []string{"0123456789"}}, 21 | {"split-by-blank", args{" 0123456789", 5}, []string{" ", "0123456789"}}, 22 | {"split-by-blank", args{"0 123456789", 5}, []string{"0 ", "123456789"}}, 23 | {"split-by-blank", args{"01 23456789", 5}, []string{"01 ", "23456789"}}, 24 | {"split-by-blank", args{"012 3456789", 5}, []string{"012 ", "3456789"}}, 25 | {"split-by-blank", args{"0123 456789", 5}, []string{"0123 ", "456789"}}, 26 | {"split-by-blank", args{"01234 56789", 5}, []string{"01234 ", "56789"}}, 27 | {"split-by-blank", args{"012345 6789", 5}, []string{"012345 ", "6789"}}, 28 | {"split-by-blank", args{"0123456 789", 5}, []string{"0123456 ", "789"}}, 29 | {"split-by-blank", args{"01234567 89", 5}, []string{"01234567 ", "89"}}, 30 | {"split-by-blank", args{"012345678 9", 5}, []string{"012345678 ", "9"}}, 31 | {"split-by-blank", args{"0123456789 ", 5}, []string{"0123456789 "}}, 32 | {"split-by-blank", args{"0123456789 ", 5}, []string{"0123456789 "}}, 33 | 34 | {"split-by-newline", args{"0123456789\n", 5}, []string{"0123456789\n"}}, 35 | 36 | {"split-by-newline", args{"0123456789\n\n", 5}, []string{"", "0123456789\n", "\n"}}, 37 | {"split-by-newline", args{"012345678\n9", 5}, []string{"", "012345678\n", "9"}}, 38 | {"split-by-newline", args{"01234567\n89", 5}, []string{"", "01234567\n", "89"}}, 39 | {"split-by-newline", args{"0123456\n789", 5}, []string{"", "0123456\n", "789"}}, 40 | {"split-by-newline", args{"012345\n6789", 5}, []string{"", "012345\n", "6789"}}, 41 | {"split-by-newline", args{"01234\n56789", 5}, []string{"", "01234\n", "56789"}}, 42 | {"split-by-newline", args{"0123\n456789", 5}, []string{"", "0123\n", "456789"}}, 43 | {"split-by-newline", args{"012\n3456789", 5}, []string{"", "012\n", "3456789"}}, 44 | {"split-by-newline", args{"01\n23456789", 5}, []string{"", "01\n", "23456789"}}, 45 | {"split-by-newline", args{"0\n123456789", 5}, []string{"", "0\n", "123456789"}}, 46 | {"split-by-newline", args{"\n0123456789", 5}, []string{"", "\n", "0123456789"}}, 47 | 48 | {"split-by-newline", args{"01234\n56789", 15}, []string{"", "01234\n", "56789"}}, 49 | 50 | {"split-by-newline", args{"01234\n56789\n01234", 5}, []string{"", "01234\n", "56789\n", "01234"}}, 51 | } 52 | for _, tt := range tests { 53 | t.Run(tt.name, func(t *testing.T) { 54 | if got := split(tt.args.long, tt.args.threshold); !reflect.DeepEqual(got, tt.want) { 55 | t.Errorf("split() = %q, want %q", got, tt.want) 56 | } 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /unittest.sh: -------------------------------------------------------------------------------- 1 | mkdir -p output 2 | gofmt -w . 3 | go mod tidy 4 | go test -gcflags all=-l -cover -race -coverprofile=output/cover.txt ./... && go tool cover -func=output/cover.txt && go tool cover -html=output/cover.txt 5 | --------------------------------------------------------------------------------