pax_global_header 0000666 0000000 0000000 00000000064 14774252036 0014524 g ustar 00root root 0000000 0000000 52 comment=5b3969925c135e2717f3466d144e8bfe97ed645e
go-i18n-2.6.0/ 0000775 0000000 0000000 00000000000 14774252036 0012713 5 ustar 00root root 0000000 0000000 go-i18n-2.6.0/.codecov.yml 0000664 0000000 0000000 00000000217 14774252036 0015136 0 ustar 00root root 0000000 0000000 coverage:
range: 50..75
status:
patch:
default:
only_pulls: true
project:
default:
informational: true
go-i18n-2.6.0/.github/ 0000775 0000000 0000000 00000000000 14774252036 0014253 5 ustar 00root root 0000000 0000000 go-i18n-2.6.0/.github/README.uk-UA.md 0000664 0000000 0000000 00000015522 14774252036 0016460 0 ustar 00root root 0000000 0000000 # go-i18n
 [](https://goreportcard.com/report/github.com/nicksnyder/go-i18n/v2) [](https://codecov.io/gh/nicksnyder/go-i18n) [](https://sourcegraph.com/github.com/nicksnyder/go-i18n?badge)
go-i18n — це Go [пакет](#package-i18n) та [інструмент](#command-goi18n), які допомагають перекладати Go програми на різні мови.
- Підтримує [множинні форми](http://cldr.unicode.org/index/cldr-spec/plural-rules) для всіх 200+ мов у [Unicode Common Locale Data Repository (CLDR)](https://www.unicode.org/cldr/charts/28/supplemental/language_plural_rules.html).
- Код і тести [автоматично генеруються](https://github.com/nicksnyder/go-i18n/tree/main/internal/plural/codegen) з даних [CLDR](http://cldr.unicode.org/index/downloads).
- Підтримує рядки з іменованими змінними, використовуючи синтаксис [text/template](http://golang.org/pkg/text/template/).
- Підтримує файли повідомлень у будь-якому форматі (наприклад, JSON, TOML, YAML).
## Пакет i18n
[](https://pkg.go.dev/github.com/nicksnyder/go-i18n/v2/i18n)
Пакет i18n забезпечує підтримку пошуку повідомлень відповідно до набору мовних уподобань.
```go
import "github.com/nicksnyder/go-i18n/v2/i18n"
```
Створіть Bundle, який використовуватимете протягом усього терміну служби вашої програми.
```go
bundle := i18n.NewBundle(language.English)
```
Завантажуйте переклади у ваш пакет під час ініціалізації.
```go
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
bundle.LoadMessageFile("es.toml")
```
```go
// Якщо використовуєте go:embed
//go:embed locale.*.toml
var LocaleFS embed.FS
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
bundle.LoadMessageFileFS(LocaleFS, "locale.es.toml")
```
Створіть Localizer, який використовуватимете для набору мовних уподобань.
```go
func(w http.ResponseWriter, r *http.Request) {
lang := r.FormValue("lang")
accept := r.Header.Get("Accept-Language")
localizer := i18n.NewLocalizer(bundle, lang, accept)
}
```
Використовуйте Localizer для пошуку повідомлень.
```go
localizer.Localize(&i18n.LocalizeConfig{
DefaultMessage: &i18n.Message{
ID: "PersonCats",
One: "{{.Name}} has {{.Count}} cat.",
Other: "{{.Name}} has {{.Count}} cats.",
},
TemplateData: map[string]interface{}{
"Name": "Nick",
"Count": 2,
},
PluralCount: 2,
}) // Nick has 2 cats.
```
## Команда goi18n
[](https://pkg.go.dev/github.com/nicksnyder/go-i18n/v2/goi18n)
Команда goi18n управляє файлами повідомлень, що використовуються пакетом i18n.
```
go install -v github.com/nicksnyder/go-i18n/v2/goi18n@latest
goi18n -help
```
### Витяг повідомлень
Використовуйте команду `goi18n extract`, щоб витягнути всі літерали структури i18n.Message із Go-файлів у файл повідомлень для перекладу.
```toml
# active.en.toml
[PersonCats]
description = "The number of cats a person has"
one = "{{.Name}} has {{.Count}} cat."
other = "{{.Name}} has {{.Count}} cats."
```
### Переклад нової мови
1. Створіть порожній файл повідомлень для мови, яку ви хочете додати (наприклад, translate.uk.toml).
2. Виконайте команду `goi18n merge active.en.toml translate.es.toml`, щоб заповнити `translate.es.toml` повідомленнями для перекладу.
```toml
# translate.uk.toml
[HelloPerson]
hash = "sha1-5b49bfdad81fedaeefb224b0ffc2acc58b09cff5"
other = "Hello {{.Name}}"
```
3. Після перекладу файлу `translate.es.toml` перейменуйте його на `active.es.toml`.
```toml
# active.uk.toml
[HelloPerson]
hash = "sha1-5b49bfdad81fedaeefb224b0ffc2acc58b09cff5"
other = "Вітаю {{.Name}}"
```
4. Завантажте файл `active.es.toml` у свій пакет.
```go
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
bundle.LoadMessageFile("active.es.toml")
```
### Переклад нових повідомлень
Якщо ви додали нові повідомлення до своєї програми:
1. Виконайте `goi18n extract`, щоб оновити файл `active.en.toml` новими повідомленнями.
2. Виконайте `goi18n merge active.*.toml`, щоб згенерувати оновлені файли `translate.*.toml`.
3. Перекладіть усі повідомлення у файлах `translate.*.toml`.
4. Виконайте `goi18n merge active.*.toml translate.*.toml`, щоб об’єднати перекладені повідомлення з активними файлами повідомлень.
## Для отримання додаткової інформації та прикладів:
- Ознайомтеся з [документацією](https://pkg.go.dev/github.com/nicksnyder/go-i18n/v2).
- Подивіться [приклади коду](https://github.com/nicksnyder/go-i18n/blob/main/i18n/example_test.go) та [тести](https://github.com/nicksnyder/go-i18n/blob/main/i18n/localizer_test.go).
- Перегляньте приклад [додатку](https://github.com/nicksnyder/go-i18n/tree/main/example).
## Переклади цього документа
Переклади цього документа, зроблені спільнотою, можна знайти в папці [.github](.github).
Ці переклади підтримуються спільнотою і не підтримуються автором цього проєкту.
Немає гарантії, що вони є точними або актуальними.
## Ліцензія
go-i18n доступний під ліцензією MIT. Див. файл [LICENSE](LICENSE) для отримання додаткової інформації.
go-i18n-2.6.0/.github/README.zh-Hans.md 0000664 0000000 0000000 00000011732 14774252036 0017045 0 ustar 00root root 0000000 0000000 # go-i18n
 [](https://goreportcard.com/report/github.com/nicksnyder/go-i18n/v2) [](https://codecov.io/gh/nicksnyder/go-i18n) [](https://sourcegraph.com/github.com/nicksnyder/go-i18n?badge)
go-i18n 是一个帮助您将 Go 程序翻译成多种语言的 Go [包](#package-i18n)和[命令](#command-goi18n)。
- 支持 [Unicode Common Locale Data Repository (CLDR)](https://www.unicode.org/cldr/charts/28/supplemental/language_plural_rules.html)
中所有 200 多种语言的[复数字符串](http://cldr.unicode.org/index/cldr-spec/plural-rules)。
- 代码和测试是基于 [CLDR 数据](http://cldr.unicode.org/index/downloads)[自动生成](https://github.com/nicksnyder/go-i18n/tree/main/internal/plural/codegen)的。
- 使用 [text/template](http://golang.org/pkg/text/template/) 语法支持带有命名变量的字符串。
- 支持所有格式的消息文件(例如:JSON、TOML、YAML)。
[**English**](../README.md) · [**简体中文**](README.zh-Hans.md)
## i18n 包
[](https://pkg.go.dev/github.com/nicksnyder/go-i18n/v2/i18n)
i18n 包支持根据一组语言环境首选项来查找消息。
```go
import "github.com/nicksnyder/go-i18n/v2/i18n"
```
创建一个 Bundle 以在应用程序的整个生命周期中使用。
```go
bundle := i18n.NewBundle(language.English)
```
在初始化时,将翻译加载到你的 Bundle 中。
```go
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
bundle.LoadMessageFile("es.toml")
```
```go
// 如果使用 go:embed
//go:embed locale.*.toml
var LocaleFS embed.FS
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
bundle.LoadMessageFileFS(LocaleFS, "locale.es.toml")
```
创建一个 Localizer 以便用于一组首选语言。
```go
func(w http.ResponseWriter, r *http.Request) {
lang := r.FormValue("lang")
accept := r.Header.Get("Accept-Language")
localizer := i18n.NewLocalizer(bundle, lang, accept)
}
```
使用此 Localizer 查找消息。
```go
localizer.Localize(&i18n.LocalizeConfig{
DefaultMessage: &i18n.Message{
ID: "PersonCats",
One: "{{.Name}} has {{.Count}} cat.",
Other: "{{.Name}} has {{.Count}} cats.",
},
TemplateData: map[string]interface{}{
"Name": "Nick",
"Count": 2,
},
PluralCount: 2,
}) // Nick 有两只猫
```
## goi18n 命令
[](https://pkg.go.dev/github.com/nicksnyder/go-i18n/v2/goi18n)
goi18n 命令管理 i18n 包所使用的消息文件。
```
go install -v github.com/nicksnyder/go-i18n/v2/goi18n@latest
goi18n -help
```
### 提取消息
使用 `goi18n extract` 将 Go 源文件中的所有 i18n.Message 结构中的文字提取到消息文件中以进行翻译。
```toml
# active.en.toml
[PersonCats]
description = "The number of cats a person has"
one = "{{.Name}} has {{.Count}} cat."
other = "{{.Name}} has {{.Count}} cats."
```
### 翻译一种新语言
1. 为你要添加的语言创建一个空的消息文件(例如:`translate.es.toml`)。
2. 运行 `goi18n merge active.en.toml translate.es.toml` 以将要翻译的消息填充到 `translate.es.toml` 中。
```toml
# translate.es.toml
[HelloPerson]
hash = "sha1-5b49bfdad81fedaeefb224b0ffc2acc58b09cff5"
other = "Hello {{.Name}}"
```
3. 完成 `translate.es.toml` 的翻译之后,将其重命名为 `active.es.toml`。
```toml
# active.es.toml
[HelloPerson]
hash = "sha1-5b49bfdad81fedaeefb224b0ffc2acc58b09cff5"
other = "Hola {{.Name}}"
```
4. 加载 `active.es.toml` 到你的 Bundle 中。
```go
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
bundle.LoadMessageFile("active.es.toml")
```
### 翻译新消息
如果你在程序中添加了新消息:
1. 运行 `goi18n extract` 以将新的消息更新到 `active.en.toml`。
2. 运行 `goi18n merge active.*.toml` 以生成更新后的 `translate.*.toml` 文件。
3. 翻译 `translate.*.toml` 文件中的所有消息。
4. 运行 `goi18n merge active.*.toml translate.*.toml` 将翻译后的消息合并到活跃消息文件
(Active Message Files)中。
## 进一步的信息和示例:
- 阅读[文档](https://pkg.go.dev/github.com/nicksnyder/go-i18n/v2)。
- 查看[代码示例](https://github.com/nicksnyder/go-i18n/blob/main/i18n/example_test.go)和
[测试](https://github.com/nicksnyder/go-i18n/blob/main/i18n/localizer_test.go)。
- 查看示例[程序](https://github.com/nicksnyder/go-i18n/tree/main/example)。
## 许可证
go-i18n 使用在 MIT 许可来提供。更多的相关信息,请参 [LICENSE](LICENSE) 文件。
go-i18n-2.6.0/.github/dependabot.yml 0000664 0000000 0000000 00000000321 14774252036 0017077 0 ustar 00root root 0000000 0000000 version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "monthly"
go-i18n-2.6.0/.github/workflows/ 0000775 0000000 0000000 00000000000 14774252036 0016310 5 ustar 00root root 0000000 0000000 go-i18n-2.6.0/.github/workflows/build.yml 0000664 0000000 0000000 00000002133 14774252036 0020131 0 ustar 00root root 0000000 0000000 name: Build
on:
push:
branches:
- main
pull_request:
jobs:
build:
name: Build (go:${{ matrix.go-version.name }})
runs-on: ubuntu-latest
strategy:
matrix:
go-version:
- name: latest
version: 1.24.x
- name: previous
version: 1.23.x
steps:
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version.version }}
- name: Git checkout
uses: actions/checkout@v4
- name: Build
uses: goreleaser/goreleaser-action@v6
with:
version: latest
args: release --clean --snapshot
- name: Test
run: go test -race -coverprofile=coverage.txt -covermode=atomic ./...
- name: Upload coverage
uses: codecov/codecov-action@v5
if: matrix.go-version.name == 'latest'
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
- name: Lint
uses: golangci/golangci-lint-action@v7
if: matrix.go-version.name == 'latest'
with:
version: v2.0 go-i18n-2.6.0/.github/workflows/goreleaser.yml 0000664 0000000 0000000 00000001014 14774252036 0021157 0 ustar 00root root 0000000 0000000 name: goreleaser
on:
push:
tags:
- '*'
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
- name: Release
uses: goreleaser/goreleaser-action@v6
with:
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
go-i18n-2.6.0/.gitignore 0000664 0000000 0000000 00000000130 14774252036 0014675 0 ustar 00root root 0000000 0000000 *.a
_*
output/
.DS_Store
*.test
*.swp
example/example
goi18n/goi18n
dist/
coverage.txt
go-i18n-2.6.0/.golangci.yaml 0000664 0000000 0000000 00000000142 14774252036 0015435 0 ustar 00root root 0000000 0000000 version: "2"
linters:
exclusions:
rules:
- path: rule_gen\.go
text: "QF1001:"
go-i18n-2.6.0/.goreleaser.yml 0000664 0000000 0000000 00000000541 14774252036 0015644 0 ustar 00root root 0000000 0000000 version: 2
builds:
- binary: goi18n
main: ./goi18n/
goos:
- windows
- darwin
- linux
goarch:
- amd64
env:
- CGO_ENABLED=0
archives:
- format: binary
name_template: "{{ .Binary }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}"
before:
hooks:
- go mod download
checksum:
name_template: "checksums.txt"
go-i18n-2.6.0/CHANGELOG.md 0000664 0000000 0000000 00000004546 14774252036 0014535 0 ustar 00root root 0000000 0000000 # Changelog
Major version changes are documented in the changelog.
To see the documentation for minor or patch version, [view the release notes](https://github.com/nicksnyder/go-i18n/releases).
## v2
### Motivation
The first commit to this project was January 2012 (go1 had not yet been released) and v1.0.0 was tagged June 2015 (go1.4).
This project has evolved with the Go ecosystem since then in a backwards compatible way,
but there is a growing list of issues and warts that cannot be addressed without breaking compatibility.
v2 is rewrite of the API from first principals to make it more idiomatic Go, and to resolve a backlog of issues: https://github.com/nicksnyder/go-i18n/milestone/1
### Improvements
* Use `golang.org/x/text/language` to get standardized behavior for language matching (https://github.com/nicksnyder/go-i18n/issues/30, https://github.com/nicksnyder/go-i18n/issues/44, https://github.com/nicksnyder/go-i18n/issues/76)
* Remove global state so that the race detector does not complain when downstream projects run tests that depend on go-i18n in parallel (https://github.com/nicksnyder/go-i18n/issues/82)
* Automatically extract messages from Go source code (https://github.com/nicksnyder/go-i18n/issues/64)
* Provide clearer documentation and examples (https://github.com/nicksnyder/go-i18n/issues/27)
* Reduce complexity of file format for simple translations (https://github.com/nicksnyder/go-i18n/issues/85)
* Support descriptions for messages (https://github.com/nicksnyder/go-i18n/issues/8)
* Support custom template delimiters (https://github.com/nicksnyder/go-i18n/issues/88)
### Upgrading from v1
The i18n package in v2 is completely different than v1.
Refer to the [documentation](https://pkg.go.dev/github.com/nicksnyder/go-i18n/v2/i18n) and [README](https://github.com/nicksnyder/go-i18n/blob/master/README.md) for guidance.
The goi18n command has similarities and differences:
* `goi18n merge` has a new implementation but accomplishes the same task.
* `goi18n extract` extracts messages from Go source files.
* `goi18n constants` no longer exists. Prefer to extract messages directly from Go source files.
v2 makes changes to the canonical message file format, but you can use v1 message files with v2. Message files will be converted to the new format the first time they are processed by the new `goi18n merge` command.
v2 requires Go 1.9 or newer.
go-i18n-2.6.0/LICENSE 0000664 0000000 0000000 00000002075 14774252036 0013724 0 ustar 00root root 0000000 0000000 Copyright (c) 2014 Nick Snyder https://github.com/nicksnyder
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
go-i18n-2.6.0/README.md 0000664 0000000 0000000 00000012036 14774252036 0014174 0 ustar 00root root 0000000 0000000 # go-i18n
 [](https://goreportcard.com/report/github.com/nicksnyder/go-i18n/v2) [](https://codecov.io/gh/nicksnyder/go-i18n)
go-i18n is a Go [package](#package-i18n) and a [command](#command-goi18n) that helps you translate Go programs into multiple languages.
- Supports [pluralized strings](http://cldr.unicode.org/index/cldr-spec/plural-rules) for all 200+ languages in the [Unicode Common Locale Data Repository (CLDR)](https://www.unicode.org/cldr/charts/28/supplemental/language_plural_rules.html).
- Code and tests are [automatically generated](https://github.com/nicksnyder/go-i18n/tree/main/internal/plural/codegen) from [CLDR data](http://cldr.unicode.org/index/downloads).
- Supports strings with named variables using [text/template](http://golang.org/pkg/text/template/) syntax.
- Supports message files of any format (e.g. JSON, TOML, YAML).
## Package i18n
[](https://pkg.go.dev/github.com/nicksnyder/go-i18n/v2/i18n)
The i18n package provides support for looking up messages according to a set of locale preferences.
```go
import "github.com/nicksnyder/go-i18n/v2/i18n"
```
Create a Bundle to use for the lifetime of your application.
```go
bundle := i18n.NewBundle(language.English)
```
Load translations into your bundle during initialization.
```go
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
bundle.LoadMessageFile("es.toml")
```
```go
// If use go:embed
//go:embed locale.*.toml
var LocaleFS embed.FS
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
bundle.LoadMessageFileFS(LocaleFS, "locale.es.toml")
```
Create a Localizer to use for a set of language preferences.
```go
func(w http.ResponseWriter, r *http.Request) {
lang := r.FormValue("lang")
accept := r.Header.Get("Accept-Language")
localizer := i18n.NewLocalizer(bundle, lang, accept)
}
```
Use the Localizer to lookup messages.
```go
localizer.Localize(&i18n.LocalizeConfig{
DefaultMessage: &i18n.Message{
ID: "PersonCats",
One: "{{.Name}} has {{.Count}} cat.",
Other: "{{.Name}} has {{.Count}} cats.",
},
TemplateData: map[string]interface{}{
"Name": "Nick",
"Count": 2,
},
PluralCount: 2,
}) // Nick has 2 cats.
```
## Command goi18n
[](https://pkg.go.dev/github.com/nicksnyder/go-i18n/v2/goi18n)
The goi18n command manages message files used by the i18n package.
```
go install -v github.com/nicksnyder/go-i18n/v2/goi18n@latest
goi18n -help
```
### Extracting messages
Use `goi18n extract` to extract all i18n.Message struct literals in Go source files to a message file for translation.
```toml
# active.en.toml
[PersonCats]
description = "The number of cats a person has"
one = "{{.Name}} has {{.Count}} cat."
other = "{{.Name}} has {{.Count}} cats."
```
### Translating a new language
1. Create an empty message file for the language that you want to add (e.g. `translate.es.toml`).
2. Run `goi18n merge active.en.toml translate.es.toml` to populate `translate.es.toml` with the messages to be translated.
```toml
# translate.es.toml
[HelloPerson]
hash = "sha1-5b49bfdad81fedaeefb224b0ffc2acc58b09cff5"
other = "Hello {{.Name}}"
```
3. After `translate.es.toml` has been translated, rename it to `active.es.toml`.
```toml
# active.es.toml
[HelloPerson]
hash = "sha1-5b49bfdad81fedaeefb224b0ffc2acc58b09cff5"
other = "Hola {{.Name}}"
```
4. Load `active.es.toml` into your bundle.
```go
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
bundle.LoadMessageFile("active.es.toml")
```
### Translating new messages
If you have added new messages to your program:
1. Run `goi18n extract` to update `active.en.toml` with the new messages.
2. Run `goi18n merge active.*.toml` to generate updated `translate.*.toml` files.
3. Translate all the messages in the `translate.*.toml` files.
4. Run `goi18n merge active.*.toml translate.*.toml` to merge the translated messages into the active message files.
## For more information and examples:
- Read the [documentation](https://pkg.go.dev/github.com/nicksnyder/go-i18n/v2).
- Look at the [code examples](https://github.com/nicksnyder/go-i18n/blob/main/i18n/example_test.go) and [tests](https://github.com/nicksnyder/go-i18n/blob/main/i18n/localizer_test.go).
- Look at an example [application](https://github.com/nicksnyder/go-i18n/tree/main/example).
## Translations of this document
Community translations of this document may be found in the [.github](.github) folder.
These translations are maintained by the community, and are not maintained by the author of this project.
They are not guaranteed to be accurate or up-to-date.
## License
go-i18n is available under the MIT license. See the [LICENSE](LICENSE) file for more info.
go-i18n-2.6.0/example/ 0000775 0000000 0000000 00000000000 14774252036 0014346 5 ustar 00root root 0000000 0000000 go-i18n-2.6.0/example/README.md 0000664 0000000 0000000 00000000527 14774252036 0015631 0 ustar 00root root 0000000 0000000 # Example
This directory contains an example project that uses go-i18n.
```
go run main.go
```
Then open http://localhost:8080 in your web browser.
You can customize the template data and locale via query parameters like this:
http://localhost:8080/?name=Nick&unreadEmailCount=2
http://localhost:8080/?name=Nick&unreadEmailCount=2&lang=es
go-i18n-2.6.0/example/active.en.toml 0000664 0000000 0000000 00000000612 14774252036 0017116 0 ustar 00root root 0000000 0000000 HelloPerson = "Hello {{.Name}}"
[MyUnreadEmails]
description = "The number of unread emails I have"
one = "I have {{.PluralCount}} unread email."
other = "I have {{.PluralCount}} unread emails."
[PersonUnreadEmails]
description = "The number of unread emails a person has"
one = "{{.Name}} has {{.UnreadEmailCount}} unread email."
other = "{{.Name}} has {{.UnreadEmailCount}} unread emails."
go-i18n-2.6.0/example/active.es.toml 0000664 0000000 0000000 00000001173 14774252036 0017126 0 ustar 00root root 0000000 0000000 [HelloPerson]
hash = "sha1-5b49bfdad81fedaeefb224b0ffc2acc58b09cff5"
other = "Hola {{.Name}}"
[MyUnreadEmails]
description = "The number of unread emails I have"
hash = "sha1-6a65d17f53981a3657db1897630e9cb069053ea8"
one = "Tengo {{.PluralCount}} correo electrónico sin leer"
other = "Tengo {{.PluralCount}} correos electrónicos no leídos"
[PersonUnreadEmails]
description = "The number of unread emails a person has"
hash = "sha1-3a672fa89c5c8564bb233c907638004983792464"
one = "{{.Name}} tiene {{.UnreadEmailCount}} correo electrónico no leído"
other = "{{.Name}} tiene {{.UnreadEmailCount}} correos electrónicos no leídos"
go-i18n-2.6.0/example/main.go 0000664 0000000 0000000 00000004606 14774252036 0015627 0 ustar 00root root 0000000 0000000 // Command example runs a sample webserver that uses go-i18n/v2/i18n.
package main
import (
"fmt"
"html/template"
"log"
"net/http"
"strconv"
"github.com/BurntSushi/toml"
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
)
var page = template.Must(template.New("").Parse(`
{{.Title}}
{{range .Paragraphs}}{{.}}
{{end}}
`))
func main() {
bundle := i18n.NewBundle(language.English)
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
// No need to load active.en.toml since we are providing default translations.
// bundle.MustLoadMessageFile("active.en.toml")
bundle.MustLoadMessageFile("active.es.toml")
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
lang := r.FormValue("lang")
accept := r.Header.Get("Accept-Language")
localizer := i18n.NewLocalizer(bundle, lang, accept)
name := r.FormValue("name")
if name == "" {
name = "Bob"
}
unreadEmailCount, _ := strconv.ParseInt(r.FormValue("unreadEmailCount"), 10, 64)
helloPerson := localizer.MustLocalize(&i18n.LocalizeConfig{
DefaultMessage: &i18n.Message{
ID: "HelloPerson",
Other: "Hello {{.Name}}",
},
TemplateData: map[string]string{
"Name": name,
},
})
myUnreadEmails := localizer.MustLocalize(&i18n.LocalizeConfig{
DefaultMessage: &i18n.Message{
ID: "MyUnreadEmails",
Description: "The number of unread emails I have",
One: "I have {{.PluralCount}} unread email.",
Other: "I have {{.PluralCount}} unread emails.",
},
PluralCount: unreadEmailCount,
})
personUnreadEmails := localizer.MustLocalize(&i18n.LocalizeConfig{
DefaultMessage: &i18n.Message{
ID: "PersonUnreadEmails",
Description: "The number of unread emails a person has",
One: "{{.Name}} has {{.UnreadEmailCount}} unread email.",
Other: "{{.Name}} has {{.UnreadEmailCount}} unread emails.",
},
PluralCount: unreadEmailCount,
TemplateData: map[string]interface{}{
"Name": name,
"UnreadEmailCount": unreadEmailCount,
},
})
err := page.Execute(w, map[string]interface{}{
"Title": helloPerson,
"Paragraphs": []string{
myUnreadEmails,
personUnreadEmails,
},
})
if err != nil {
panic(err)
}
})
fmt.Println("Listening on http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
go-i18n-2.6.0/go.mod 0000664 0000000 0000000 00000000253 14774252036 0014021 0 ustar 00root root 0000000 0000000 module github.com/nicksnyder/go-i18n/v2
go 1.23.0
toolchain go1.24.2
require (
github.com/BurntSushi/toml v1.5.0
golang.org/x/text v0.23.0
gopkg.in/yaml.v3 v3.0.1
)
go-i18n-2.6.0/go.sum 0000664 0000000 0000000 00000001256 14774252036 0014052 0 ustar 00root root 0000000 0000000 github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
go-i18n-2.6.0/goi18n/ 0000775 0000000 0000000 00000000000 14774252036 0014020 5 ustar 00root root 0000000 0000000 go-i18n-2.6.0/goi18n/active.en.toml 0000664 0000000 0000000 00000000000 14774252036 0016557 0 ustar 00root root 0000000 0000000 go-i18n-2.6.0/goi18n/common_test.go 0000664 0000000 0000000 00000000440 14774252036 0016674 0 ustar 00root root 0000000 0000000 package main
import (
"os"
"testing"
)
func mustTempDir(prefix string) string {
outdir, err := os.MkdirTemp("", prefix)
if err != nil {
panic(err)
}
return outdir
}
func mustRemoveAll(t *testing.T, path string) {
if err := os.RemoveAll(path); err != nil {
t.Fatal(err)
}
}
go-i18n-2.6.0/goi18n/extract_command.go 0000664 0000000 0000000 00000014410 14774252036 0017517 0 ustar 00root root 0000000 0000000 package main
import (
"flag"
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
"github.com/nicksnyder/go-i18n/v2/i18n"
)
func usageExtract() {
fmt.Fprintf(os.Stderr, `usage: goi18n extract [options] [paths]
Extract walks the files and directories in paths and extracts all messages to a single file.
If no files or paths are provided, it walks the current working directory.
xx-yy.active.format
This file contains messages that should be loaded at runtime.
Flags:
-sourceLanguage tag
The language tag of the extracted messages (e.g. en, en-US, zh-Hant-CN).
Default: en
-outdir directory
Write message files to this directory.
Default: .
-format format
Output message files in this format.
Supported formats: json, toml, yaml
Default: toml
`)
}
type extractCommand struct {
paths []string
sourceLanguage languageTag
outdir string
format string
}
func (ec *extractCommand) name() string {
return "extract"
}
func (ec *extractCommand) parse(args []string) error {
flags := flag.NewFlagSet("extract", flag.ExitOnError)
flags.Usage = usageExtract
flags.Var(&ec.sourceLanguage, "sourceLanguage", "en")
flags.StringVar(&ec.outdir, "outdir", ".", "")
flags.StringVar(&ec.format, "format", "toml", "")
if err := flags.Parse(args); err != nil {
return err
}
ec.paths = flags.Args()
return nil
}
func (ec *extractCommand) execute() error {
if len(ec.paths) == 0 {
ec.paths = []string{"."}
}
messages := []*i18n.Message{}
for _, path := range ec.paths {
if err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
if filepath.Ext(path) != ".go" {
return nil
}
// Don't extract from test files.
if strings.HasSuffix(path, "_test.go") {
return nil
}
buf, err := os.ReadFile(path)
if err != nil {
return err
}
msgs, err := extractMessages(buf)
if err != nil {
return err
}
messages = append(messages, msgs...)
return nil
}); err != nil {
return err
}
}
messageTemplates := map[string]*i18n.MessageTemplate{}
for _, m := range messages {
if mt := i18n.NewMessageTemplate(m); mt != nil {
if duplicateMessage, ok := messageTemplates[m.ID]; ok && !reflect.DeepEqual(mt, duplicateMessage) {
return &duplicateMessageIDErr{messageID: m.ID}
}
messageTemplates[m.ID] = mt
}
}
path, content, err := writeFile(ec.outdir, "active", ec.sourceLanguage.Tag(), ec.format, messageTemplates, true)
if err != nil {
return err
}
return os.WriteFile(path, content, 0666)
}
type duplicateMessageIDErr struct {
messageID string
}
func (e *duplicateMessageIDErr) Error() string {
return fmt.Sprintf("duplicate message ID: %s", e.messageID)
}
// extractMessages extracts messages from the bytes of a Go source file.
func extractMessages(buf []byte) ([]*i18n.Message, error) {
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "", buf, parser.AllErrors)
if err != nil {
return nil, err
}
extractor := newExtractor(file)
ast.Walk(extractor, file)
return extractor.messages, nil
}
func newExtractor(file *ast.File) *extractor {
return &extractor{i18nPackageName: i18nPackageName(file)}
}
type extractor struct {
i18nPackageName string
messages []*i18n.Message
}
func (e *extractor) Visit(node ast.Node) ast.Visitor {
e.extractMessages(node)
return e
}
func (e *extractor) extractMessages(node ast.Node) {
cl, ok := node.(*ast.CompositeLit)
if !ok {
return
}
switch t := cl.Type.(type) {
case *ast.SelectorExpr:
if !e.isMessageType(t) {
return
}
e.extractMessage(cl)
case *ast.ArrayType:
if !e.isMessageType(t.Elt) {
return
}
for _, el := range cl.Elts {
ecl, ok := el.(*ast.CompositeLit)
if !ok {
continue
}
e.extractMessage(ecl)
}
case *ast.MapType:
if !e.isMessageType(t.Value) {
return
}
for _, el := range cl.Elts {
kve, ok := el.(*ast.KeyValueExpr)
if !ok {
continue
}
vcl, ok := kve.Value.(*ast.CompositeLit)
if !ok {
continue
}
e.extractMessage(vcl)
}
}
}
func (e *extractor) isMessageType(expr ast.Expr) bool {
se := unwrapSelectorExpr(expr)
if se == nil {
return false
}
if se.Sel.Name != "Message" && se.Sel.Name != "LocalizeConfig" {
return false
}
x, ok := se.X.(*ast.Ident)
if !ok {
return false
}
return x.Name == e.i18nPackageName
}
func unwrapSelectorExpr(e ast.Expr) *ast.SelectorExpr {
switch et := e.(type) {
case *ast.SelectorExpr:
return et
case *ast.StarExpr:
se, _ := et.X.(*ast.SelectorExpr)
return se
default:
return nil
}
}
func (e *extractor) extractMessage(cl *ast.CompositeLit) {
data := make(map[string]string)
for _, elt := range cl.Elts {
kve, ok := elt.(*ast.KeyValueExpr)
if !ok {
continue
}
key, ok := kve.Key.(*ast.Ident)
if !ok {
continue
}
v, ok := extractStringLiteral(kve.Value)
if !ok {
continue
}
data[key.Name] = v
}
if len(data) == 0 {
return
}
if messageID := data["MessageID"]; messageID != "" {
data["ID"] = messageID
}
e.messages = append(e.messages, i18n.MustNewMessage(data))
}
func extractStringLiteral(expr ast.Expr) (string, bool) {
switch v := expr.(type) {
case *ast.BasicLit:
if v.Kind != token.STRING {
return "", false
}
s, err := strconv.Unquote(v.Value)
if err != nil {
return "", false
}
return s, true
case *ast.BinaryExpr:
if v.Op != token.ADD {
return "", false
}
x, ok := extractStringLiteral(v.X)
if !ok {
return "", false
}
y, ok := extractStringLiteral(v.Y)
if !ok {
return "", false
}
return x + y, true
case *ast.Ident:
if v.Obj == nil {
return "", false
}
switch z := v.Obj.Decl.(type) {
case *ast.ValueSpec:
if len(z.Values) == 0 {
return "", false
}
s, ok := extractStringLiteral(z.Values[0])
if !ok {
return "", false
}
return s, true
}
case *ast.CallExpr:
if fun, ok := v.Fun.(*ast.Ident); ok && fun.Name == "string" {
return extractStringLiteral(v.Args[0])
}
}
return "", false
}
func i18nPackageName(file *ast.File) string {
for _, i := range file.Imports {
if i.Path.Kind == token.STRING && i.Path.Value == `"github.com/nicksnyder/go-i18n/v2/i18n"` {
if i.Name == nil {
return "i18n"
}
return i.Name.Name
}
}
return ""
}
go-i18n-2.6.0/goi18n/extract_command_test.go 0000664 0000000 0000000 00000016471 14774252036 0020567 0 ustar 00root root 0000000 0000000 package main
import (
"bytes"
"os"
"path/filepath"
"testing"
)
func TestExtract(t *testing.T) {
tests := []struct {
name string
fileName string
file string
activeFile []byte
expectedExitCode int
expectedErr error
}{
{
name: "no translations",
fileName: "file.go",
file: `package main`,
},
{
name: "global declaration",
fileName: "file.go",
file: `package main
import "github.com/nicksnyder/go-i18n/v2/i18n"
var m = &i18n.Message{
ID: "Plural ID",
}
`,
},
{
name: "duplicate ID with same message is not an error",
fileName: "file.go",
file: `package main
import "github.com/nicksnyder/go-i18n/v2/i18n"
var m1 = &i18n.Message{
ID: "m",
Other: "m",
}
var m2 = &i18n.Message{
ID: "m",
Other: "m",
}
`,
activeFile: []byte(`m = "m"
`),
},
{
name: "duplicate ID with different message is an error",
fileName: "file.go",
file: `package main
import "github.com/nicksnyder/go-i18n/v2/i18n"
var m1 = &i18n.Message{
ID: "m",
Other: "m1",
}
var m2 = &i18n.Message{
ID: "m",
Other: "m2",
}
`,
expectedExitCode: 1,
},
{
name: "escape newline",
fileName: "file.go",
file: `package main
import "github.com/nicksnyder/go-i18n/v2/i18n"
var hasnewline = &i18n.Message{
ID: "hasnewline",
Other: "\nfoo\nbar\\",
}
`,
activeFile: []byte(`hasnewline = "\nfoo\nbar\\"
`),
},
{
name: "escape",
fileName: "file.go",
file: `package main
import "github.com/nicksnyder/go-i18n/v2/i18n"
var a = &i18n.Message{
ID: "a",
Other: "a \" b",
}
var b = &i18n.Message{
ID: "b",
Other: ` + "`" + `a " b` + "`" + `,
}
`,
activeFile: []byte(`a = "a \" b"
b = "a \" b"
`),
},
{
name: "array",
fileName: "file.go",
file: `package main
import "github.com/nicksnyder/go-i18n/v2/i18n"
var a = []*i18n.Message{
{
ID: "a",
Other: "a",
},
{
ID: "b",
Other: "b",
},
}
`,
activeFile: []byte(`a = "a"
b = "b"
`),
},
{
name: "map",
fileName: "file.go",
file: `package main
import "github.com/nicksnyder/go-i18n/v2/i18n"
var a = map[string]*i18n.Message{
"a": {
ID: "a",
Other: "a",
},
"b": {
ID: "b",
Other: "b",
},
}
`,
activeFile: []byte(`a = "a"
b = "b"
`),
},
{
name: "no extract from test",
fileName: "file_test.go",
file: `package main
import "github.com/nicksnyder/go-i18n/v2/i18n"
func main() {
bundle := i18n.NewBundle(language.English)
l := i18n.NewLocalizer(bundle, "en")
l.Localize(&i18n.LocalizeConfig{MessageID: "Plural ID"})
}
`,
},
{
name: "must short form id only",
fileName: "file.go",
file: `package main
import "github.com/nicksnyder/go-i18n/v2/i18n"
func main() {
bundle := i18n.NewBundle(language.English)
l := i18n.NewLocalizer(bundle, "en")
l.MustLocalize(&i18n.LocalizeConfig{MessageID: "Plural ID"})
}
`,
},
{
name: "custom package name",
fileName: "file.go",
file: `package main
import bar "github.com/nicksnyder/go-i18n/v2/i18n"
func main() {
_ := &bar.Message{
ID: "Plural ID",
}
}
`,
},
{
name: "exhaustive plural translation",
fileName: "file.go",
file: `package main
import "github.com/nicksnyder/go-i18n/v2/i18n"
func main() {
_ := &i18n.Message{
ID: "Plural ID",
Description: "Plural description",
Zero: "Zero translation",
One: "One translation",
Two: "Two translation",
Few: "Few translation",
Many: "Many translation",
Other: "Other translation",
}
}
`,
activeFile: []byte(`["Plural ID"]
description = "Plural description"
few = "Few translation"
many = "Many translation"
one = "One translation"
other = "Other translation"
two = "Two translation"
zero = "Zero translation"
`),
},
{
name: "concat id",
fileName: "file.go",
file: `package main
import "github.com/nicksnyder/go-i18n/v2/i18n"
func main() {
_ := &i18n.Message{
ID: "Plural" +
" " +
"ID",
}
}
`,
},
{
name: "global declaration",
fileName: "file.go",
file: `package main
import "github.com/nicksnyder/go-i18n/v2/i18n"
const constID = "ConstantID"
var m = &i18n.Message{
ID: constID,
Other: "ID is a constant",
}
`,
activeFile: []byte(`ConstantID = "ID is a constant"
`),
},
{
name: "undefined identifier in composite lit",
fileName: "file.go",
file: `package main
import "github.com/nicksnyder/go-i18n/v2/i18n"
var m = &i18n.LocalizeConfig{
Funcs: Funcs,
}
`,
},
{
name: "casted const",
fileName: "file.go",
file: `package main
import "github.com/nicksnyder/go-i18n/v2/i18n"
type ConstType string
const Const ConstType = "my const"
var m = &i18n.LocalizeConfig{
ID: "id",
Other: string(Const),
}
`,
activeFile: []byte(`id = "my const"
`),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
indir := mustTempDir("TestExtractCommandIn")
defer mustRemoveAll(t, indir)
outdir := mustTempDir("TestExtractCommandOut")
defer mustRemoveAll(t, outdir)
inpath := filepath.Join(indir, test.fileName)
if err := os.WriteFile(inpath, []byte(test.file), 0666); err != nil {
t.Fatal(err)
}
code := testableMain([]string{"extract", "-outdir", outdir, indir})
if code != test.expectedExitCode {
t.Fatalf("expected exit code %d; got %d\n", test.expectedExitCode, code)
}
files, err := os.ReadDir(outdir)
if err != nil {
t.Fatal(err)
}
if code != 0 {
if len(files) != 0 {
t.Fatalf("expected 0 files; got %#v", files)
}
return
}
if len(files) != 1 {
t.Fatalf("expected 1 file; got %#v", files)
}
actualFile := files[0]
expectedName := "active.en.toml"
if actualFile.Name() != expectedName {
t.Fatalf("expected %s; got %s", expectedName, actualFile.Name())
}
outpath := filepath.Join(outdir, actualFile.Name())
actual, err := os.ReadFile(outpath)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(actual, test.activeFile) {
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", test.activeFile, actual)
}
})
}
}
func TestExtractCommand(t *testing.T) {
outdir, err := os.MkdirTemp("", "TestExtractCommand")
if err != nil {
t.Fatal(err)
}
defer mustRemoveAll(t, outdir)
if code := testableMain([]string{"extract", "-outdir", outdir, "../example/"}); code != 0 {
t.Fatalf("expected exit code 0; got %d", code)
}
actual, err := os.ReadFile(filepath.Join(outdir, "active.en.toml"))
if err != nil {
t.Fatal(err)
}
expected := []byte(`HelloPerson = "Hello {{.Name}}"
[MyUnreadEmails]
description = "The number of unread emails I have"
one = "I have {{.PluralCount}} unread email."
other = "I have {{.PluralCount}} unread emails."
[PersonUnreadEmails]
description = "The number of unread emails a person has"
one = "{{.Name}} has {{.UnreadEmailCount}} unread email."
other = "{{.Name}} has {{.UnreadEmailCount}} unread emails."
`)
if !bytes.Equal(actual, expected) {
t.Fatalf("files not equal\nactual:\n%s\nexpected:\n%s", actual, expected)
}
}
go-i18n-2.6.0/goi18n/main.go 0000664 0000000 0000000 00000007342 14774252036 0015301 0 ustar 00root root 0000000 0000000 // Command goi18n manages message files used by the i18n package.
//
// go get -u github.com/nicksnyder/go-i18n/v2/goi18n
// goi18n -help
//
// Use `goi18n extract` to create a message file that contains the messages defined in your Go source files.
//
// # en.toml
// [PersonCats]
// description = "The number of cats a person has"
// one = "{{.Name}} has {{.Count}} cat."
// other = "{{.Name}} has {{.Count}} cats."
//
// Use `goi18n merge` to create message files for translation.
//
// # translate.es.toml
// [PersonCats]
// description = "The number of cats a person has"
// hash = "sha1-f937a0e05e19bfe6cd70937c980eaf1f9832f091"
// one = "{{.Name}} has {{.Count}} cat."
// other = "{{.Name}} has {{.Count}} cats."
//
// Use `goi18n merge` to merge translated message files with your existing message files.
//
// # active.es.toml
// [PersonCats]
// description = "The number of cats a person has"
// hash = "sha1-f937a0e05e19bfe6cd70937c980eaf1f9832f091"
// one = "{{.Name}} tiene {{.Count}} gato."
// other = "{{.Name}} tiene {{.Count}} gatos."
//
// Load the active messages into your bundle.
//
// bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
// bundle.MustLoadMessageFile("active.es.toml")
package main
import (
"flag"
"fmt"
"os"
"golang.org/x/text/language"
)
func mainUsage() {
fmt.Fprintf(os.Stderr, `goi18n (v2) is a tool for managing message translations.
Usage:
goi18n command [arguments]
The commands are:
merge merge message files
extract extract messages from Go files
Workflow:
Use 'goi18n extract' to create a message file that contains the messages defined in your Go source files.
# en.toml
[PersonCats]
description = "The number of cats a person has"
one = "{{.Name}} has {{.Count}} cat."
other = "{{.Name}} has {{.Count}} cats."
Use 'goi18n merge' to create message files for translation.
# translate.es.toml
[PersonCats]
description = "The number of cats a person has"
hash = "sha1-f937a0e05e19bfe6cd70937c980eaf1f9832f091"
one = "{{.Name}} has {{.Count}} cat."
other = "{{.Name}} has {{.Count}} cats."
Use 'goi18n merge' to merge translated message files with your existing message files.
# active.es.toml
[PersonCats]
description = "The number of cats a person has"
hash = "sha1-f937a0e05e19bfe6cd70937c980eaf1f9832f091"
one = "{{.Name}} tiene {{.Count}} gato."
other = "{{.Name}} tiene {{.Count}} gatos."
Load the active messages into your bundle.
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
bundle.MustLoadMessageFile("active.es.toml")
`)
}
type command interface {
name() string
parse(arguments []string) error
execute() error
}
func main() {
os.Exit(testableMain(os.Args[1:]))
}
func testableMain(args []string) int {
flags := flag.NewFlagSet("goi18n", flag.ContinueOnError)
flags.Usage = mainUsage
if err := flags.Parse(args); err != nil {
if err == flag.ErrHelp {
return 2
}
return 1
}
if flags.NArg() == 0 {
mainUsage()
return 2
}
commands := []command{
&mergeCommand{},
&extractCommand{},
}
cmdName := flags.Arg(0)
for _, cmd := range commands {
if cmd.name() == cmdName {
if err := cmd.parse(flags.Args()[1:]); err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
if err := cmd.execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
return 0
}
}
fmt.Fprintf(os.Stderr, "goi18n: unknown subcommand %s\n", cmdName)
return 1
}
type languageTag language.Tag
func (lt languageTag) String() string {
return lt.Tag().String()
}
func (lt *languageTag) Set(value string) error {
t, err := language.Parse(value)
if err != nil {
return err
}
*lt = languageTag(t)
return nil
}
func (lt languageTag) Tag() language.Tag {
tag := language.Tag(lt)
if tag.IsRoot() {
return language.English
}
return tag
}
go-i18n-2.6.0/goi18n/main_test.go 0000664 0000000 0000000 00000001100 14774252036 0016322 0 ustar 00root root 0000000 0000000 package main
import (
"strings"
"testing"
)
func TestMain(t *testing.T) {
testCases := []struct {
args []string
exitCode int
}{
{
args: []string{"-help"},
exitCode: 2,
},
{
args: []string{"extract"},
exitCode: 0,
},
{
args: []string{"merge"},
exitCode: 1,
},
}
for _, testCase := range testCases {
t.Run(strings.Join(testCase.args, " "), func(t *testing.T) {
if code := testableMain(testCase.args); code != testCase.exitCode {
t.Fatalf("expected exit code %d; got %d", testCase.exitCode, code)
}
})
}
}
go-i18n-2.6.0/goi18n/marshal.go 0000664 0000000 0000000 00000003705 14774252036 0016003 0 ustar 00root root 0000000 0000000 package main
import (
"bytes"
"encoding/json"
"fmt"
"path/filepath"
"github.com/BurntSushi/toml"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/nicksnyder/go-i18n/v2/internal/plural"
"golang.org/x/text/language"
yaml "gopkg.in/yaml.v3"
)
func writeFile(outdir, label string, langTag language.Tag, format string, messageTemplates map[string]*i18n.MessageTemplate, sourceLanguage bool) (path string, content []byte, err error) {
v := marshalValue(messageTemplates, sourceLanguage)
content, err = marshal(v, format)
if err != nil {
return "", nil, fmt.Errorf("failed to marshal %s strings to %s: %s", langTag, format, err)
}
path = filepath.Join(outdir, fmt.Sprintf("%s.%s.%s", label, langTag, format))
return
}
func marshalValue(messageTemplates map[string]*i18n.MessageTemplate, sourceLanguage bool) interface{} {
v := make(map[string]interface{}, len(messageTemplates))
for id, template := range messageTemplates {
if other := template.PluralTemplates[plural.Other]; sourceLanguage && len(template.PluralTemplates) == 1 &&
other != nil && template.Description == "" && template.LeftDelim == "" && template.RightDelim == "" {
v[id] = other.Src
} else {
m := map[string]string{}
if template.Description != "" {
m["description"] = template.Description
}
if !sourceLanguage {
m["hash"] = template.Hash
}
for pluralForm, template := range template.PluralTemplates {
m[string(pluralForm)] = template.Src
}
v[id] = m
}
}
return v
}
func marshal(v interface{}, format string) ([]byte, error) {
switch format {
case "json":
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
err := enc.Encode(v)
return buf.Bytes(), err
case "toml":
var buf bytes.Buffer
enc := toml.NewEncoder(&buf)
enc.Indent = ""
err := enc.Encode(v)
return buf.Bytes(), err
case "yaml":
return yaml.Marshal(v)
}
return nil, fmt.Errorf("unsupported format: %s", format)
}
go-i18n-2.6.0/goi18n/marshal_test.go 0000664 0000000 0000000 00000000500 14774252036 0017030 0 ustar 00root root 0000000 0000000 package main
import "testing"
func TestMarshal(t *testing.T) {
actual, err := marshal(map[string]string{
"&": "&",
}, "json")
if err != nil {
t.Fatal(err)
}
expected := `{
"&": "&"
}
`
if a := string(actual); a != expected {
t.Fatalf("\nexpected:\n%s\n\ngot\n%s", expected, a)
}
}
go-i18n-2.6.0/goi18n/merge_command.go 0000664 0000000 0000000 00000021531 14774252036 0017146 0 ustar 00root root 0000000 0000000 package main
import (
"crypto/sha1"
"encoding/json"
"flag"
"fmt"
"io"
"os"
"github.com/BurntSushi/toml"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/nicksnyder/go-i18n/v2/internal"
"github.com/nicksnyder/go-i18n/v2/internal/plural"
"golang.org/x/text/language"
yaml "gopkg.in/yaml.v3"
)
func usageMerge() {
fmt.Fprintf(os.Stderr, `usage: goi18n merge [options] [message files]
Merge reads all messages in the message files and produces two files per language.
xx-yy.active.format
This file contains messages that should be loaded at runtime.
xx-yy.translate.format
This file contains messages that are empty and should be translated.
Message file names must have a suffix of a supported format (e.g. ".json") and
contain a valid language tag as defined by RFC 5646 (e.g. "en-us", "fr", "zh-hant", etc.).
To add support for a new language, create an empty translation file with the
appropriate name and pass it in to goi18n merge.
Flags:
-sourceLanguage tag
Translate messages from this language (e.g. en, en-US, zh-Hant-CN)
Default: en
-outdir directory
Write message files to this directory.
Default: .
-format format
Output message files in this format.
Supported formats: json, toml, yaml
Default: toml
`)
}
type mergeCommand struct {
messageFiles []string
sourceLanguage languageTag
outdir string
format string
}
func (mc *mergeCommand) name() string {
return "merge"
}
func (mc *mergeCommand) parse(args []string) error {
flags := flag.NewFlagSet("merge", flag.ExitOnError)
flags.Usage = usageMerge
flags.Var(&mc.sourceLanguage, "sourceLanguage", "en")
flags.StringVar(&mc.outdir, "outdir", ".", "")
flags.StringVar(&mc.format, "format", "toml", "")
if err := flags.Parse(args); err != nil {
return err
}
mc.messageFiles = flags.Args()
return nil
}
func (mc *mergeCommand) execute() error {
if len(mc.messageFiles) < 1 {
return fmt.Errorf("need at least one message file to parse")
}
inFiles := make(map[string][]byte)
for _, path := range mc.messageFiles {
content, err := os.ReadFile(path)
if err != nil {
return err
}
inFiles[path] = content
}
ops, err := merge(inFiles, mc.sourceLanguage.Tag(), mc.outdir, mc.format)
if err != nil {
return err
}
for path, content := range ops.writeFiles {
if err := os.WriteFile(path, content, 0666); err != nil {
return err
}
}
for _, path := range ops.deleteFiles {
// Ignore error since it isn't guaranteed to exist.
_ = os.Remove(path)
}
return nil
}
type fileSystemOp struct {
writeFiles map[string][]byte
deleteFiles []string
}
func merge(messageFiles map[string][]byte, sourceLanguageTag language.Tag, outdir, outputFormat string) (*fileSystemOp, error) {
unmerged := make(map[language.Tag][]map[string]*i18n.MessageTemplate)
sourceMessageTemplates := make(map[string]*i18n.MessageTemplate)
unmarshalFuncs := map[string]i18n.UnmarshalFunc{
"json": json.Unmarshal,
"toml": toml.Unmarshal,
"yaml": yaml.Unmarshal,
}
for path, content := range messageFiles {
mf, err := i18n.ParseMessageFileBytes(content, path, unmarshalFuncs)
if err != nil {
return nil, fmt.Errorf("failed to load message file %s: %s", path, err)
}
templates := map[string]*i18n.MessageTemplate{}
for _, m := range mf.Messages {
template := i18n.NewMessageTemplate(m)
if template == nil {
continue
}
templates[m.ID] = template
}
if mf.Tag == sourceLanguageTag {
for _, template := range templates {
if sourceMessageTemplates[template.ID] != nil {
return nil, fmt.Errorf("multiple source translations for id %q", template.ID)
}
template.Hash = hash(template)
sourceMessageTemplates[template.ID] = template
}
}
unmerged[mf.Tag] = append(unmerged[mf.Tag], templates)
}
if len(sourceMessageTemplates) == 0 {
return nil, fmt.Errorf("no messages found for source locale %s", sourceLanguageTag)
}
pluralRules := plural.DefaultRules()
all := make(map[language.Tag]map[string]*i18n.MessageTemplate)
all[sourceLanguageTag] = sourceMessageTemplates
for _, srcTemplate := range sourceMessageTemplates {
for dstLangTag, messageTemplates := range unmerged {
if dstLangTag == sourceLanguageTag {
continue
}
pluralRule := pluralRules.Rule(dstLangTag)
if pluralRule == nil {
// Non-standard languages not supported because
// we don't know if translations are complete or not.
continue
}
if all[dstLangTag] == nil {
all[dstLangTag] = make(map[string]*i18n.MessageTemplate)
}
dstMessageTemplate := all[dstLangTag][srcTemplate.ID]
if dstMessageTemplate == nil {
dstMessageTemplate = &i18n.MessageTemplate{
Message: &i18n.Message{
ID: srcTemplate.ID,
Description: srcTemplate.Description,
Hash: srcTemplate.Hash,
},
PluralTemplates: make(map[plural.Form]*internal.Template),
}
all[dstLangTag][srcTemplate.ID] = dstMessageTemplate
}
// Check all unmerged message templates for this message id.
for _, messageTemplates := range messageTemplates {
unmergedTemplate := messageTemplates[srcTemplate.ID]
if unmergedTemplate == nil {
continue
}
// Ignore empty hashes for v1 backward compatibility.
if unmergedTemplate.Hash != "" && unmergedTemplate.Hash != srcTemplate.Hash {
// This was translated from different content so discard.
continue
}
// Merge in the translated messages.
for pluralForm := range pluralRule.PluralForms {
dt := unmergedTemplate.PluralTemplates[pluralForm]
if dt != nil && dt.Src != "" {
dstMessageTemplate.PluralTemplates[pluralForm] = dt
}
}
}
}
}
translate := make(map[language.Tag]map[string]*i18n.MessageTemplate)
active := make(map[language.Tag]map[string]*i18n.MessageTemplate)
for langTag, messageTemplates := range all {
active[langTag] = make(map[string]*i18n.MessageTemplate)
if langTag == sourceLanguageTag {
active[langTag] = messageTemplates
continue
}
pluralRule := pluralRules.Rule(langTag)
if pluralRule == nil {
// Non-standard languages not supported because
// we don't know if translations are complete or not.
continue
}
for _, messageTemplate := range messageTemplates {
srcMessageTemplate := sourceMessageTemplates[messageTemplate.ID]
activeMessageTemplate, translateMessageTemplate := activeDst(srcMessageTemplate, messageTemplate, pluralRule)
if translateMessageTemplate != nil {
if translate[langTag] == nil {
translate[langTag] = make(map[string]*i18n.MessageTemplate)
}
translate[langTag][messageTemplate.ID] = translateMessageTemplate
}
if activeMessageTemplate != nil {
active[langTag][messageTemplate.ID] = activeMessageTemplate
}
}
}
writeFiles := make(map[string][]byte, len(translate)+len(active))
for langTag, messageTemplates := range translate {
path, content, err := writeFile(outdir, "translate", langTag, outputFormat, messageTemplates, false)
if err != nil {
return nil, err
}
writeFiles[path] = content
}
deleteFiles := []string{}
for langTag, messageTemplates := range active {
path, content, err := writeFile(outdir, "active", langTag, outputFormat, messageTemplates, langTag == sourceLanguageTag)
if err != nil {
return nil, err
}
if len(content) > 0 {
writeFiles[path] = content
} else {
deleteFiles = append(deleteFiles, path)
}
}
return &fileSystemOp{writeFiles: writeFiles, deleteFiles: deleteFiles}, nil
}
// activeDst returns the active part of the dst and whether dst is a complete translation of src.
func activeDst(src, dst *i18n.MessageTemplate, pluralRule *plural.Rule) (active *i18n.MessageTemplate, translateMessageTemplate *i18n.MessageTemplate) {
pluralForms := pluralRule.PluralForms
if len(src.PluralTemplates) == 1 {
pluralForms = map[plural.Form]struct{}{
plural.Other: {},
}
}
for pluralForm := range pluralForms {
dt := dst.PluralTemplates[pluralForm]
if dt == nil || dt.Src == "" {
if translateMessageTemplate == nil {
translateMessageTemplate = &i18n.MessageTemplate{
Message: &i18n.Message{
ID: src.ID,
Description: src.Description,
Hash: src.Hash,
},
PluralTemplates: make(map[plural.Form]*internal.Template),
}
}
srcPlural := src.PluralTemplates[pluralForm]
if srcPlural == nil {
srcPlural = src.PluralTemplates[plural.Other]
}
translateMessageTemplate.PluralTemplates[pluralForm] = srcPlural
continue
}
if active == nil {
active = &i18n.MessageTemplate{
Message: &i18n.Message{
ID: src.ID,
Description: src.Description,
Hash: src.Hash,
},
PluralTemplates: make(map[plural.Form]*internal.Template),
}
}
active.PluralTemplates[pluralForm] = dt
}
return
}
func hash(t *i18n.MessageTemplate) string {
h := sha1.New()
_, _ = io.WriteString(h, t.Description)
_, _ = io.WriteString(h, t.PluralTemplates[plural.Other].Src)
return fmt.Sprintf("sha1-%x", h.Sum(nil))
}
go-i18n-2.6.0/goi18n/merge_command_test.go 0000664 0000000 0000000 00000037716 14774252036 0020221 0 ustar 00root root 0000000 0000000 package main
import (
"bytes"
"os"
"path/filepath"
"testing"
"golang.org/x/text/language"
)
type testCase struct {
name string
inFiles map[string][]byte
sourceLanguage language.Tag
outFiles map[string][]byte
deleteFiles []string
}
func expectFile(s string) []byte {
// Trimming leading newlines gives nicer formatting for file literals in test cases.
return bytes.TrimLeft([]byte(s), "\n")
}
func TestMerge(t *testing.T) {
testCases := []*testCase{
{
name: "single identity",
sourceLanguage: language.AmericanEnglish,
inFiles: map[string][]byte{
"one.en-US.toml": []byte("1HelloMessage = \"Hello\"\n"),
},
outFiles: map[string][]byte{
"active.en-US.toml": []byte("1HelloMessage = \"Hello\"\n"),
},
},
{
name: "single identity, one localization text missing",
sourceLanguage: language.AmericanEnglish,
inFiles: map[string][]byte{
"one.en-US.toml": []byte(`
1HelloMessage = ""
Body = "Finally some text!"
`),
},
outFiles: map[string][]byte{
"active.en-US.toml": []byte(`Body = "Finally some text!"
`),
},
},
{
name: "plural identity",
sourceLanguage: language.AmericanEnglish,
inFiles: map[string][]byte{
"active.en-US.toml": []byte(`
[UnreadEmails]
description = "Message that tells the user how many unread emails they have"
one = "{{.Count}} unread email"
other = "{{.Count}} unread emails"
`),
},
outFiles: map[string][]byte{
"active.en-US.toml": expectFile(`
[UnreadEmails]
description = "Message that tells the user how many unread emails they have"
one = "{{.Count}} unread email"
other = "{{.Count}} unread emails"
`),
},
},
{
name: "plural identity, missing localization text",
sourceLanguage: language.AmericanEnglish,
inFiles: map[string][]byte{
"active.en-US.toml": []byte(`
Body = "Some text!"
[MissingTranslation]
description = "I wonder what it was?!"
one = ""
other = ""
`),
},
outFiles: map[string][]byte{
"active.en-US.toml": expectFile(`Body = "Some text!"
`),
},
},
{
name: "migrate source lang from v1 format",
sourceLanguage: language.AmericanEnglish,
inFiles: map[string][]byte{
"one.en-US.json": []byte(`[
{
"id": "simple",
"translation": "simple translation"
},
{
"id": "everything",
"translation": {
"zero": "zero translation",
"one": "one translation",
"two": "two translation",
"few": "few translation",
"many": "many translation",
"other": "other translation"
}
}
]`),
},
outFiles: map[string][]byte{
"active.en-US.toml": expectFile(`
simple = "simple translation"
[everything]
few = "few translation"
many = "many translation"
one = "one translation"
other = "other translation"
two = "two translation"
zero = "zero translation"
`),
},
},
{
name: "migrate source lang from v1 flat format",
sourceLanguage: language.AmericanEnglish,
inFiles: map[string][]byte{
"one.en-US.json": []byte(`{
"simple": {
"other": "simple translation"
},
"everything": {
"zero": "zero translation",
"one": "one translation",
"two": "two translation",
"few": "few translation",
"many": "many translation",
"other": "other translation"
}
}`),
},
outFiles: map[string][]byte{
"active.en-US.toml": expectFile(`
simple = "simple translation"
[everything]
few = "few translation"
many = "many translation"
one = "one translation"
other = "other translation"
two = "two translation"
zero = "zero translation"
`),
},
},
{
name: "merge source files",
sourceLanguage: language.AmericanEnglish,
inFiles: map[string][]byte{
"one.en-US.toml": []byte("1HelloMessage = \"Hello\"\n"),
"two.en-US.toml": []byte("2GoodbyeMessage = \"Goodbye\"\n"),
},
outFiles: map[string][]byte{
"active.en-US.toml": []byte("1HelloMessage = \"Hello\"\n2GoodbyeMessage = \"Goodbye\"\n"),
},
},
{
name: "missing hash",
sourceLanguage: language.AmericanEnglish,
inFiles: map[string][]byte{
"en-US.toml": []byte(`
1HelloMessage = "Hello"
`),
"es-ES.toml": []byte(`
[1HelloMessage]
other = "Hola"
`),
},
outFiles: map[string][]byte{
"active.en-US.toml": expectFile(`
1HelloMessage = "Hello"
`),
"active.es-ES.toml": expectFile(`
[1HelloMessage]
hash = "sha1-f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0"
other = "Hola"
`),
},
},
{
name: "add single translation",
sourceLanguage: language.AmericanEnglish,
inFiles: map[string][]byte{
"en-US.toml": []byte(`
1HelloMessage = "Hello"
2GoodbyeMessage = "Goodbye"
`),
"es-ES.toml": []byte(`
[1HelloMessage]
hash = "sha1-f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0"
other = "Hola"
`),
},
outFiles: map[string][]byte{
"active.en-US.toml": expectFile(`
1HelloMessage = "Hello"
2GoodbyeMessage = "Goodbye"
`),
"active.es-ES.toml": expectFile(`
[1HelloMessage]
hash = "sha1-f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0"
other = "Hola"
`),
"translate.es-ES.toml": expectFile(`
[2GoodbyeMessage]
hash = "sha1-b5b29c53e3c71cb9c6581ab053d7758fab8ca24d"
other = "Goodbye"
`),
},
},
{
name: "remove single translation",
sourceLanguage: language.AmericanEnglish,
inFiles: map[string][]byte{
"en-US.toml": []byte(`
1HelloMessage = "Hello"
`),
"es-ES.toml": []byte(`
[1HelloMessage]
hash = "sha1-f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0"
other = "Hola"
[2GoodbyeMessage]
hash = "sha1-b5b29c53e3c71cb9c6581ab053d7758fab8ca24d"
other = "Goodbye"
`),
},
outFiles: map[string][]byte{
"active.en-US.toml": expectFile(`
1HelloMessage = "Hello"
`),
"active.es-ES.toml": expectFile(`
[1HelloMessage]
hash = "sha1-f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0"
other = "Hola"
`),
},
},
{
name: "edit single translation",
sourceLanguage: language.AmericanEnglish,
inFiles: map[string][]byte{
"en-US.toml": []byte(`
1HelloMessage = "Hi"
`),
"es-ES.toml": []byte(`
[1HelloMessage]
hash = "sha1-f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0"
other = "Hola"
`),
},
outFiles: map[string][]byte{
"active.en-US.toml": expectFile(`
1HelloMessage = "Hi"
`),
"translate.es-ES.toml": expectFile(`
[1HelloMessage]
hash = "sha1-94dd9e08c129c785f7f256e82fbe0a30e6d1ae40"
other = "Hi"
`),
},
},
{
name: "add plural translation",
sourceLanguage: language.AmericanEnglish,
inFiles: map[string][]byte{
"en-US.toml": []byte(`
[UnreadEmails]
description = "Message that tells the user how many unread emails they have"
one = "{{.Count}} unread email"
other = "{{.Count}} unread emails"
`),
"es-ES.toml": nil,
"ar-AR.toml": nil,
"zh-CN.toml": nil,
},
outFiles: map[string][]byte{
"active.en-US.toml": expectFile(`
[UnreadEmails]
description = "Message that tells the user how many unread emails they have"
one = "{{.Count}} unread email"
other = "{{.Count}} unread emails"
`),
"translate.es-ES.toml": expectFile(`
[UnreadEmails]
description = "Message that tells the user how many unread emails they have"
hash = "sha1-5afbc91dfedb9755627655c365eb47a89e541099"
many = "{{.Count}} unread emails"
one = "{{.Count}} unread email"
other = "{{.Count}} unread emails"
`),
"translate.ar-AR.toml": expectFile(`
[UnreadEmails]
description = "Message that tells the user how many unread emails they have"
few = "{{.Count}} unread emails"
hash = "sha1-5afbc91dfedb9755627655c365eb47a89e541099"
many = "{{.Count}} unread emails"
one = "{{.Count}} unread email"
other = "{{.Count}} unread emails"
two = "{{.Count}} unread emails"
zero = "{{.Count}} unread emails"
`),
"translate.zh-CN.toml": expectFile(`
[UnreadEmails]
description = "Message that tells the user how many unread emails they have"
hash = "sha1-5afbc91dfedb9755627655c365eb47a89e541099"
other = "{{.Count}} unread emails"
`),
},
},
{
name: "remove plural translation",
sourceLanguage: language.AmericanEnglish,
inFiles: map[string][]byte{
"en-US.toml": []byte(`
1HelloMessage = "Hello"
`),
"es-ES.toml": []byte(`
[1HelloMessage]
hash = "sha1-f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0"
other = "Hola"
[UnreadEmails]
description = "Message that tells the user how many unread emails they have"
hash = "sha1-5afbc91dfedb9755627655c365eb47a89e541099"
one = "{{.Count}} unread emails"
other = "{{.Count}} unread emails"
`),
"ar-AR.toml": []byte(`
[1HelloMessage]
hash = "sha1-f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0"
other = "Hello"
[UnreadEmails]
description = "Message that tells the user how many unread emails they have"
few = "{{.Count}} unread emails"
hash = "sha1-5afbc91dfedb9755627655c365eb47a89e541099"
many = "{{.Count}} unread emails"
one = "{{.Count}} unread emails"
other = "{{.Count}} unread emails"
two = "{{.Count}} unread emails"
zero = "{{.Count}} unread emails"
`),
"zh-CN.toml": []byte(`
[1HelloMessage]
hash = "sha1-f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0"
other = "Hello"
[UnreadEmails]
description = "Message that tells the user how many unread emails they have"
hash = "sha1-5afbc91dfedb9755627655c365eb47a89e541099"
other = "{{.Count}} unread emails"
`),
},
outFiles: map[string][]byte{
"active.en-US.toml": expectFile(`
1HelloMessage = "Hello"
`),
"active.es-ES.toml": expectFile(`
[1HelloMessage]
hash = "sha1-f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0"
other = "Hola"
`),
"active.ar-AR.toml": expectFile(`
[1HelloMessage]
hash = "sha1-f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0"
other = "Hello"
`),
"active.zh-CN.toml": expectFile(`
[1HelloMessage]
hash = "sha1-f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0"
other = "Hello"
`),
},
},
{
name: "edit plural translation",
sourceLanguage: language.AmericanEnglish,
inFiles: map[string][]byte{
"en-US.toml": []byte(`
[UnreadEmails]
description = "Message that tells the user how many unread emails they have"
one = "{{.Count}} unread emails!"
other = "{{.Count}} unread emails!"
`),
"es-ES.toml": []byte(`
[UnreadEmails]
description = "Message that tells the user how many unread emails they have"
hash = "sha1-5afbc91dfedb9755627655c365eb47a89e541099"
one = "{{.Count}} unread emails"
other = "{{.Count}} unread emails"
`),
"ar-AR.toml": []byte(`
[UnreadEmails]
description = "Message that tells the user how many unread emails they have"
few = "{{.Count}} unread emails"
hash = "sha1-5afbc91dfedb9755627655c365eb47a89e541099"
many = "{{.Count}} unread emails"
one = "{{.Count}} unread emails"
other = "{{.Count}} unread emails"
two = "{{.Count}} unread emails"
zero = "{{.Count}} unread emails"
`),
"zh-CN.toml": []byte(`
[UnreadEmails]
description = "Message that tells the user how many unread emails they have"
hash = "sha1-5afbc91dfedb9755627655c365eb47a89e541099"
other = "{{.Count}} unread emails"
`),
},
deleteFiles: []string{
"active.es-ES.toml",
"active.ar-AR.toml",
"active.zh-CN.toml",
},
outFiles: map[string][]byte{
"active.en-US.toml": expectFile(`
[UnreadEmails]
description = "Message that tells the user how many unread emails they have"
one = "{{.Count}} unread emails!"
other = "{{.Count}} unread emails!"
`),
"translate.es-ES.toml": expectFile(`
[UnreadEmails]
description = "Message that tells the user how many unread emails they have"
hash = "sha1-92a24983c5bbc0c42462cdc252dca68ebdb46501"
many = "{{.Count}} unread emails!"
one = "{{.Count}} unread emails!"
other = "{{.Count}} unread emails!"
`),
"translate.ar-AR.toml": expectFile(`
[UnreadEmails]
description = "Message that tells the user how many unread emails they have"
few = "{{.Count}} unread emails!"
hash = "sha1-92a24983c5bbc0c42462cdc252dca68ebdb46501"
many = "{{.Count}} unread emails!"
one = "{{.Count}} unread emails!"
other = "{{.Count}} unread emails!"
two = "{{.Count}} unread emails!"
zero = "{{.Count}} unread emails!"
`),
"translate.zh-CN.toml": expectFile(`
[UnreadEmails]
description = "Message that tells the user how many unread emails they have"
hash = "sha1-92a24983c5bbc0c42462cdc252dca68ebdb46501"
other = "{{.Count}} unread emails!"
`),
},
},
{
name: "merge plural translation",
sourceLanguage: language.AmericanEnglish,
inFiles: map[string][]byte{
"en-US.toml": []byte(`
[UnreadEmails]
description = "Message that tells the user how many unread emails they have"
one = "{{.Count}} unread emails"
other = "{{.Count}} unread emails"
`),
"zero.ar-AR.toml": []byte(`
[UnreadEmails]
description = "Message that tells the user how many unread emails they have"
hash = "sha1-5afbc91dfedb9755627655c365eb47a89e541099"
zero = "{{.Count}} unread emails"
`),
"one.ar-AR.toml": []byte(`
[UnreadEmails]
description = "Message that tells the user how many unread emails they have"
hash = "sha1-5afbc91dfedb9755627655c365eb47a89e541099"
one = "{{.Count}} unread emails"
`),
"two.ar-AR.toml": []byte(`
[UnreadEmails]
description = "Message that tells the user how many unread emails they have"
hash = "sha1-5afbc91dfedb9755627655c365eb47a89e541099"
two = "{{.Count}} unread emails"
`),
"few.ar-AR.toml": []byte(`
[UnreadEmails]
description = "Message that tells the user how many unread emails they have"
few = "{{.Count}} unread emails"
hash = "sha1-5afbc91dfedb9755627655c365eb47a89e541099"
`),
"many.ar-AR.toml": []byte(`
[UnreadEmails]
description = "Message that tells the user how many unread emails they have"
hash = "sha1-5afbc91dfedb9755627655c365eb47a89e541099"
many = "{{.Count}} unread emails"
`),
"other.ar-AR.toml": []byte(`
[UnreadEmails]
description = "Message that tells the user how many unread emails they have"
hash = "sha1-5afbc91dfedb9755627655c365eb47a89e541099"
other = "{{.Count}} unread emails"
`),
},
outFiles: map[string][]byte{
"active.en-US.toml": expectFile(`
[UnreadEmails]
description = "Message that tells the user how many unread emails they have"
one = "{{.Count}} unread emails"
other = "{{.Count}} unread emails"
`),
"active.ar-AR.toml": expectFile(`
[UnreadEmails]
description = "Message that tells the user how many unread emails they have"
few = "{{.Count}} unread emails"
hash = "sha1-5afbc91dfedb9755627655c365eb47a89e541099"
many = "{{.Count}} unread emails"
one = "{{.Count}} unread emails"
other = "{{.Count}} unread emails"
two = "{{.Count}} unread emails"
zero = "{{.Count}} unread emails"
`),
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
indir := mustTempDir("TestMergeCommandIn")
defer mustRemoveAll(t, indir)
outdir := mustTempDir("TestMergeCommandOut")
defer mustRemoveAll(t, outdir)
infiles := make([]string, 0, len(testCase.inFiles))
for name, content := range testCase.inFiles {
path := filepath.Join(indir, name)
infiles = append(infiles, path)
if err := os.WriteFile(path, content, 0666); err != nil {
t.Fatal(err)
}
}
for _, name := range testCase.deleteFiles {
path := filepath.Join(outdir, name)
if err := os.WriteFile(path, []byte(`this file should get deleted`), 0666); err != nil {
t.Fatal(err)
}
}
args := append([]string{"merge", "-sourceLanguage", testCase.sourceLanguage.String(), "-outdir", outdir}, infiles...)
if code := testableMain(args); code != 0 {
t.Fatalf("expected exit code 0; got %d\n", code)
}
files, err := os.ReadDir(outdir)
if err != nil {
t.Fatal(err)
}
// Verify that all actual files have expected contents.
actualFiles := make(map[string]struct{}, len(files))
for _, f := range files {
actualFiles[f.Name()] = struct{}{}
if f.IsDir() {
t.Errorf("found unexpected dir %s", f.Name())
continue
}
path := filepath.Join(outdir, f.Name())
actual, err := os.ReadFile(path)
if err != nil {
t.Error(err)
continue
}
expected, ok := testCase.outFiles[f.Name()]
if !ok {
t.Errorf("found unexpected file %s with contents:\n%s\n", f.Name(), actual)
continue
}
if !bytes.Equal(actual, expected) {
t.Errorf("unexpected contents %s\ngot\n%s\nexpected\n%s", f.Name(), actual, expected)
continue
}
}
// Verify that all expected files are accounted for.
for name := range testCase.outFiles {
if _, ok := actualFiles[name]; !ok {
t.Errorf("did not find expected file %s", name)
}
}
})
}
}
go-i18n-2.6.0/i18n/ 0000775 0000000 0000000 00000000000 14774252036 0013472 5 ustar 00root root 0000000 0000000 go-i18n-2.6.0/i18n/bundle.go 0000664 0000000 0000000 00000010356 14774252036 0015277 0 ustar 00root root 0000000 0000000 package i18n
import (
"fmt"
"os"
"github.com/nicksnyder/go-i18n/v2/internal/plural"
"golang.org/x/text/language"
)
// UnmarshalFunc unmarshals data into v.
type UnmarshalFunc func(data []byte, v interface{}) error
// Bundle stores a set of messages and pluralization rules.
// Most applications only need a single bundle
// that is initialized early in the application's lifecycle.
// It is not goroutine safe to modify the bundle while Localizers
// are reading from it.
type Bundle struct {
defaultLanguage language.Tag
unmarshalFuncs map[string]UnmarshalFunc
messageTemplates map[language.Tag]map[string]*MessageTemplate
pluralRules plural.Rules
tags []language.Tag
matcher language.Matcher
}
// artTag is the language tag used for artificial languages
// https://en.wikipedia.org/wiki/Codes_for_constructed_languages
var artTag = language.MustParse("art")
// NewBundle returns a bundle with a default language and a default set of plural rules.
func NewBundle(defaultLanguage language.Tag) *Bundle {
b := &Bundle{
defaultLanguage: defaultLanguage,
pluralRules: plural.DefaultRules(),
}
b.pluralRules[artTag] = b.pluralRules.Rule(language.English)
b.addTag(defaultLanguage)
return b
}
// RegisterUnmarshalFunc registers an UnmarshalFunc for format.
func (b *Bundle) RegisterUnmarshalFunc(format string, unmarshalFunc UnmarshalFunc) {
if b.unmarshalFuncs == nil {
b.unmarshalFuncs = make(map[string]UnmarshalFunc)
}
b.unmarshalFuncs[format] = unmarshalFunc
}
// LoadMessageFile loads the bytes from path
// and then calls ParseMessageFileBytes.
func (b *Bundle) LoadMessageFile(path string) (*MessageFile, error) {
buf, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return b.ParseMessageFileBytes(buf, path)
}
// MustLoadMessageFile is similar to LoadMessageFile
// except it panics if an error happens.
func (b *Bundle) MustLoadMessageFile(path string) {
if _, err := b.LoadMessageFile(path); err != nil {
panic(err)
}
}
// ParseMessageFileBytes parses the bytes in buf to add translations to the bundle.
//
// The format of the file is everything after the last ".".
//
// The language tag of the file is everything after the second to last "." or after the last path separator, but before the format.
func (b *Bundle) ParseMessageFileBytes(buf []byte, path string) (*MessageFile, error) {
messageFile, err := ParseMessageFileBytes(buf, path, b.unmarshalFuncs)
if err != nil {
return nil, err
}
if err := b.AddMessages(messageFile.Tag, messageFile.Messages...); err != nil {
return nil, err
}
return messageFile, nil
}
// MustParseMessageFileBytes is similar to ParseMessageFileBytes
// except it panics if an error happens.
func (b *Bundle) MustParseMessageFileBytes(buf []byte, path string) {
if _, err := b.ParseMessageFileBytes(buf, path); err != nil {
panic(err)
}
}
// AddMessages adds messages for a language.
// It is useful if your messages are in a format not supported by ParseMessageFileBytes.
func (b *Bundle) AddMessages(tag language.Tag, messages ...*Message) error {
pluralRule := b.pluralRules.Rule(tag)
if pluralRule == nil {
return fmt.Errorf("no plural rule registered for %s", tag)
}
if b.messageTemplates == nil {
b.messageTemplates = map[language.Tag]map[string]*MessageTemplate{}
}
if b.messageTemplates[tag] == nil {
b.messageTemplates[tag] = map[string]*MessageTemplate{}
b.addTag(tag)
}
for _, m := range messages {
b.messageTemplates[tag][m.ID] = NewMessageTemplate(m)
}
return nil
}
// MustAddMessages is similar to AddMessages except it panics if an error happens.
func (b *Bundle) MustAddMessages(tag language.Tag, messages ...*Message) {
if err := b.AddMessages(tag, messages...); err != nil {
panic(err)
}
}
func (b *Bundle) addTag(tag language.Tag) {
for _, t := range b.tags {
if t == tag {
// Tag already exists
return
}
}
b.tags = append(b.tags, tag)
b.matcher = language.NewMatcher(b.tags)
}
// LanguageTags returns the list of language tags
// of all the translations loaded into the bundle
func (b *Bundle) LanguageTags() []language.Tag {
return b.tags
}
func (b *Bundle) getMessageTemplate(tag language.Tag, id string) *MessageTemplate {
templates := b.messageTemplates[tag]
if templates == nil {
return nil
}
return templates[id]
}
go-i18n-2.6.0/i18n/bundle_test.go 0000664 0000000 0000000 00000016440 14774252036 0016336 0 ustar 00root root 0000000 0000000 package i18n
import (
"fmt"
"reflect"
"testing"
"github.com/BurntSushi/toml"
"golang.org/x/text/language"
yaml "gopkg.in/yaml.v3"
)
var simpleMessage = MustNewMessage(map[string]string{
"id": "simple",
"other": "simple translation",
})
var detailMessage = MustNewMessage(map[string]string{
"id": "detail",
"description": "detail description",
"other": "detail translation",
})
var everythingMessage = MustNewMessage(map[string]string{
"id": "everything",
"description": "everything description",
"zero": "zero translation",
"one": "one translation",
"two": "two translation",
"few": "few translation",
"many": "many translation",
"other": "other translation",
"leftDelim": "<<",
"rightDelim": ">>",
})
func TestConcurrentAccess(t *testing.T) {
bundle := NewBundle(language.English)
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
bundle.MustParseMessageFileBytes([]byte(`
# Comment
hello = "world"
`), "en.toml")
count := 10
errch := make(chan error, count)
for i := 0; i < count; i++ {
go func() {
localized := NewLocalizer(bundle, "en").MustLocalize(&LocalizeConfig{MessageID: "hello"})
if localized != "world" {
errch <- fmt.Errorf(`expected "world"; got %q`, localized)
} else {
errch <- nil
}
}()
}
for i := 0; i < count; i++ {
if err := <-errch; err != nil {
t.Fatal(err)
}
}
}
func TestPseudoLanguage(t *testing.T) {
bundle := NewBundle(language.English)
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
expected := "nuqneH"
bundle.MustParseMessageFileBytes([]byte(`
# Comment
hello = "`+expected+`"
`), "art-x-klingon.toml")
{
localized, err := NewLocalizer(bundle, "art-x-klingon").Localize(&LocalizeConfig{MessageID: "hello"})
if err != nil {
t.Fatal(err)
}
if localized != expected {
t.Fatalf("expected %q\ngot %q", expected, localized)
}
}
{
localized, err := NewLocalizer(bundle, "art").Localize(&LocalizeConfig{MessageID: "hello"})
if err != nil {
t.Fatal(err)
}
if localized != expected {
t.Fatalf("expected %q\ngot %q", expected, localized)
}
}
{
expected := ""
localized, err := NewLocalizer(bundle, "en").Localize(&LocalizeConfig{MessageID: "hello"})
if err == nil {
t.Fatal(err)
}
if localized != expected {
t.Fatalf("expected %q\ngot %q", expected, localized)
}
}
}
func TestJSON(t *testing.T) {
bundle := NewBundle(language.English)
bundle.MustParseMessageFileBytes([]byte(`{
"simple": "simple translation",
"detail": {
"description": "detail description",
"other": "detail translation"
},
"everything": {
"description": "everything description",
"zero": "zero translation",
"one": "one translation",
"two": "two translation",
"few": "few translation",
"many": "many translation",
"other": "other translation",
"leftDelim": "<<",
"rightDelim": ">>"
}
}`), "en-US.json")
expectMessage(t, bundle, language.AmericanEnglish, "simple", simpleMessage)
expectMessage(t, bundle, language.AmericanEnglish, "detail", detailMessage)
expectMessage(t, bundle, language.AmericanEnglish, "everything", everythingMessage)
}
func TestYAML(t *testing.T) {
bundle := NewBundle(language.English)
bundle.RegisterUnmarshalFunc("yaml", yaml.Unmarshal)
bundle.MustParseMessageFileBytes([]byte(`
# Comment
simple: simple translation
# Comment
detail:
description: detail description
other: detail translation
# Comment
everything:
description: everything description
zero: zero translation
one: one translation
two: two translation
few: few translation
many: many translation
other: other translation
leftDelim: "<<"
rightDelim: ">>"
`), "en-US.yaml")
expectMessage(t, bundle, language.AmericanEnglish, "simple", simpleMessage)
expectMessage(t, bundle, language.AmericanEnglish, "detail", detailMessage)
expectMessage(t, bundle, language.AmericanEnglish, "everything", everythingMessage)
}
func TestInvalidYAML(t *testing.T) {
bundle := NewBundle(language.English)
bundle.RegisterUnmarshalFunc("yaml", yaml.Unmarshal)
_, err := bundle.ParseMessageFileBytes([]byte(`
# Comment
simple: simple translation
# Comment
detail:
description: detail description
other: detail translation
# Comment
everything:
description: everything description
zero: zero translation
one: one translation
two: two translation
few: few translation
many: many translation
other: other translation
leftDelim: "<<"
rightDelmin: ">>"
garbage: something
description: translation
`), "en-US.yaml")
expectedErr := &mixedKeysError{
reservedKeys: []string{"description"},
unreservedKeys: []string{"detail", "everything", "simple"},
}
if err == nil {
t.Fatalf("expected error %#v; got nil", expectedErr)
}
if err.Error() != expectedErr.Error() {
t.Fatalf("expected error %q; got %q", expectedErr, err)
}
if c := len(bundle.messageTemplates); c > 0 {
t.Fatalf("expected no message templates in bundle; got %d", c)
}
}
func TestTOML(t *testing.T) {
bundle := NewBundle(language.English)
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
bundle.MustParseMessageFileBytes([]byte(`
# Comment
simple = "simple translation"
# Comment
[detail]
description = "detail description"
other = "detail translation"
# Comment
[everything]
description = "everything description"
zero = "zero translation"
one = "one translation"
two = "two translation"
few = "few translation"
many = "many translation"
other = "other translation"
leftDelim = "<<"
rightDelim = ">>"
`), "en-US.toml")
expectMessage(t, bundle, language.AmericanEnglish, "simple", simpleMessage)
expectMessage(t, bundle, language.AmericanEnglish, "detail", detailMessage)
expectMessage(t, bundle, language.AmericanEnglish, "everything", everythingMessage)
}
func TestV1Format(t *testing.T) {
bundle := NewBundle(language.English)
bundle.MustParseMessageFileBytes([]byte(`[
{
"id": "simple",
"translation": "simple translation"
},
{
"id": "everything",
"translation": {
"zero": "zero translation",
"one": "one translation",
"two": "two translation",
"few": "few translation",
"many": "many translation",
"other": "other translation"
}
}
]
`), "en-US.json")
expectMessage(t, bundle, language.AmericanEnglish, "simple", simpleMessage)
expectMessage(t, bundle, language.AmericanEnglish, "everything", newV1EverythingMessage())
}
func TestV1FlatFormat(t *testing.T) {
bundle := NewBundle(language.English)
bundle.MustParseMessageFileBytes([]byte(`{
"simple": {
"other": "simple translation"
},
"everything": {
"zero": "zero translation",
"one": "one translation",
"two": "two translation",
"few": "few translation",
"many": "many translation",
"other": "other translation"
}
}
`), "en-US.json")
expectMessage(t, bundle, language.AmericanEnglish, "simple", simpleMessage)
expectMessage(t, bundle, language.AmericanEnglish, "everything", newV1EverythingMessage())
}
func expectMessage(t *testing.T, bundle *Bundle, tag language.Tag, messageID string, message *Message) {
expected := NewMessageTemplate(message)
actual := bundle.messageTemplates[tag][messageID]
if !reflect.DeepEqual(actual, expected) {
t.Errorf("bundle.MessageTemplates[%q][%q]\ngot %#v\nwant %#v", tag, messageID, actual, expected)
}
}
func newV1EverythingMessage() *Message {
e := *everythingMessage
e.Description = ""
e.LeftDelim = ""
e.RightDelim = ""
return &e
}
go-i18n-2.6.0/i18n/bundlefs.go 0000664 0000000 0000000 00000000607 14774252036 0015626 0 ustar 00root root 0000000 0000000 package i18n
import (
"io/fs"
)
// LoadMessageFileFS is like LoadMessageFile but instead of reading from the
// hosts operating system's file system it reads from the fs file system.
func (b *Bundle) LoadMessageFileFS(fsys fs.FS, path string) (*MessageFile, error) {
buf, err := fs.ReadFile(fsys, path)
if err != nil {
return nil, err
}
return b.ParseMessageFileBytes(buf, path)
}
go-i18n-2.6.0/i18n/doc.go 0000664 0000000 0000000 00000001517 14774252036 0014572 0 ustar 00root root 0000000 0000000 // Package i18n provides support for looking up messages
// according to a set of locale preferences.
//
// Create a Bundle to use for the lifetime of your application.
//
// bundle := i18n.NewBundle(language.English)
//
// Load translations into your bundle during initialization.
//
// bundle.LoadMessageFile("en-US.yaml")
//
// Create a Localizer to use for a set of language preferences.
//
// func(w http.ResponseWriter, r *http.Request) {
// lang := r.FormValue("lang")
// accept := r.Header.Get("Accept-Language")
// localizer := i18n.NewLocalizer(bundle, lang, accept)
// }
//
// Use the Localizer to lookup messages.
//
// localizer.MustLocalize(&i18n.LocalizeConfig{
// DefaultMessage: &i18n.Message{
// ID: "HelloWorld",
// Other: "Hello World!",
// },
// })
package i18n
go-i18n-2.6.0/i18n/example_test.go 0000664 0000000 0000000 00000007136 14774252036 0016522 0 ustar 00root root 0000000 0000000 package i18n_test
import (
"fmt"
"github.com/BurntSushi/toml"
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
)
func ExampleLocalizer_MustLocalize() {
bundle := i18n.NewBundle(language.English)
localizer := i18n.NewLocalizer(bundle, "en")
fmt.Println(localizer.MustLocalize(&i18n.LocalizeConfig{
DefaultMessage: &i18n.Message{
ID: "HelloWorld",
Other: "Hello World!",
},
}))
// Output:
// Hello World!
}
func ExampleLocalizer_MustLocalize_noDefaultMessage() {
bundle := i18n.NewBundle(language.English)
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
bundle.MustParseMessageFileBytes([]byte(`
HelloWorld = "Hello World!"
`), "en.toml")
bundle.MustParseMessageFileBytes([]byte(`
HelloWorld = "Hola Mundo!"
`), "es.toml")
{
localizer := i18n.NewLocalizer(bundle, "en-US")
fmt.Println(localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "HelloWorld"}))
}
{
localizer := i18n.NewLocalizer(bundle, "es-ES")
fmt.Println(localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "HelloWorld"}))
}
// Output:
// Hello World!
// Hola Mundo!
}
func ExampleLocalizer_MustLocalize_plural() {
bundle := i18n.NewBundle(language.English)
localizer := i18n.NewLocalizer(bundle, "en")
catsMessage := &i18n.Message{
ID: "Cats",
One: "I have {{.PluralCount}} cat.",
Other: "I have {{.PluralCount}} cats.",
}
fmt.Println(localizer.MustLocalize(&i18n.LocalizeConfig{
DefaultMessage: catsMessage,
PluralCount: 1,
}))
fmt.Println(localizer.MustLocalize(&i18n.LocalizeConfig{
DefaultMessage: catsMessage,
PluralCount: 2,
}))
fmt.Println(localizer.MustLocalize(&i18n.LocalizeConfig{
DefaultMessage: catsMessage,
PluralCount: "2.5",
}))
// Output:
// I have 1 cat.
// I have 2 cats.
// I have 2.5 cats.
}
func ExampleLocalizer_MustLocalize_template() {
bundle := i18n.NewBundle(language.English)
localizer := i18n.NewLocalizer(bundle, "en")
helloPersonMessage := &i18n.Message{
ID: "HelloPerson",
Other: "Hello {{.Name}}!",
}
fmt.Println(localizer.MustLocalize(&i18n.LocalizeConfig{
DefaultMessage: helloPersonMessage,
TemplateData: map[string]string{"Name": "Nick"},
}))
// Output:
// Hello Nick!
}
func ExampleLocalizer_MustLocalize_plural_template() {
bundle := i18n.NewBundle(language.English)
localizer := i18n.NewLocalizer(bundle, "en")
personCatsMessage := &i18n.Message{
ID: "PersonCats",
One: "{{.Name}} has {{.Count}} cat.",
Other: "{{.Name}} has {{.Count}} cats.",
}
fmt.Println(localizer.MustLocalize(&i18n.LocalizeConfig{
DefaultMessage: personCatsMessage,
PluralCount: 1,
TemplateData: map[string]interface{}{
"Name": "Nick",
"Count": 1,
},
}))
fmt.Println(localizer.MustLocalize(&i18n.LocalizeConfig{
DefaultMessage: personCatsMessage,
PluralCount: 2,
TemplateData: map[string]interface{}{
"Name": "Nick",
"Count": 2,
},
}))
fmt.Println(localizer.MustLocalize(&i18n.LocalizeConfig{
DefaultMessage: personCatsMessage,
PluralCount: "2.5",
TemplateData: map[string]interface{}{
"Name": "Nick",
"Count": "2.5",
},
}))
// Output:
// Nick has 1 cat.
// Nick has 2 cats.
// Nick has 2.5 cats.
}
func ExampleLocalizer_MustLocalize_customTemplateDelims() {
bundle := i18n.NewBundle(language.English)
localizer := i18n.NewLocalizer(bundle, "en")
helloPersonMessage := &i18n.Message{
ID: "HelloPerson",
Other: "Hello <<.Name>>!",
LeftDelim: "<<",
RightDelim: ">>",
}
fmt.Println(localizer.MustLocalize(&i18n.LocalizeConfig{
DefaultMessage: helloPersonMessage,
TemplateData: map[string]string{"Name": "Nick"},
}))
// Output:
// Hello Nick!
}
go-i18n-2.6.0/i18n/language_test.go 0000664 0000000 0000000 00000001443 14774252036 0016645 0 ustar 00root root 0000000 0000000 package i18n_test
import (
"testing"
"golang.org/x/text/language"
)
var matcher language.Matcher
func BenchmarkNewMatcher(b *testing.B) {
langs := []language.Tag{
language.English,
language.AmericanEnglish,
language.BritishEnglish,
language.Spanish,
language.EuropeanSpanish,
language.Portuguese,
language.French,
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
matcher = language.NewMatcher(langs)
}
}
func BenchmarkMatchStrings(b *testing.B) {
langs := []language.Tag{
language.English,
language.AmericanEnglish,
language.BritishEnglish,
language.Spanish,
language.EuropeanSpanish,
language.Portuguese,
language.French,
}
matcher := language.NewMatcher(langs)
b.ResetTimer()
for i := 0; i < b.N; i++ {
language.MatchStrings(matcher, "en-US,en;q=0.9")
}
}
go-i18n-2.6.0/i18n/localizer.go 0000664 0000000 0000000 00000017167 14774252036 0016021 0 ustar 00root root 0000000 0000000 package i18n
import (
"fmt"
texttemplate "text/template"
"github.com/nicksnyder/go-i18n/v2/i18n/template"
"github.com/nicksnyder/go-i18n/v2/internal/plural"
"golang.org/x/text/language"
)
// Localizer provides Localize and MustLocalize methods that return localized messages.
// Localize and MustLocalize methods use a language.Tag matching algorithm based
// on the best possible value. This algorithm may cause an unexpected language.Tag returned
// value depending on the order of the tags stored in memory. For example, if the bundle
// used to create a Localizer instance ingested locales following this order
// ["en-US", "en-GB", "en-IE", "en"] and the locale "en" is asked, the underlying matching
// algorithm will return "en-US" thinking it is the best match possible. More information
// about the algorithm in this Github issue: https://github.com/golang/go/issues/49176.
// There is additional informations inside the Go code base:
// https://github.com/golang/text/blob/master/language/match.go#L142
type Localizer struct {
// bundle contains the messages that can be returned by the Localizer.
bundle *Bundle
// tags is the list of language tags that the Localizer checks
// in order when localizing a message.
tags []language.Tag
}
// NewLocalizer returns a new Localizer that looks up messages
// in the bundle according to the language preferences in langs.
// It can parse Accept-Language headers as defined in http://www.ietf.org/rfc/rfc2616.txt.
func NewLocalizer(bundle *Bundle, langs ...string) *Localizer {
return &Localizer{
bundle: bundle,
tags: parseTags(langs),
}
}
func parseTags(langs []string) []language.Tag {
tags := []language.Tag{}
for _, lang := range langs {
t, _, err := language.ParseAcceptLanguage(lang)
if err != nil {
continue
}
tags = append(tags, t...)
}
return tags
}
// LocalizeConfig configures a call to the Localize method on Localizer.
type LocalizeConfig struct {
// MessageID is the id of the message to lookup.
// This field is ignored if DefaultMessage is set.
MessageID string
// TemplateData is the data passed when executing the message's template.
// If TemplateData is nil and PluralCount is not nil, then the message template
// will be executed with data that contains the plural count.
TemplateData interface{}
// PluralCount determines which plural form of the message is used.
PluralCount interface{}
// DefaultMessage is used if the message is not found in any message files.
DefaultMessage *Message
// Funcs is used to configure a template.TextParser if TemplateParser is not set.
Funcs texttemplate.FuncMap
// The TemplateParser to use for parsing templates.
// If one is not set, a template.TextParser is used (configured with Funcs if it is set).
TemplateParser template.Parser
}
var defaultTextParser = &template.TextParser{}
func (lc *LocalizeConfig) getTemplateParser() template.Parser {
if lc.TemplateParser != nil {
return lc.TemplateParser
}
if lc.Funcs != nil {
return &template.TextParser{
Funcs: lc.Funcs,
}
}
return defaultTextParser
}
type invalidPluralCountErr struct {
messageID string
pluralCount interface{}
err error
}
func (e *invalidPluralCountErr) Error() string {
return fmt.Sprintf("invalid plural count %#v for message id %q: %s", e.pluralCount, e.messageID, e.err)
}
// MessageNotFoundErr is returned from Localize when a message could not be found.
type MessageNotFoundErr struct {
Tag language.Tag
MessageID string
}
func (e *MessageNotFoundErr) Error() string {
return fmt.Sprintf("message %q not found in language %q", e.MessageID, e.Tag)
}
type messageIDMismatchErr struct {
messageID string
defaultMessageID string
}
func (e *messageIDMismatchErr) Error() string {
return fmt.Sprintf("message id %q does not match default message id %q", e.messageID, e.defaultMessageID)
}
// Localize returns a localized message.
func (l *Localizer) Localize(lc *LocalizeConfig) (string, error) {
msg, _, err := l.LocalizeWithTag(lc)
return msg, err
}
// LocalizeMessage returns a localized message.
func (l *Localizer) LocalizeMessage(msg *Message) (string, error) {
return l.Localize(&LocalizeConfig{
DefaultMessage: msg,
})
}
// TODO: uncomment this (and the test) when extract has been updated to extract these call sites too.
// Localize returns a localized message.
// func (l *Localizer) LocalizeMessageID(messageID string) (string, error) {
// return l.Localize(&LocalizeConfig{
// MessageID: messageID,
// })
// }
// LocalizeWithTag returns a localized message and the language tag.
// It may return a best effort localized message even if an error happens.
func (l *Localizer) LocalizeWithTag(lc *LocalizeConfig) (string, language.Tag, error) {
messageID := lc.MessageID
if lc.DefaultMessage != nil {
if messageID != "" && messageID != lc.DefaultMessage.ID {
return "", language.Und, &messageIDMismatchErr{messageID: messageID, defaultMessageID: lc.DefaultMessage.ID}
}
messageID = lc.DefaultMessage.ID
}
var operands *plural.Operands
templateData := lc.TemplateData
if lc.PluralCount != nil {
var err error
operands, err = plural.NewOperands(lc.PluralCount)
if err != nil {
return "", language.Und, &invalidPluralCountErr{messageID: messageID, pluralCount: lc.PluralCount, err: err}
}
if templateData == nil {
templateData = map[string]interface{}{
"PluralCount": lc.PluralCount,
}
}
}
tag, template, err := l.getMessageTemplate(messageID, lc.DefaultMessage)
if template == nil {
return "", language.Und, err
}
pluralForm := l.pluralForm(tag, operands)
templateParser := lc.getTemplateParser()
msg, err2 := template.execute(pluralForm, templateData, templateParser)
if err2 != nil {
if err == nil {
err = err2
}
// Attempt to fallback to "Other" pluralization in case translations are incomplete.
if pluralForm != plural.Other {
msg2, err3 := template.execute(plural.Other, templateData, templateParser)
if err3 == nil {
msg = msg2
}
}
}
return msg, tag, err
}
func (l *Localizer) getMessageTemplate(id string, defaultMessage *Message) (language.Tag, *MessageTemplate, error) {
_, i, _ := l.bundle.matcher.Match(l.tags...)
tag := l.bundle.tags[i]
mt := l.bundle.getMessageTemplate(tag, id)
if mt != nil {
return tag, mt, nil
}
if tag == l.bundle.defaultLanguage {
if defaultMessage == nil {
return language.Und, nil, &MessageNotFoundErr{Tag: tag, MessageID: id}
}
mt := NewMessageTemplate(defaultMessage)
if mt == nil {
return language.Und, nil, &MessageNotFoundErr{Tag: tag, MessageID: id}
}
return tag, mt, nil
}
// Fallback to default language in bundle.
mt = l.bundle.getMessageTemplate(l.bundle.defaultLanguage, id)
if mt != nil {
return l.bundle.defaultLanguage, mt, &MessageNotFoundErr{Tag: tag, MessageID: id}
}
// Fallback to default message.
if defaultMessage == nil {
return language.Und, nil, &MessageNotFoundErr{Tag: tag, MessageID: id}
}
return l.bundle.defaultLanguage, NewMessageTemplate(defaultMessage), &MessageNotFoundErr{Tag: tag, MessageID: id}
}
func (l *Localizer) pluralForm(tag language.Tag, operands *plural.Operands) plural.Form {
if operands == nil {
return plural.Other
}
return l.bundle.pluralRules.Rule(tag).PluralFormFunc(operands)
}
// MustLocalize is similar to Localize, except it panics if an error happens.
func (l *Localizer) MustLocalize(lc *LocalizeConfig) string {
localized, err := l.Localize(lc)
if err != nil {
panic(err)
}
return localized
}
// MustLocalizeMessage is similar to LocalizeMessage, except it panics if an error happens.
func (l *Localizer) MustLocalizeMessage(msg *Message) string {
localized, err := l.LocalizeMessage(msg)
if err != nil {
panic(err)
}
return localized
}
go-i18n-2.6.0/i18n/localizer_test.go 0000664 0000000 0000000 00000055035 14774252036 0017054 0 ustar 00root root 0000000 0000000 package i18n
import (
"errors"
"fmt"
"reflect"
"testing"
gotmpl "text/template"
"github.com/nicksnyder/go-i18n/v2/i18n/template"
"github.com/nicksnyder/go-i18n/v2/internal/plural"
"golang.org/x/text/language"
)
type localizerTest struct {
name string
defaultLanguage language.Tag
messages map[language.Tag][]*Message
acceptLangs []string
conf *LocalizeConfig
expectedErr error
expectedLocalized string
}
func localizerTests() []localizerTest {
return []localizerTest{
{
name: "message id mismatch",
defaultLanguage: language.English,
acceptLangs: []string{"en"},
conf: &LocalizeConfig{
MessageID: "HelloWorld",
DefaultMessage: &Message{
ID: "DefaultHelloWorld",
},
},
expectedErr: &messageIDMismatchErr{messageID: "HelloWorld", defaultMessageID: "DefaultHelloWorld"},
},
{
name: "message id not mismatched",
defaultLanguage: language.English,
messages: map[language.Tag][]*Message{
language.English: {{ID: "HelloWorld", Other: "Hello!"}},
},
acceptLangs: []string{"en"},
conf: &LocalizeConfig{
MessageID: "HelloWorld",
DefaultMessage: &Message{
ID: "HelloWorld",
},
},
expectedLocalized: "Hello!",
},
{
name: "missing translation from default language",
defaultLanguage: language.English,
acceptLangs: []string{"en"},
conf: &LocalizeConfig{MessageID: "HelloWorld"},
expectedErr: &MessageNotFoundErr{Tag: language.English, MessageID: "HelloWorld"},
expectedLocalized: "",
},
{
name: "empty translation without fallback",
defaultLanguage: language.English,
messages: map[language.Tag][]*Message{
language.English: {{ID: "HelloWorld", Other: "Hello World!"}},
language.Spanish: {{ID: "HelloWorld"}},
},
acceptLangs: []string{"es"},
conf: &LocalizeConfig{MessageID: "HelloWorld"},
expectedErr: &MessageNotFoundErr{Tag: language.Spanish, MessageID: "HelloWorld"},
expectedLocalized: "Hello World!",
},
{
name: "missing translation from default language with other translation",
defaultLanguage: language.English,
messages: map[language.Tag][]*Message{
language.Spanish: {{ID: "HelloWorld", Other: "other"}},
},
acceptLangs: []string{"en"},
conf: &LocalizeConfig{MessageID: "HelloWorld"},
expectedErr: &MessageNotFoundErr{Tag: language.English, MessageID: "HelloWorld"},
expectedLocalized: "",
},
{
name: "missing translations from not default language",
defaultLanguage: language.English,
acceptLangs: []string{"es"},
conf: &LocalizeConfig{MessageID: "HelloWorld"},
expectedErr: &MessageNotFoundErr{Tag: language.English, MessageID: "HelloWorld"},
expectedLocalized: "",
},
{
name: "missing translation from not default language",
defaultLanguage: language.English,
messages: map[language.Tag][]*Message{
language.Spanish: {{ID: "SomethingElse", Other: "other"}},
},
acceptLangs: []string{"es"},
conf: &LocalizeConfig{MessageID: "HelloWorld"},
expectedErr: &MessageNotFoundErr{Tag: language.Spanish, MessageID: "HelloWorld"},
expectedLocalized: "",
},
{
name: "missing translation not default language with other translation",
defaultLanguage: language.English,
messages: map[language.Tag][]*Message{
language.French: {{ID: "HelloWorld", Other: "other"}},
language.Spanish: {{ID: "SomethingElse", Other: "other"}},
},
acceptLangs: []string{"es"},
conf: &LocalizeConfig{MessageID: "HelloWorld"},
expectedErr: &MessageNotFoundErr{Tag: language.Spanish, MessageID: "HelloWorld"},
expectedLocalized: "",
},
{
name: "accept default language, message in bundle",
defaultLanguage: language.English,
messages: map[language.Tag][]*Message{
language.English: {{ID: "HelloWorld", Other: "other"}},
},
acceptLangs: []string{"en"},
conf: &LocalizeConfig{MessageID: "HelloWorld"},
expectedLocalized: "other",
},
{
name: "accept default language, message in bundle, default message",
defaultLanguage: language.English,
messages: map[language.Tag][]*Message{
language.English: {{ID: "HelloWorld", Other: "bundle other"}},
},
acceptLangs: []string{"en"},
conf: &LocalizeConfig{
DefaultMessage: &Message{ID: "HelloWorld", Other: "default other"},
},
expectedLocalized: "bundle other",
},
{
name: "accept not default language, message in bundle",
defaultLanguage: language.English,
messages: map[language.Tag][]*Message{
language.Spanish: {{ID: "HelloWorld", Other: "other"}},
},
acceptLangs: []string{"es"},
conf: &LocalizeConfig{MessageID: "HelloWorld"},
expectedLocalized: "other",
},
{
name: "accept not default language, other message in bundle, default message",
defaultLanguage: language.English,
messages: map[language.Tag][]*Message{
language.English: {{ID: "HelloWorld", Other: "bundle other"}},
},
acceptLangs: []string{"es"},
conf: &LocalizeConfig{
DefaultMessage: &Message{ID: "HelloWorld", Other: "default other"},
},
expectedLocalized: "bundle other",
},
{
name: "accept not default language, message in bundle, default message",
defaultLanguage: language.English,
messages: map[language.Tag][]*Message{
language.Spanish: {{ID: "HelloWorld", Other: "bundle other"}},
},
acceptLangs: []string{"es"},
conf: &LocalizeConfig{
DefaultMessage: &Message{ID: "HelloWorld", Other: "default other"},
},
expectedLocalized: "bundle other",
},
{
name: "accept default language, default message",
defaultLanguage: language.English,
acceptLangs: []string{"en"},
conf: &LocalizeConfig{
DefaultMessage: &Message{ID: "HelloWorld", Other: "default other"},
},
expectedLocalized: "default other",
},
{
name: "accept not default language, default message",
defaultLanguage: language.English,
acceptLangs: []string{"es"},
conf: &LocalizeConfig{
DefaultMessage: &Message{ID: "HelloWorld", Other: "default other"},
},
expectedLocalized: "default other",
},
{
name: "fallback to non-default less specific language",
defaultLanguage: language.English,
messages: map[language.Tag][]*Message{
language.Spanish: {{ID: "HelloWorld", Other: "bundle other"}},
},
acceptLangs: []string{"es-ES"},
conf: &LocalizeConfig{
DefaultMessage: &Message{ID: "HelloWorld", Other: "default other"},
},
expectedLocalized: "bundle other",
},
{
name: "fallback to non-default more specific language",
defaultLanguage: language.English,
messages: map[language.Tag][]*Message{
language.EuropeanSpanish: {{ID: "HelloWorld", Other: "bundle other"}},
},
acceptLangs: []string{"es"},
conf: &LocalizeConfig{
DefaultMessage: &Message{ID: "HelloWorld", Other: "default other"},
},
expectedLocalized: "bundle other",
},
{
name: "plural count one, bundle message",
defaultLanguage: language.English,
messages: map[language.Tag][]*Message{
language.English: {{
ID: "Cats",
One: "I have {{.PluralCount}} cat",
Other: "I have {{.PluralCount}} cats",
}},
},
acceptLangs: []string{"en"},
conf: &LocalizeConfig{
MessageID: "Cats",
PluralCount: 1,
},
expectedLocalized: "I have 1 cat",
},
{
name: "plural count other, bundle message",
defaultLanguage: language.English,
messages: map[language.Tag][]*Message{
language.English: {{
ID: "Cats",
One: "I have {{.PluralCount}} cat",
Other: "I have {{.PluralCount}} cats",
}},
},
acceptLangs: []string{"en"},
conf: &LocalizeConfig{
MessageID: "Cats",
PluralCount: 2,
},
expectedLocalized: "I have 2 cats",
},
{
name: "plural count float, bundle message",
defaultLanguage: language.English,
messages: map[language.Tag][]*Message{
language.English: {{
ID: "Cats",
One: "I have {{.PluralCount}} cat",
Other: "I have {{.PluralCount}} cats",
}},
},
acceptLangs: []string{"en"},
conf: &LocalizeConfig{
MessageID: "Cats",
PluralCount: "2.5",
},
expectedLocalized: "I have 2.5 cats",
},
{
name: "plural count one, default message",
defaultLanguage: language.English,
acceptLangs: []string{"en"},
conf: &LocalizeConfig{
PluralCount: 1,
DefaultMessage: &Message{
ID: "Cats",
One: "I have {{.PluralCount}} cat",
Other: "I have {{.PluralCount}} cats",
},
},
expectedLocalized: "I have 1 cat",
},
{
name: "plural count missing one, default message",
defaultLanguage: language.English,
acceptLangs: []string{"en"},
conf: &LocalizeConfig{
PluralCount: 1,
DefaultMessage: &Message{
ID: "Cats",
Other: "I have {{.PluralCount}} cats",
},
},
expectedLocalized: "I have 1 cats",
expectedErr: pluralFormNotFoundError{messageID: "Cats", pluralForm: plural.One},
},
{
name: "plural count missing other, default message",
defaultLanguage: language.English,
acceptLangs: []string{"en"},
conf: &LocalizeConfig{
PluralCount: 2,
DefaultMessage: &Message{
ID: "Cats",
One: "I have {{.PluralCount}} cat",
},
},
expectedLocalized: "",
expectedErr: pluralFormNotFoundError{messageID: "Cats", pluralForm: plural.Other},
},
{
name: "plural count other, default message",
defaultLanguage: language.English,
acceptLangs: []string{"en"},
conf: &LocalizeConfig{
PluralCount: 2,
DefaultMessage: &Message{
ID: "Cats",
One: "I have {{.PluralCount}} cat",
Other: "I have {{.PluralCount}} cats",
},
},
expectedLocalized: "I have 2 cats",
},
{
name: "plural count float, default message",
defaultLanguage: language.English,
acceptLangs: []string{"en"},
conf: &LocalizeConfig{
PluralCount: "2.5",
DefaultMessage: &Message{
ID: "Cats",
One: "I have {{.PluralCount}} cat",
Other: "I have {{.PluralCount}} cats",
},
},
expectedLocalized: "I have 2.5 cats",
},
{
name: "template data, bundle message",
defaultLanguage: language.English,
messages: map[language.Tag][]*Message{
language.English: {{
ID: "HelloPerson",
Other: "Hello {{.Person}}",
}},
},
acceptLangs: []string{"en"},
conf: &LocalizeConfig{
MessageID: "HelloPerson",
TemplateData: map[string]string{
"Person": "Nick",
},
},
expectedLocalized: "Hello Nick",
},
{
name: "template data, default message",
defaultLanguage: language.English,
acceptLangs: []string{"en"},
conf: &LocalizeConfig{
DefaultMessage: &Message{
ID: "HelloPerson",
Other: "Hello {{.Person}}",
},
TemplateData: map[string]string{
"Person": "Nick",
},
},
expectedLocalized: "Hello Nick",
},
{
name: "identity parser, bundle message",
defaultLanguage: language.English,
messages: map[language.Tag][]*Message{
language.English: {{
ID: "HelloPerson",
Other: "Hello {{.Person}}",
}},
},
acceptLangs: []string{"en"},
conf: &LocalizeConfig{
MessageID: "HelloPerson",
TemplateData: map[string]string{
"Person": "Nick",
},
TemplateParser: template.IdentityParser{},
},
expectedLocalized: "Hello {{.Person}}",
},
{
name: "identity parser, default message",
defaultLanguage: language.English,
acceptLangs: []string{"en"},
conf: &LocalizeConfig{
DefaultMessage: &Message{
ID: "HelloPerson",
Other: "Hello {{.Person}}",
},
TemplateData: map[string]string{
"Person": "Nick",
},
TemplateParser: template.IdentityParser{},
},
expectedLocalized: "Hello {{.Person}}",
},
{
name: "custom funcs, default message",
defaultLanguage: language.English,
acceptLangs: []string{"en"},
conf: &LocalizeConfig{
DefaultMessage: &Message{
ID: "HelloWorld",
Other: "{{HelloWorldFunc}}",
},
Funcs: map[string]any{
"HelloWorldFunc": func() string { return "Hello World" },
},
},
expectedLocalized: "Hello World",
},
{
name: "template data, custom delims, bundle message",
defaultLanguage: language.English,
messages: map[language.Tag][]*Message{
language.English: {{
ID: "HelloPerson",
Other: "Hello <<.Person>>",
LeftDelim: "<<",
RightDelim: ">>",
}},
},
acceptLangs: []string{"en"},
conf: &LocalizeConfig{
MessageID: "HelloPerson",
TemplateData: map[string]string{
"Person": "Nick",
},
},
expectedLocalized: "Hello Nick",
},
{
name: "template data, custom delims, default message",
defaultLanguage: language.English,
acceptLangs: []string{"en"},
conf: &LocalizeConfig{
DefaultMessage: &Message{
ID: "HelloPerson",
Other: "Hello <<.Person>>",
LeftDelim: "<<",
RightDelim: ">>",
},
TemplateData: map[string]string{
"Person": "Nick",
},
},
expectedLocalized: "Hello Nick",
},
{
name: "template data, plural count one, bundle message",
defaultLanguage: language.English,
messages: map[language.Tag][]*Message{
language.English: {{
ID: "PersonCats",
One: "{{.Person}} has {{.Count}} cat",
Other: "{{.Person}} has {{.Count}} cats",
}},
},
acceptLangs: []string{"en"},
conf: &LocalizeConfig{
MessageID: "PersonCats",
TemplateData: map[string]interface{}{
"Person": "Nick",
"Count": 1,
},
PluralCount: 1,
},
expectedLocalized: "Nick has 1 cat",
},
{
name: "template data, plural count other, bundle message",
defaultLanguage: language.English,
messages: map[language.Tag][]*Message{
language.English: {{
ID: "PersonCats",
One: "{{.Person}} has {{.Count}} cat",
Other: "{{.Person}} has {{.Count}} cats",
}},
},
acceptLangs: []string{"en"},
conf: &LocalizeConfig{
MessageID: "PersonCats",
TemplateData: map[string]interface{}{
"Person": "Nick",
"Count": 2,
},
PluralCount: 2,
},
expectedLocalized: "Nick has 2 cats",
},
{
name: "template data, plural count float, bundle message",
defaultLanguage: language.English,
messages: map[language.Tag][]*Message{
language.English: {{
ID: "PersonCats",
One: "{{.Person}} has {{.Count}} cat",
Other: "{{.Person}} has {{.Count}} cats",
}},
},
acceptLangs: []string{"en"},
conf: &LocalizeConfig{
MessageID: "PersonCats",
TemplateData: map[string]interface{}{
"Person": "Nick",
"Count": "2.5",
},
PluralCount: "2.5",
},
expectedLocalized: "Nick has 2.5 cats",
},
{
name: "template data, plural count one, default message",
defaultLanguage: language.English,
acceptLangs: []string{"en"},
conf: &LocalizeConfig{
DefaultMessage: &Message{
ID: "PersonCats",
One: "{{.Person}} has {{.Count}} cat",
Other: "{{.Person}} has {{.Count}} cats",
},
TemplateData: map[string]interface{}{
"Person": "Nick",
"Count": 1,
},
PluralCount: 1,
},
expectedLocalized: "Nick has 1 cat",
},
{
name: "template data, plural count other, default message",
defaultLanguage: language.English,
acceptLangs: []string{"en"},
conf: &LocalizeConfig{
DefaultMessage: &Message{
ID: "PersonCats",
One: "{{.Person}} has {{.Count}} cat",
Other: "{{.Person}} has {{.Count}} cats",
},
TemplateData: map[string]interface{}{
"Person": "Nick",
"Count": 2,
},
PluralCount: 2,
},
expectedLocalized: "Nick has 2 cats",
},
{
name: "template data, plural count float, default message",
defaultLanguage: language.English,
acceptLangs: []string{"en"},
conf: &LocalizeConfig{
DefaultMessage: &Message{
ID: "PersonCats",
One: "{{.Person}} has {{.Count}} cat",
Other: "{{.Person}} has {{.Count}} cats",
},
TemplateData: map[string]interface{}{
"Person": "Nick",
"Count": "2.5",
},
PluralCount: "2.5",
},
expectedLocalized: "Nick has 2.5 cats",
},
{
name: "no fallback",
defaultLanguage: language.Spanish,
messages: map[language.Tag][]*Message{
language.English: {{
ID: "Hello",
Other: "Hello!",
}},
language.AmericanEnglish: {{
ID: "Goodbye",
Other: "Goodbye!",
}},
},
acceptLangs: []string{"en-US"},
conf: &LocalizeConfig{
MessageID: "Hello",
},
expectedErr: &MessageNotFoundErr{Tag: language.AmericanEnglish, MessageID: "Hello"},
},
{
name: "fallback default message",
defaultLanguage: language.Spanish,
messages: map[language.Tag][]*Message{
language.English: {{
ID: "Goodbye",
Other: "Goodbye!",
}},
language.AmericanEnglish: {{
ID: "Goodbye",
Other: "Goodbye!",
}},
},
acceptLangs: []string{"en-US"},
conf: &LocalizeConfig{
DefaultMessage: &Message{
ID: "Hello",
Other: "Hola!",
},
},
expectedLocalized: "Hola!",
expectedErr: &MessageNotFoundErr{Tag: language.AmericanEnglish, MessageID: "Hello"},
},
{
name: "no fallback default message",
defaultLanguage: language.Spanish,
messages: map[language.Tag][]*Message{
language.English: {{
ID: "Goodbye",
Other: "Goodbye!",
}},
language.AmericanEnglish: {{
ID: "Goodbye",
Other: "Goodbye!",
}},
},
acceptLangs: []string{"en-US"},
conf: &LocalizeConfig{
MessageID: "Hello",
},
expectedErr: &MessageNotFoundErr{Tag: language.AmericanEnglish, MessageID: "Hello"},
},
{
name: "empty default message",
defaultLanguage: language.English,
acceptLangs: []string{"en"},
conf: &LocalizeConfig{
DefaultMessage: &Message{},
},
expectedErr: &MessageNotFoundErr{Tag: language.English, MessageID: ""},
},
{
name: "empty default message with id",
defaultLanguage: language.English,
acceptLangs: []string{"en"},
conf: &LocalizeConfig{
DefaultMessage: &Message{
ID: "Hello",
},
},
expectedErr: &MessageNotFoundErr{Tag: language.English, MessageID: "Hello"},
},
{
name: "use option missingkey=error with missing key",
defaultLanguage: language.English,
messages: map[language.Tag][]*Message{
language.English: {{ID: "Foo", Other: "Foo {{.bar}}"}},
},
acceptLangs: []string{"en"},
conf: &LocalizeConfig{
MessageID: "Foo",
TemplateData: map[string]string{},
TemplateParser: &template.TextParser{Option: "missingkey=error"},
},
expectedErr: gotmpl.ExecError{Name: "", Err: errors.New(`template: :1:6: executing "" at <.bar>: map has no entry for key "bar"`)},
},
{
name: "use option missingkey=default with missing key",
defaultLanguage: language.English,
messages: map[language.Tag][]*Message{
language.English: {{ID: "Foo", Other: "Foo {{.bar}}"}},
},
acceptLangs: []string{"en"},
conf: &LocalizeConfig{
MessageID: "Foo",
TemplateData: map[string]string{},
TemplateParser: &template.TextParser{Option: "missingkey=default"},
},
expectedLocalized: "Foo ",
},
}
}
func TestLocalizer_Localize(t *testing.T) {
for _, test := range localizerTests() {
t.Run(test.name, func(t *testing.T) {
bundle := NewBundle(test.defaultLanguage)
for tag, messages := range test.messages {
if err := bundle.AddMessages(tag, messages...); err != nil {
t.Fatal(err)
}
}
check := func(localized string, err error) {
t.Helper()
if !reflect.DeepEqual(err, test.expectedErr) {
t.Errorf("\nexpected error: %#v\n got error: %#v", test.expectedErr, err)
}
if localized != test.expectedLocalized {
t.Errorf("expected localized string %q; got %q", test.expectedLocalized, localized)
}
}
localizer := NewLocalizer(bundle, test.acceptLangs...)
check(localizer.Localize(test.conf))
if test.conf.DefaultMessage != nil && reflect.DeepEqual(test.conf, &LocalizeConfig{DefaultMessage: test.conf.DefaultMessage}) {
check(localizer.LocalizeMessage(test.conf.DefaultMessage))
}
// if test.conf.MessageID != "" && reflect.DeepEqual(test.conf, &LocalizeConfig{MessageID: test.conf.MessageID}) {
// check(localizer.LocalizeMessageID(test.conf.MessageID))
// }
})
}
}
func BenchmarkLocalizer_Localize(b *testing.B) {
for _, test := range localizerTests() {
b.Run(test.name, func(b *testing.B) {
bundle := NewBundle(test.defaultLanguage)
for tag, messages := range test.messages {
if err := bundle.AddMessages(tag, messages...); err != nil {
b.Fatal(err)
}
}
localizer := NewLocalizer(bundle, test.acceptLangs...)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = localizer.Localize(test.conf)
}
})
}
}
func TestMessageNotFoundError(t *testing.T) {
actual := (&MessageNotFoundErr{Tag: language.AmericanEnglish, MessageID: "hello"}).Error()
expected := `message "hello" not found in language "en-US"`
if actual != expected {
t.Fatalf("expected %q; got %q", expected, actual)
}
}
func TestMessageIDMismatchError(t *testing.T) {
actual := (&messageIDMismatchErr{messageID: "hello", defaultMessageID: "world"}).Error()
expected := `message id "hello" does not match default message id "world"`
if actual != expected {
t.Fatalf("expected %q; got %q", expected, actual)
}
}
func TestInvalidPluralCountError(t *testing.T) {
actual := (&invalidPluralCountErr{messageID: "hello", pluralCount: "blah", err: fmt.Errorf("error")}).Error()
expected := `invalid plural count "blah" for message id "hello": error`
if actual != expected {
t.Fatalf("expected %q; got %q", expected, actual)
}
}
func TestMustLocalize(t *testing.T) {
defer func() {
if recover() == nil {
t.Fatalf("MustLocalize did not panic")
}
}()
bundle := NewBundle(language.English)
localizer := NewLocalizer(bundle)
localizer.MustLocalize(&LocalizeConfig{
MessageID: "hello",
})
}
func TestMustLocalizeMessage(t *testing.T) {
defer func() {
if recover() == nil {
t.Fatalf("MustLocalizeMessage did not panic")
}
}()
bundle := NewBundle(language.English)
localizer := NewLocalizer(bundle)
localizer.MustLocalizeMessage(&Message{})
}
go-i18n-2.6.0/i18n/message.go 0000664 0000000 0000000 00000014163 14774252036 0015452 0 ustar 00root root 0000000 0000000 package i18n
import (
"fmt"
"sort"
"strings"
)
// Message is a string that can be localized.
type Message struct {
// ID uniquely identifies the message.
ID string
// Hash uniquely identifies the content of the message
// that this message was translated from.
Hash string
// Description describes the message to give additional
// context to translators that may be relevant for translation.
Description string
// LeftDelim is the left Go template delimiter.
LeftDelim string
// RightDelim is the right Go template delimiter.
RightDelim string
// Zero is the content of the message for the CLDR plural form "zero".
Zero string
// One is the content of the message for the CLDR plural form "one".
One string
// Two is the content of the message for the CLDR plural form "two".
Two string
// Few is the content of the message for the CLDR plural form "few".
Few string
// Many is the content of the message for the CLDR plural form "many".
Many string
// Other is the content of the message for the CLDR plural form "other".
Other string
}
// NewMessage parses data and returns a new message.
func NewMessage(data interface{}) (*Message, error) {
m := &Message{}
if err := m.unmarshalInterface(data); err != nil {
return nil, err
}
return m, nil
}
// MustNewMessage is similar to NewMessage except it panics if an error happens.
func MustNewMessage(data interface{}) *Message {
m, err := NewMessage(data)
if err != nil {
panic(err)
}
return m
}
// unmarshalInterface unmarshals a message from data.
func (m *Message) unmarshalInterface(v interface{}) error {
strdata, err := stringMap(v)
if err != nil {
return err
}
for k, v := range strdata {
switch strings.ToLower(k) {
case "id":
m.ID = v
case "description":
m.Description = v
case "hash":
m.Hash = v
case "leftdelim":
m.LeftDelim = v
case "rightdelim":
m.RightDelim = v
case "zero":
m.Zero = v
case "one":
m.One = v
case "two":
m.Two = v
case "few":
m.Few = v
case "many":
m.Many = v
case "other":
m.Other = v
}
}
return nil
}
type keyTypeErr struct {
key interface{}
}
func (err *keyTypeErr) Error() string {
return fmt.Sprintf("expected key to be a string but got %#v", err.key)
}
type valueTypeErr struct {
value interface{}
}
func (err *valueTypeErr) Error() string {
return fmt.Sprintf("unsupported type %#v", err.value)
}
func stringMap(v interface{}) (map[string]string, error) {
switch value := v.(type) {
case string:
return map[string]string{
"other": value,
}, nil
case map[string]string:
return value, nil
case map[string]interface{}:
strdata := make(map[string]string, len(value))
for k, v := range value {
err := stringSubmap(k, v, strdata)
if err != nil {
return nil, err
}
}
return strdata, nil
case map[interface{}]interface{}:
strdata := make(map[string]string, len(value))
for k, v := range value {
kstr, ok := k.(string)
if !ok {
return nil, &keyTypeErr{key: k}
}
err := stringSubmap(kstr, v, strdata)
if err != nil {
return nil, err
}
}
return strdata, nil
default:
return nil, &valueTypeErr{value: value}
}
}
func stringSubmap(k string, v interface{}, strdata map[string]string) error {
if k == "translation" {
switch vt := v.(type) {
case string:
strdata["other"] = vt
default:
v1Message, err := stringMap(v)
if err != nil {
return err
}
for kk, vv := range v1Message {
strdata[kk] = vv
}
}
return nil
}
switch vt := v.(type) {
case string:
strdata[k] = vt
return nil
case nil:
return nil
default:
return fmt.Errorf("expected value for key %q be a string but got %#v", k, v)
}
}
var reservedKeys = map[string]struct{}{
"id": {},
"description": {},
"hash": {},
"leftdelim": {},
"rightdelim": {},
"zero": {},
"one": {},
"two": {},
"few": {},
"many": {},
"other": {},
"translation": {},
}
func isReserved(key string, val any) bool {
lk := strings.ToLower(key)
if _, ok := reservedKeys[lk]; ok {
if key == "translation" {
return true
}
if _, ok := val.(string); ok {
return true
}
}
return false
}
// isMessage returns true if v contains only message keys and false if it contains no message keys.
// It returns an error if v contains both message and non-message keys.
// - {"message": {"description": "world"}} is a message
// - {"error": {"description": "world", "foo": "bar"}} is an error
// - {"notmessage": {"description": {"hello": "world"}}} is not a message
// - {"notmessage": {"foo": "bar"}} is not a message
func isMessage(v interface{}) (bool, error) {
switch data := v.(type) {
case nil, string:
return true, nil
case map[string]interface{}:
reservedKeys := make([]string, 0, len(reservedKeys))
unreservedKeys := make([]string, 0, len(data))
for k, v := range data {
if isReserved(k, v) {
reservedKeys = append(reservedKeys, k)
} else {
unreservedKeys = append(unreservedKeys, k)
}
}
hasReservedKeys := len(reservedKeys) > 0
if hasReservedKeys && len(unreservedKeys) > 0 {
return false, &mixedKeysError{
reservedKeys: reservedKeys,
unreservedKeys: unreservedKeys,
}
}
return hasReservedKeys, nil
case map[interface{}]interface{}:
reservedKeys := make([]string, 0, len(reservedKeys))
unreservedKeys := make([]string, 0, len(data))
for key, v := range data {
k, ok := key.(string)
if !ok {
unreservedKeys = append(unreservedKeys, fmt.Sprintf("%+v", key))
} else if isReserved(k, v) {
reservedKeys = append(reservedKeys, k)
} else {
unreservedKeys = append(unreservedKeys, k)
}
}
hasReservedKeys := len(reservedKeys) > 0
if hasReservedKeys && len(unreservedKeys) > 0 {
return false, &mixedKeysError{
reservedKeys: reservedKeys,
unreservedKeys: unreservedKeys,
}
}
return hasReservedKeys, nil
}
return false, nil
}
type mixedKeysError struct {
reservedKeys []string
unreservedKeys []string
}
func (e *mixedKeysError) Error() string {
sort.Strings(e.reservedKeys)
sort.Strings(e.unreservedKeys)
return fmt.Sprintf("reserved keys %v mixed with unreserved keys %v", e.reservedKeys, e.unreservedKeys)
}
go-i18n-2.6.0/i18n/message_template.go 0000664 0000000 0000000 00000005174 14774252036 0017347 0 ustar 00root root 0000000 0000000 package i18n
import (
"fmt"
texttemplate "text/template"
"github.com/nicksnyder/go-i18n/v2/i18n/template"
"github.com/nicksnyder/go-i18n/v2/internal"
"github.com/nicksnyder/go-i18n/v2/internal/plural"
)
// MessageTemplate is an executable template for a message.
type MessageTemplate struct {
*Message
PluralTemplates map[plural.Form]*internal.Template
}
// NewMessageTemplate returns a new message template.
func NewMessageTemplate(m *Message) *MessageTemplate {
pluralTemplates := map[plural.Form]*internal.Template{}
setPluralTemplate(pluralTemplates, plural.Zero, m.Zero, m.LeftDelim, m.RightDelim)
setPluralTemplate(pluralTemplates, plural.One, m.One, m.LeftDelim, m.RightDelim)
setPluralTemplate(pluralTemplates, plural.Two, m.Two, m.LeftDelim, m.RightDelim)
setPluralTemplate(pluralTemplates, plural.Few, m.Few, m.LeftDelim, m.RightDelim)
setPluralTemplate(pluralTemplates, plural.Many, m.Many, m.LeftDelim, m.RightDelim)
setPluralTemplate(pluralTemplates, plural.Other, m.Other, m.LeftDelim, m.RightDelim)
if len(pluralTemplates) == 0 {
return nil
}
return &MessageTemplate{
Message: m,
PluralTemplates: pluralTemplates,
}
}
func setPluralTemplate(pluralTemplates map[plural.Form]*internal.Template, pluralForm plural.Form, src, leftDelim, rightDelim string) {
if src != "" {
pluralTemplates[pluralForm] = &internal.Template{
Src: src,
LeftDelim: leftDelim,
RightDelim: rightDelim,
}
}
}
type pluralFormNotFoundError struct {
pluralForm plural.Form
messageID string
}
func (e pluralFormNotFoundError) Error() string {
return fmt.Sprintf("message %q has no plural form %q", e.messageID, e.pluralForm)
}
// Execute executes the template for the plural form and template data.
// Deprecated: This method is no longer used internally by go-i18n and it probably should not have been exported to
// begin with. Its replacement is not exported. If you depend on this method for some reason and/or have
// a use case for exporting execute, please file an issue.
func (mt *MessageTemplate) Execute(pluralForm plural.Form, data interface{}, funcs texttemplate.FuncMap) (string, error) {
t := mt.PluralTemplates[pluralForm]
if t == nil {
return "", pluralFormNotFoundError{
pluralForm: pluralForm,
messageID: mt.ID,
}
}
parser := &template.TextParser{
Funcs: funcs,
}
return t.Execute(parser, data)
}
func (mt *MessageTemplate) execute(pluralForm plural.Form, data interface{}, parser template.Parser) (string, error) {
t := mt.PluralTemplates[pluralForm]
if t == nil {
return "", pluralFormNotFoundError{
pluralForm: pluralForm,
messageID: mt.ID,
}
}
return t.Execute(parser, data)
}
go-i18n-2.6.0/i18n/message_template_test.go 0000664 0000000 0000000 00000001561 14774252036 0020402 0 ustar 00root root 0000000 0000000 package i18n
import (
"reflect"
"testing"
"github.com/nicksnyder/go-i18n/v2/internal/plural"
)
func TestMessageTemplate(t *testing.T) {
mt := NewMessageTemplate(&Message{ID: "HelloWorld", Other: "Hello World"})
if mt.PluralTemplates[plural.Other].Src != "Hello World" {
t.Fatal(mt.PluralTemplates)
}
}
func TestNilMessageTemplate(t *testing.T) {
if mt := NewMessageTemplate(&Message{ID: "HelloWorld"}); mt != nil {
t.Fatal(mt)
}
}
func TestMessageTemplatePluralFormMissing(t *testing.T) {
mt := NewMessageTemplate(&Message{ID: "HelloWorld", Other: "Hello World"})
s, err := mt.Execute(plural.Few, nil, nil)
if s != "" {
t.Errorf("expected %q; got %q", "", s)
}
expectedErr := pluralFormNotFoundError{pluralForm: plural.Few, messageID: "HelloWorld"}
if !reflect.DeepEqual(err, expectedErr) {
t.Errorf("expected error %#v; got %#v", expectedErr, err)
}
}
go-i18n-2.6.0/i18n/message_test.go 0000664 0000000 0000000 00000006743 14774252036 0016516 0 ustar 00root root 0000000 0000000 package i18n
import (
"reflect"
"testing"
)
func TestNewMessage(t *testing.T) {
tests := []struct {
name string
data interface{}
message *Message
err error
}{
{
name: "string",
data: "other",
message: &Message{
Other: "other",
},
},
{
name: "nil value",
data: map[interface{}]interface{}{
"ID": "id",
"Zero": nil,
"Other": "other",
},
message: &Message{
ID: "id",
Other: "other",
},
},
{
name: "map[string]string",
data: map[string]string{
"ID": "id",
"Hash": "hash",
"Description": "description",
"LeftDelim": "leftdelim",
"RightDelim": "rightdelim",
"Zero": "zero",
"One": "one",
"Two": "two",
"Few": "few",
"Many": "many",
"Other": "other",
},
message: &Message{
ID: "id",
Hash: "hash",
Description: "description",
LeftDelim: "leftdelim",
RightDelim: "rightdelim",
Zero: "zero",
One: "one",
Two: "two",
Few: "few",
Many: "many",
Other: "other",
},
},
{
name: "map[string]interface{}",
data: map[string]interface{}{
"ID": "id",
"Hash": "hash",
"Description": "description",
"LeftDelim": "leftdelim",
"RightDelim": "rightdelim",
"Zero": "zero",
"One": "one",
"Two": "two",
"Few": "few",
"Many": "many",
"Other": "other",
},
message: &Message{
ID: "id",
Hash: "hash",
Description: "description",
LeftDelim: "leftdelim",
RightDelim: "rightdelim",
Zero: "zero",
One: "one",
Two: "two",
Few: "few",
Many: "many",
Other: "other",
},
},
{
name: "map[interface{}]interface{}",
data: map[interface{}]interface{}{
"ID": "id",
"Hash": "hash",
"Description": "description",
"LeftDelim": "leftdelim",
"RightDelim": "rightdelim",
"Zero": "zero",
"One": "one",
"Two": "two",
"Few": "few",
"Many": "many",
"Other": "other",
},
message: &Message{
ID: "id",
Hash: "hash",
Description: "description",
LeftDelim: "leftdelim",
RightDelim: "rightdelim",
Zero: "zero",
One: "one",
Two: "two",
Few: "few",
Many: "many",
Other: "other",
},
},
{
name: "map[int]int",
data: map[interface{}]interface{}{
1: 2,
},
err: &keyTypeErr{key: 1},
},
{
name: "int",
data: 1,
err: &valueTypeErr{value: 1},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual, err := NewMessage(test.data)
if !reflect.DeepEqual(err, test.err) {
t.Fatalf("expected %#v; got %#v", test.err, err)
}
if !reflect.DeepEqual(actual, test.message) {
t.Fatalf("\nexpected\n%#v\ngot\n%#v", test.message, actual)
}
})
}
}
func TestKeyTypeErr(t *testing.T) {
expected := "expected key to be a string but got 1"
if actual := (&keyTypeErr{key: 1}).Error(); actual != expected {
t.Fatalf("expected %#v; got %#v", expected, actual)
}
}
func TestValueTypeErr(t *testing.T) {
expected := "unsupported type 1"
if actual := (&valueTypeErr{value: 1}).Error(); actual != expected {
t.Fatalf("expected %#v; got %#v", expected, actual)
}
}
go-i18n-2.6.0/i18n/parse.go 0000664 0000000 0000000 00000010176 14774252036 0015140 0 ustar 00root root 0000000 0000000 package i18n
import (
"encoding/json"
"errors"
"fmt"
"os"
"golang.org/x/text/language"
)
// MessageFile represents a parsed message file.
type MessageFile struct {
Path string
Tag language.Tag
Format string
Messages []*Message
}
// ParseMessageFileBytes returns the messages parsed from file.
func ParseMessageFileBytes(buf []byte, path string, unmarshalFuncs map[string]UnmarshalFunc) (*MessageFile, error) {
lang, format := parsePath(path)
tag := language.Make(lang)
messageFile := &MessageFile{
Path: path,
Tag: tag,
Format: format,
}
if len(buf) == 0 {
return messageFile, nil
}
unmarshalFunc := unmarshalFuncs[messageFile.Format]
if unmarshalFunc == nil {
if messageFile.Format == "json" {
unmarshalFunc = json.Unmarshal
} else {
return nil, fmt.Errorf("no unmarshaler registered for %s", messageFile.Format)
}
}
var err error
var raw interface{}
if err = unmarshalFunc(buf, &raw); err != nil {
return nil, err
}
m, err := isMessage(raw)
if err != nil {
return nil, err
}
if messageFile.Messages, err = recGetMessages(raw, m, true); err != nil {
return nil, err
}
return messageFile, nil
}
const nestedSeparator = "."
var errInvalidTranslationFile = errors.New("invalid translation file, expected key-values, got a single value")
// recGetMessages looks for translation messages inside "raw" parameter,
// scanning nested maps using recursion.
func recGetMessages(raw interface{}, isMapMessage, isInitialCall bool) ([]*Message, error) {
var messages []*Message
var err error
switch data := raw.(type) {
case string:
if isInitialCall {
return nil, errInvalidTranslationFile
}
m, err := NewMessage(data)
return []*Message{m}, err
case map[string]interface{}:
if isMapMessage {
m, err := NewMessage(data)
return []*Message{m}, err
}
messages = make([]*Message, 0, len(data))
for id, data := range data {
// recursively scan map items
messages, err = addChildMessages(id, data, messages)
if err != nil {
return nil, err
}
}
case map[interface{}]interface{}:
if isMapMessage {
m, err := NewMessage(data)
return []*Message{m}, err
}
messages = make([]*Message, 0, len(data))
for id, data := range data {
strid, ok := id.(string)
if !ok {
return nil, fmt.Errorf("expected key to be string but got %#v", id)
}
// recursively scan map items
messages, err = addChildMessages(strid, data, messages)
if err != nil {
return nil, err
}
}
case []interface{}:
// Backward compatibility for v1 file format.
messages = make([]*Message, 0, len(data))
for _, data := range data {
// recursively scan slice items
m, err := isMessage(data)
if err != nil {
return nil, err
}
childMessages, err := recGetMessages(data, m, false)
if err != nil {
return nil, err
}
messages = append(messages, childMessages...)
}
case nil:
if isInitialCall {
return nil, errInvalidTranslationFile
}
m, err := NewMessage("")
return []*Message{m}, err
default:
return nil, fmt.Errorf("unsupported file format %T", raw)
}
return messages, nil
}
func addChildMessages(id string, data interface{}, messages []*Message) ([]*Message, error) {
isChildMessage, err := isMessage(data)
if err != nil {
return nil, err
}
childMessages, err := recGetMessages(data, isChildMessage, false)
if err != nil {
return nil, err
}
for _, m := range childMessages {
if isChildMessage {
if m.ID == "" {
m.ID = id // start with innermost key
}
} else {
m.ID = id + nestedSeparator + m.ID // update ID with each nested key on the way
}
messages = append(messages, m)
}
return messages, nil
}
func parsePath(path string) (langTag, format string) {
formatStartIdx := -1
for i := len(path) - 1; i >= 0; i-- {
c := path[i]
if os.IsPathSeparator(c) {
if formatStartIdx != -1 {
langTag = path[i+1 : formatStartIdx]
}
return
}
if path[i] == '.' {
if formatStartIdx != -1 {
langTag = path[i+1 : formatStartIdx]
return
}
if formatStartIdx == -1 {
format = path[i+1:]
formatStartIdx = i
}
}
}
if formatStartIdx != -1 {
langTag = path[:formatStartIdx]
}
return
}
go-i18n-2.6.0/i18n/parse_test.go 0000664 0000000 0000000 00000015513 14774252036 0016177 0 ustar 00root root 0000000 0000000 package i18n
import (
"errors"
"reflect"
"sort"
"testing"
"golang.org/x/text/language"
yaml "gopkg.in/yaml.v3"
)
func TestParseMessageFileBytes(t *testing.T) {
testCases := []struct {
name string
file string
path string
unmarshalFuncs map[string]UnmarshalFunc
messageFile *MessageFile
err error
}{
{
name: "basic test",
file: `{"hello": "world"}`,
path: "en.json",
messageFile: &MessageFile{
Path: "en.json",
Tag: language.English,
Format: "json",
Messages: []*Message{{
ID: "hello",
Other: "world",
}},
},
},
{
name: "nested with reserved key",
file: `{"nested": {"description": {"other": "world"}}}`,
path: "en.json",
messageFile: &MessageFile{
Path: "en.json",
Tag: language.English,
Format: "json",
Messages: []*Message{{
ID: "nested.description",
Other: "world",
}},
},
},
{
name: "basic test reserved key top level",
file: `{"other": "world", "foo": "bar"}`,
path: "en.json",
err: &mixedKeysError{
reservedKeys: []string{"other"},
unreservedKeys: []string{"foo"},
},
},
{
name: "basic test with dot separator in key",
file: `{"prepended.hello": "world"}`,
path: "en.json",
messageFile: &MessageFile{
Path: "en.json",
Tag: language.English,
Format: "json",
Messages: []*Message{{
ID: "prepended.hello",
Other: "world",
}},
},
},
{
name: "invalid test (no key)",
file: `"hello"`,
path: "en.json",
err: errInvalidTranslationFile,
},
{
name: "nested test",
file: `{"nested": {"hello": "world"}}`,
path: "en.json",
messageFile: &MessageFile{
Path: "en.json",
Tag: language.English,
Format: "json",
Messages: []*Message{{
ID: "nested.hello",
Other: "world",
}},
},
},
{
name: "basic test with description",
file: `{"notnested": {"description": "world"}}`,
path: "en.json",
messageFile: &MessageFile{
Path: "en.json",
Tag: language.English,
Format: "json",
Messages: []*Message{{
ID: "notnested",
Description: "world",
}},
},
},
{
name: "basic test with id",
file: `{"key": {"id": "forced.id"}}`,
path: "en.json",
messageFile: &MessageFile{
Path: "en.json",
Tag: language.English,
Format: "json",
Messages: []*Message{{
ID: "forced.id",
}},
},
},
{
name: "basic test with description and dummy",
file: `{"notnested": {"description": "world", "dummy": "nothing"}}`,
path: "en.json",
err: &mixedKeysError{
reservedKeys: []string{"description"},
unreservedKeys: []string{"dummy"},
},
},
{
name: "deeply nested test",
file: `{"outer": {"nested": {"inner": "value"}}}`,
path: "en.json",
messageFile: &MessageFile{
Path: "en.json",
Tag: language.English,
Format: "json",
Messages: []*Message{{
ID: "outer.nested.inner",
Other: "value",
}},
},
},
{
name: "multiple nested test",
file: `{"nested": {"hello": "world", "bye": "all"}}`,
path: "en.json",
messageFile: &MessageFile{
Path: "en.json",
Tag: language.English,
Format: "json",
Messages: []*Message{{
ID: "nested.hello",
Other: "world",
}, {
ID: "nested.bye",
Other: "all",
}},
},
},
{
name: "YAML nested test",
file: `
outer:
nested:
inner: "value"`,
path: "en.yaml",
unmarshalFuncs: map[string]UnmarshalFunc{"yaml": yaml.Unmarshal},
messageFile: &MessageFile{
Path: "en.yaml",
Tag: language.English,
Format: "yaml",
Messages: []*Message{{
ID: "outer.nested.inner",
Other: "value",
}},
},
},
{
name: "YAML empty key test",
file: `
some-keys:
non-empty-key: not empty
empty-key-but-type-specified: ""
empty-key:
null-key: null`,
path: "en.yaml",
unmarshalFuncs: map[string]UnmarshalFunc{"yaml": yaml.Unmarshal},
messageFile: &MessageFile{
Path: "en.yaml",
Tag: language.English,
Format: "yaml",
Messages: []*Message{
{
ID: "some-keys.non-empty-key",
Other: "not empty",
},
{
ID: "some-keys.empty-key-but-type-specified",
},
{
ID: "some-keys.empty-key",
},
{
ID: "some-keys.null-key",
},
},
},
},
{
name: "YAML number key test",
file: `
some-keys:
hello: world
2: legit`,
path: "en.yaml",
unmarshalFuncs: map[string]UnmarshalFunc{"yaml": yaml.Unmarshal},
err: errors.New("expected key to be string but got 2"),
},
{
name: "YAML extra number key test",
file: `
some-keys:
other: world
2: legit`,
path: "en.yaml",
unmarshalFuncs: map[string]UnmarshalFunc{"yaml": yaml.Unmarshal},
err: &mixedKeysError{
reservedKeys: []string{"other"},
unreservedKeys: []string{"2"},
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
actual, err := ParseMessageFileBytes([]byte(testCase.file), testCase.path, testCase.unmarshalFuncs)
if (err == nil && testCase.err != nil) ||
(err != nil && testCase.err == nil) ||
(err != nil && testCase.err != nil && err.Error() != testCase.err.Error()) {
t.Fatalf("expected error %#v; got %#v", testCase.err, err)
}
if testCase.messageFile == nil && actual != nil || testCase.messageFile != nil && actual == nil {
t.Fatalf("expected message file %#v; got %#v", testCase.messageFile, actual)
}
if testCase.messageFile != nil {
if actual.Path != testCase.messageFile.Path {
t.Errorf("expected path %q; got %q", testCase.messageFile.Path, actual.Path)
}
if actual.Tag != testCase.messageFile.Tag {
t.Errorf("expected tag %q; got %q", testCase.messageFile.Tag, actual.Tag)
}
if actual.Format != testCase.messageFile.Format {
t.Errorf("expected format %q; got %q", testCase.messageFile.Format, actual.Format)
}
if !equalMessages(actual.Messages, testCase.messageFile.Messages) {
t.Errorf("expected %#v; got %#v", deref(testCase.messageFile.Messages), deref(actual.Messages))
}
}
})
}
}
func deref(mptrs []*Message) []Message {
messages := make([]Message, len(mptrs))
for i, m := range mptrs {
messages[i] = *m
}
return messages
}
// equalMessages compares two slices of messages, ignoring private fields and order.
// Sorts both input slices, which are therefore modified by this function.
func equalMessages(m1, m2 []*Message) bool {
if len(m1) != len(m2) {
return false
}
var less = func(m []*Message) func(int, int) bool {
return func(i, j int) bool {
return m[i].ID < m[j].ID
}
}
sort.Slice(m1, less(m1))
sort.Slice(m2, less(m2))
for i, m := range m1 {
if !reflect.DeepEqual(m, m2[i]) {
return false
}
}
return true
}
go-i18n-2.6.0/i18n/template/ 0000775 0000000 0000000 00000000000 14774252036 0015305 5 ustar 00root root 0000000 0000000 go-i18n-2.6.0/i18n/template/identity_parser.go 0000664 0000000 0000000 00000001023 14774252036 0021035 0 ustar 00root root 0000000 0000000 package template
// IdentityParser is an Parser that does no parsing and returns template string unchanged.
type IdentityParser struct{}
func (IdentityParser) Cacheable() bool {
// Caching is not necessary because Parse is cheap.
return false
}
func (IdentityParser) Parse(src, leftDelim, rightDelim string) (ParsedTemplate, error) {
return &identityParsedTemplate{src: src}, nil
}
type identityParsedTemplate struct {
src string
}
func (t *identityParsedTemplate) Execute(data any) (string, error) {
return t.src, nil
}
go-i18n-2.6.0/i18n/template/parser.go 0000664 0000000 0000000 00000001137 14774252036 0017132 0 ustar 00root root 0000000 0000000 // Package template defines a generic interface for template parsers and implementations of that interface.
package template
// Parser parses strings into executable templates.
type Parser interface {
// Parse parses src and returns a ParsedTemplate.
Parse(src, leftDelim, rightDelim string) (ParsedTemplate, error)
// Cacheable returns true if Parse returns ParsedTemplates that are always safe to cache.
Cacheable() bool
}
// ParsedTemplate is an executable template.
type ParsedTemplate interface {
// Execute applies a parsed template to the specified data.
Execute(data any) (string, error)
}
go-i18n-2.6.0/i18n/template/text_parser.go 0000664 0000000 0000000 00000002353 14774252036 0020177 0 ustar 00root root 0000000 0000000 package template
import (
"bytes"
"strings"
"text/template"
)
// TextParser is a Parser that uses text/template.
type TextParser struct {
LeftDelim string
RightDelim string
Funcs template.FuncMap
Option string
}
func (te *TextParser) Cacheable() bool {
return te.Funcs == nil
}
func (te *TextParser) Parse(src, leftDelim, rightDelim string) (ParsedTemplate, error) {
if leftDelim == "" {
leftDelim = te.LeftDelim
}
if leftDelim == "" {
leftDelim = "{{"
}
if !strings.Contains(src, leftDelim) {
// Fast path to avoid parsing a template that has no actions.
return &identityParsedTemplate{src: src}, nil
}
if rightDelim == "" {
rightDelim = te.RightDelim
}
if rightDelim == "" {
rightDelim = "}}"
}
option := "missingkey=default"
if te.Option != "" {
option = te.Option
}
tmpl, err := template.New("").Delims(leftDelim, rightDelim).Option(option).Funcs(te.Funcs).Parse(src)
if err != nil {
return nil, err
}
return &parsedTextTemplate{tmpl: tmpl}, nil
}
type parsedTextTemplate struct {
tmpl *template.Template
}
func (t *parsedTextTemplate) Execute(data any) (string, error) {
var buf bytes.Buffer
if err := t.tmpl.Execute(&buf, data); err != nil {
return "", err
}
return buf.String(), nil
}
go-i18n-2.6.0/internal/ 0000775 0000000 0000000 00000000000 14774252036 0014527 5 ustar 00root root 0000000 0000000 go-i18n-2.6.0/internal/plural/ 0000775 0000000 0000000 00000000000 14774252036 0016026 5 ustar 00root root 0000000 0000000 go-i18n-2.6.0/internal/plural/codegen/ 0000775 0000000 0000000 00000000000 14774252036 0017432 5 ustar 00root root 0000000 0000000 go-i18n-2.6.0/internal/plural/codegen/README.md 0000664 0000000 0000000 00000000353 14774252036 0020712 0 ustar 00root root 0000000 0000000 # How to upgrade CLDR data
1. Go to https://github.com/unicode-org/cldr/releases to find the latest release and download the source code.
1. Unzip and copy `common/supplemental/plurals.xml` to this directory.
1. Run `generate.sh`.
go-i18n-2.6.0/internal/plural/codegen/generate.sh 0000664 0000000 0000000 00000000303 14774252036 0021554 0 ustar 00root root 0000000 0000000 #!/bin/sh
OUT=..
go build && ./codegen -cout $OUT/rule_gen.go -tout $OUT/rule_gen_test.go && \
gofmt -w=true $OUT/rule_gen.go && \
gofmt -w=true $OUT/rule_gen_test.go && \
rm codegen
go-i18n-2.6.0/internal/plural/codegen/main.go 0000664 0000000 0000000 00000006323 14774252036 0020711 0 ustar 00root root 0000000 0000000 package main
import (
"encoding/xml"
"flag"
"fmt"
"os"
"text/template"
)
var usage = `%[1]s generates Go code to support CLDR plural rules.
Usage: %[1]s [options]
Options:
`
func main() {
flag.Usage = func() {
fmt.Fprintf(os.Stderr, usage, os.Args[0])
flag.PrintDefaults()
}
var in, cout, tout string
flag.StringVar(&in, "i", "plurals.xml", "the input XML file containing CLDR plural rules")
flag.StringVar(&cout, "cout", "", "the code output file")
flag.StringVar(&tout, "tout", "", "the test output file")
flag.BoolVar(&verbose, "v", false, "verbose output")
flag.Parse()
buf, err := os.ReadFile(in)
if err != nil {
fatalf("failed to read file: %s", err)
}
var data SupplementalData
if err := xml.Unmarshal(buf, &data); err != nil {
fatalf("failed to unmarshal xml: %s", err)
}
count := 0
for _, pg := range data.PluralGroups {
count += len(pg.SplitLocales())
}
infof("parsed %d locales", count)
if cout != "" {
file := openWritableFile(cout)
if err := codeTemplate.Execute(file, data); err != nil {
fatalf("unable to execute code template because %s", err)
} else {
infof("generated %s", cout)
}
} else {
infof("not generating code file (use -cout)")
}
if tout != "" {
file := openWritableFile(tout)
if err := testTemplate.Execute(file, data); err != nil {
fatalf("unable to execute test template because %s", err)
} else {
infof("generated %s", tout)
}
} else {
infof("not generating test file (use -tout)")
}
}
func openWritableFile(name string) *os.File {
file, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
fatalf("failed to write file %s because %s", name, err)
}
return file
}
var codeTemplate = template.Must(template.New("rule").Parse(`// This file is generated by i18n/plural/codegen/generate.sh; DO NOT EDIT
package plural
// DefaultRules returns a map of Rules generated from CLDR language data.
func DefaultRules() Rules {
rules := Rules{}
{{range .PluralGroups}}
addPluralRules(rules, {{printf "%#v" .SplitLocales}}, &Rule{
PluralForms: newPluralFormSet({{range $i, $e := .PluralRules}}{{if $i}}, {{end}}{{$e.CountTitle}}{{end}}),
PluralFormFunc: func(ops *Operands) Form { {{range .PluralRules}}{{if .GoCondition}}
// {{.Condition}}
if {{.GoCondition}} {
return {{.CountTitle}}
}{{end}}{{end}}
return Other
},
}){{end}}
return rules
}
`))
var testTemplate = template.Must(template.New("rule").Parse(`// This file is generated by i18n/plural/codegen/generate.sh; DO NOT EDIT
package plural
import "testing"
{{range .PluralGroups}}
func Test{{.Name}}(t *testing.T) {
var tests []pluralFormTest
{{range .PluralRules}}
{{if .IntegerExamples}}tests = appendIntegerTests(tests, {{.CountTitle}}, {{printf "%#v" .IntegerExamples}}){{end}}
{{if .DecimalExamples}}tests = appendDecimalTests(tests, {{.CountTitle}}, {{printf "%#v" .DecimalExamples}}){{end}}
{{end}}
locales := {{printf "%#v" .SplitLocales}}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
{{end}}
`))
func infof(format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, format+"\n", args...)
}
var verbose bool
func fatalf(format string, args ...interface{}) {
infof("fatal: "+format+"\n", args...)
os.Exit(1)
}
go-i18n-2.6.0/internal/plural/codegen/plurals.xml 0000664 0000000 0000000 00000061727 14774252036 0021653 0 ustar 00root root 0000000 0000000
@integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04
@integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~2.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
i = 0,1 @integer 0, 1 @decimal 0.0~1.5
@integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
i = 1 and v = 0 @integer 1
@integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
n = 0,1 or i = 0 and f = 1 @integer 0, 1 @decimal 0.0, 0.1, 1.0, 0.00, 0.01, 1.00, 0.000, 0.001, 1.000, 0.0000, 0.0001, 1.0000
@integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.2~0.9, 1.1~1.8, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
n = 0..1 @integer 0, 1 @decimal 0.0, 1.0, 0.00, 1.00, 0.000, 1.000, 0.0000, 1.0000
@integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
n = 0..1 or n = 11..99 @integer 0, 1, 11~24 @decimal 0.0, 1.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0, 21.0, 22.0, 23.0, 24.0
@integer 2~10, 100~106, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000
@integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
n = 1 or t != 0 and i = 0,1 @integer 1 @decimal 0.1~1.6
@integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 2.0~3.4, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
t = 0 and i % 10 = 1 and i % 100 != 11 or t % 10 = 1 and t % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.0, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …
@integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.2~0.9, 1.2~1.8, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …
@integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.2~1.0, 1.2~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
v = 0 and i = 1,2,3 or v = 0 and i % 10 != 4,6,9 or v != 0 and f % 10 != 4,6,9 @integer 0~3, 5, 7, 8, 10~13, 15, 17, 18, 20, 21, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.3, 0.5, 0.7, 0.8, 1.0~1.3, 1.5, 1.7, 1.8, 2.0, 2.1, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
@integer 4, 6, 9, 14, 16, 19, 24, 26, 104, 1004, … @decimal 0.4, 0.6, 0.9, 1.4, 1.6, 1.9, 2.4, 2.6, 10.4, 100.4, 1000.4, …
n % 10 = 0 or n % 100 = 11..19 or v = 2 and f % 100 = 11..19 @integer 0, 10~20, 30, 40, 50, 60, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
n % 10 = 1 and n % 100 != 11 or v = 2 and f % 10 = 1 and f % 100 != 11 or v != 2 and f % 10 = 1 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.0, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …
@integer 2~9, 22~29, 102, 1002, … @decimal 0.2~0.9, 1.2~1.9, 10.2, 100.2, 1000.2, …
n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000
i = 0,1 and n != 0 @integer 1 @decimal 0.1~1.6
@integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000
n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000
@integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000
n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000
@integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
i = 1 and v = 0 or i = 0 and v != 0 @integer 1 @decimal 0.0~0.9, 0.00~0.05
i = 2 and v = 0 @integer 2
@integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, … @decimal 1.0~2.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000
n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000
@integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04
n = 2..10 @integer 2~10 @decimal 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 2.00, 3.00, 4.00, 5.00, 6.00, 7.00, 8.00
@integer 11~26, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~1.9, 2.1~2.7, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
i = 1 and v = 0 @integer 1
v != 0 or n = 0 or n != 1 and n % 100 = 1..19 @integer 0, 2~16, 101, 1001, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
@integer 20~35, 100, 1000, 10000, 100000, 1000000, …
v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …
v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, … @decimal 0.2~0.4, 1.2~1.4, 2.2~2.4, 3.2~3.4, 4.2~4.4, 5.2, 10.2, 100.2, 1000.2, …
@integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.5~1.0, 1.5~2.0, 2.5~2.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
i = 0,1 @integer 0, 1 @decimal 0.0~1.5
e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5 @integer 1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, … @decimal 1.0000001c6, 1.1c6, 2.0000001c6, 2.1c6, 3.0000001c6, 3.1c6, …
@integer 2~17, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 1.0001c3, 1.1c3, 2.0001c3, 2.1c3, 3.0001c3, 3.1c3, …
i = 0..1 @integer 0, 1 @decimal 0.0~1.5
e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5 @integer 1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, … @decimal 1.0000001c6, 1.1c6, 2.0000001c6, 2.1c6, 3.0000001c6, 3.1c6, …
@integer 2~17, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 1.0001c3, 1.1c3, 2.0001c3, 2.1c3, 3.0001c3, 3.1c3, …
i = 1 and v = 0 @integer 1
e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5 @integer 1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, … @decimal 1.0000001c6, 1.1c6, 2.0000001c6, 2.1c6, 3.0000001c6, 3.1c6, …
@integer 0, 2~16, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 1.0001c3, 1.1c3, 2.0001c3, 2.1c3, 3.0001c3, 3.1c3, …
n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000
e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5 @integer 1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, … @decimal 1.0000001c6, 1.1c6, 2.0000001c6, 2.1c6, 3.0000001c6, 3.1c6, …
@integer 0, 2~16, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 1.0001c3, 1.1c3, 2.0001c3, 2.1c3, 3.0001c3, 3.1c3, …
n = 1,11 @integer 1, 11 @decimal 1.0, 11.0, 1.00, 11.00, 1.000, 11.000, 1.0000
n = 2,12 @integer 2, 12 @decimal 2.0, 12.0, 2.00, 12.00, 2.000, 12.000, 2.0000
n = 3..10,13..19 @integer 3~10, 13~19 @decimal 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 3.00
@integer 0, 20~34, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
v = 0 and i % 100 = 1 @integer 1, 101, 201, 301, 401, 501, 601, 701, 1001, …
v = 0 and i % 100 = 2 @integer 2, 102, 202, 302, 402, 502, 602, 702, 1002, …
v = 0 and i % 100 = 3..4 or v != 0 @integer 3, 4, 103, 104, 203, 204, 303, 304, 403, 404, 503, 504, 603, 604, 703, 704, 1003, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
@integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …
v = 0 and i % 100 = 1 or f % 100 = 1 @integer 1, 101, 201, 301, 401, 501, 601, 701, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …
v = 0 and i % 100 = 2 or f % 100 = 2 @integer 2, 102, 202, 302, 402, 502, 602, 702, 1002, … @decimal 0.2, 1.2, 2.2, 3.2, 4.2, 5.2, 6.2, 7.2, 10.2, 100.2, 1000.2, …
v = 0 and i % 100 = 3..4 or f % 100 = 3..4 @integer 3, 4, 103, 104, 203, 204, 303, 304, 403, 404, 503, 504, 603, 604, 703, 704, 1003, … @decimal 0.3, 0.4, 1.3, 1.4, 2.3, 2.4, 3.3, 3.4, 4.3, 4.4, 5.3, 5.4, 6.3, 6.4, 7.3, 7.4, 10.3, 100.3, 1000.3, …
@integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.5~1.0, 1.5~2.0, 2.5~2.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
i = 1 and v = 0 @integer 1
i = 2..4 and v = 0 @integer 2~4
v != 0 @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
@integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …
i = 1 and v = 0 @integer 1
v = 0 and i % 10 = 2..4 and i % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, …
v = 0 and i != 1 and i % 10 = 0..1 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 12..14 @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …
@decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
n % 10 = 1 and n % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 71.0, 81.0, 101.0, 1001.0, …
n % 10 = 2..4 and n % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, … @decimal 2.0, 3.0, 4.0, 22.0, 23.0, 24.0, 32.0, 33.0, 102.0, 1002.0, …
n % 10 = 0 or n % 10 = 5..9 or n % 100 = 11..14 @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
@decimal 0.1~0.9, 1.1~1.7, 10.1, 100.1, 1000.1, …
n % 10 = 1 and n % 100 != 11..19 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 71.0, 81.0, 101.0, 1001.0, …
n % 10 = 2..9 and n % 100 != 11..19 @integer 2~9, 22~29, 102, 1002, … @decimal 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 22.0, 102.0, 1002.0, …
f != 0 @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.1, 1000.1, …
@integer 0, 10~20, 30, 40, 50, 60, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
v = 0 and i % 10 = 1 and i % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …
v = 0 and i % 10 = 2..4 and i % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, …
v = 0 and i % 10 = 0 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 11..14 @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …
@decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
n % 10 = 1 and n % 100 != 11,71,91 @integer 1, 21, 31, 41, 51, 61, 81, 101, 1001, … @decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 81.0, 101.0, 1001.0, …
n % 10 = 2 and n % 100 != 12,72,92 @integer 2, 22, 32, 42, 52, 62, 82, 102, 1002, … @decimal 2.0, 22.0, 32.0, 42.0, 52.0, 62.0, 82.0, 102.0, 1002.0, …
n % 10 = 3..4,9 and n % 100 != 10..19,70..79,90..99 @integer 3, 4, 9, 23, 24, 29, 33, 34, 39, 43, 44, 49, 103, 1003, … @decimal 3.0, 4.0, 9.0, 23.0, 24.0, 29.0, 33.0, 34.0, 103.0, 1003.0, …
n != 0 and n % 1000000 = 0 @integer 1000000, … @decimal 1000000.0, 1000000.00, 1000000.000, 1000000.0000, …
@integer 0, 5~8, 10~20, 100, 1000, 10000, 100000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, …
n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000
n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000
n = 0 or n % 100 = 3..10 @integer 0, 3~10, 103~109, 1003, … @decimal 0.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 103.0, 1003.0, …
n % 100 = 11..19 @integer 11~19, 111~117, 1011, … @decimal 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 111.0, 1011.0, …
@integer 20~35, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000
n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000
n = 3..6 @integer 3~6 @decimal 3.0, 4.0, 5.0, 6.0, 3.00, 4.00, 5.00, 6.00, 3.000, 4.000, 5.000, 6.000, 3.0000, 4.0000, 5.0000, 6.0000
n = 7..10 @integer 7~10 @decimal 7.0, 8.0, 9.0, 10.0, 7.00, 8.00, 9.00, 10.00, 7.000, 8.000, 9.000, 10.000, 7.0000, 8.0000, 9.0000, 10.0000
@integer 0, 11~25, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
v = 0 and i % 10 = 1 @integer 1, 11, 21, 31, 41, 51, 61, 71, 101, 1001, …
v = 0 and i % 10 = 2 @integer 2, 12, 22, 32, 42, 52, 62, 72, 102, 1002, …
v = 0 and i % 100 = 0,20,40,60,80 @integer 0, 20, 40, 60, 80, 100, 120, 140, 1000, 10000, 100000, 1000000, …
v != 0 @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
@integer 3~10, 13~19, 23, 103, 1003, …
n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000
n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000
n % 100 = 2,22,42,62,82 or n % 1000 = 0 and n % 100000 = 1000..20000,40000,60000,80000 or n != 0 and n % 1000000 = 100000 @integer 2, 22, 42, 62, 82, 102, 122, 142, 1000, 10000, 100000, … @decimal 2.0, 22.0, 42.0, 62.0, 82.0, 102.0, 122.0, 142.0, 1000.0, 10000.0, 100000.0, …
n % 100 = 3,23,43,63,83 @integer 3, 23, 43, 63, 83, 103, 123, 143, 1003, … @decimal 3.0, 23.0, 43.0, 63.0, 83.0, 103.0, 123.0, 143.0, 1003.0, …
n != 1 and n % 100 = 1,21,41,61,81 @integer 21, 41, 61, 81, 101, 121, 141, 161, 1001, … @decimal 21.0, 41.0, 61.0, 81.0, 101.0, 121.0, 141.0, 161.0, 1001.0, …
@integer 4~19, 100, 1004, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.1, 1000000.0, …
n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000
n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000
n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000
n % 100 = 3..10 @integer 3~10, 103~110, 1003, … @decimal 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 103.0, 1003.0, …
n % 100 = 11..99 @integer 11~26, 111, 1011, … @decimal 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 111.0, 1011.0, …
@integer 100~102, 200~202, 300~302, 400~402, 500~502, 600, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000
n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000
n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000
n = 3 @integer 3 @decimal 3.0, 3.00, 3.000, 3.0000
n = 6 @integer 6 @decimal 6.0, 6.00, 6.000, 6.0000
@integer 4, 5, 7~20, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
go-i18n-2.6.0/internal/plural/codegen/xml.go 0000664 0000000 0000000 00000011237 14774252036 0020565 0 ustar 00root root 0000000 0000000 package main
import (
"encoding/xml"
"fmt"
"regexp"
"strconv"
"strings"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
// SupplementalData is the top level struct of plural.xml
type SupplementalData struct {
XMLName xml.Name `xml:"supplementalData"`
PluralGroups []PluralGroup `xml:"plurals>pluralRules"`
}
// PluralGroup is a group of locales with the same plural rules.
type PluralGroup struct {
Locales string `xml:"locales,attr"`
PluralRules []PluralRule `xml:"pluralRule"`
}
// Name returns a unique name for this plural group.
func (pg *PluralGroup) Name() string {
n := cases.Title(language.AmericanEnglish).String(pg.Locales)
return strings.ReplaceAll(n, " ", "")
}
// SplitLocales returns all the locales in the PluralGroup as a slice.
func (pg *PluralGroup) SplitLocales() []string {
return strings.Split(pg.Locales, " ")
}
// PluralRule is the rule for a single plural form.
type PluralRule struct {
Count string `xml:"count,attr"`
Rule string `xml:",innerxml"`
}
// CountTitle returns the title case of the PluralRule's count.
func (pr *PluralRule) CountTitle() string {
return cases.Title(language.AmericanEnglish).String(pr.Count)
}
// Condition returns the condition where the PluralRule applies.
func (pr *PluralRule) Condition() string {
i := strings.Index(pr.Rule, "@")
return pr.Rule[:i]
}
// Examples returns the integer and decimal examples for the PluralRule.
func (pr *PluralRule) Examples() (integers []string, decimals []string) {
ex := strings.ReplaceAll(pr.Rule, ", …", "")
ddelim := "@decimal"
if i := strings.Index(ex, ddelim); i > 0 {
dex := strings.TrimSpace(ex[i+len(ddelim):])
dex = strings.ReplaceAll(dex, "c", "e")
decimals = strings.Split(dex, ", ")
ex = ex[:i]
}
idelim := "@integer"
if i := strings.Index(ex, idelim); i > 0 {
iex := strings.TrimSpace(ex[i+len(idelim):])
integers = strings.Split(iex, ", ")
for j, integer := range integers {
ii := strings.IndexAny(integer, "eEcC")
if ii > 0 {
zeros, err := strconv.ParseInt(integer[ii+1:], 10, 0)
if err != nil {
panic(err)
}
integers[j] = integer[:ii] + strings.Repeat("0", int(zeros))
}
}
}
return integers, decimals
}
// IntegerExamples returns the integer examples for the PluralRule.
func (pr *PluralRule) IntegerExamples() []string {
integer, _ := pr.Examples()
return integer
}
// DecimalExamples returns the decimal examples for the PluralRule.
func (pr *PluralRule) DecimalExamples() []string {
_, decimal := pr.Examples()
return decimal
}
var relationRegexp = regexp.MustCompile(`([niftvwce])(?:\s*%\s*([0-9]+))?\s*(!=|=)(.*)`)
// GoCondition converts the XML condition to valid Go code.
func (pr *PluralRule) GoCondition() string {
var ors []string
for _, and := range strings.Split(pr.Condition(), "or") {
var ands []string
for _, relation := range strings.Split(and, "and") {
parts := relationRegexp.FindStringSubmatch(relation)
if parts == nil {
continue
}
lvar := cases.Title(language.AmericanEnglish).String(parts[1])
lmod, op, rhs := parts[2], parts[3], strings.TrimSpace(parts[4])
if op == "=" {
op = "=="
}
if lvar == "E" {
// E is a deprecated symbol for C
// https://unicode.org/reports/tr35/tr35-numbers.html#Plural_Operand_Meanings
lvar = "C"
}
lvar = "ops." + lvar
var rhor []string
var rany []string
for _, rh := range strings.Split(rhs, ",") {
if parts := strings.Split(rh, ".."); len(parts) == 2 {
from, to := parts[0], parts[1]
if lvar == "ops.N" {
if lmod != "" {
rhor = append(rhor, fmt.Sprintf("ops.NModInRange(%s, %s, %s)", lmod, from, to))
} else {
rhor = append(rhor, fmt.Sprintf("ops.NInRange(%s, %s)", from, to))
}
} else if lmod != "" {
rhor = append(rhor, fmt.Sprintf("intInRange(%s %% %s, %s, %s)", lvar, lmod, from, to))
} else {
rhor = append(rhor, fmt.Sprintf("intInRange(%s, %s, %s)", lvar, from, to))
}
} else {
rany = append(rany, rh)
}
}
if len(rany) > 0 {
rh := strings.Join(rany, ",")
if lvar == "ops.N" {
if lmod != "" {
rhor = append(rhor, fmt.Sprintf("ops.NModEqualsAny(%s, %s)", lmod, rh))
} else {
rhor = append(rhor, fmt.Sprintf("ops.NEqualsAny(%s)", rh))
}
} else if lmod != "" {
rhor = append(rhor, fmt.Sprintf("intEqualsAny(%s %% %s, %s)", lvar, lmod, rh))
} else {
rhor = append(rhor, fmt.Sprintf("intEqualsAny(%s, %s)", lvar, rh))
}
}
r := strings.Join(rhor, " || ")
if len(rhor) > 1 {
r = "(" + r + ")"
}
if op == "!=" {
r = "!" + r
}
ands = append(ands, r)
}
ors = append(ors, strings.Join(ands, " && "))
}
return strings.Join(ors, " ||\n")
}
go-i18n-2.6.0/internal/plural/doc.go 0000664 0000000 0000000 00000000233 14774252036 0017120 0 ustar 00root root 0000000 0000000 // Package plural provides support for pluralizing messages
// according to CLDR rules http://cldr.unicode.org/index/cldr-spec/plural-rules
package plural
go-i18n-2.6.0/internal/plural/form.go 0000664 0000000 0000000 00000000536 14774252036 0017324 0 ustar 00root root 0000000 0000000 package plural
// Form represents a language pluralization form as defined here:
// http://cldr.unicode.org/index/cldr-spec/plural-rules
type Form string
// All defined plural forms.
const (
Invalid Form = ""
Zero Form = "zero"
One Form = "one"
Two Form = "two"
Few Form = "few"
Many Form = "many"
Other Form = "other"
)
go-i18n-2.6.0/internal/plural/operands.go 0000664 0000000 0000000 00000011475 14774252036 0020200 0 ustar 00root root 0000000 0000000 package plural
import (
"fmt"
"strconv"
"strings"
)
// Operands is a representation of http://unicode.org/reports/tr35/tr35-numbers.html#Operands
// If there is a compact decimal exponent value C, then the N, I, V, W, F, and T values are computed after shifting the decimal point in the original by the ‘c’ value.
// So for 1.2c3, the values are the same as those of 1200: i=1200 and f=0.
// Similarly, 1.2005c3 has i=1200 and f=5 (corresponding to 1200.5).
type Operands struct {
N float64 // absolute value of the source number (integer and decimals)
I int64 // integer digits of n
V int64 // number of visible fraction digits in n, with trailing zeros
W int64 // number of visible fraction digits in n, without trailing zeros
F int64 // visible fractional digits in n, with trailing zeros
T int64 // visible fractional digits in n, without trailing zeros
C int64 // compact decimal exponent value: exponent of the power of 10 used in compact decimal formatting.
}
// NEqualsAny returns true if o represents an integer equal to any of the arguments.
func (o *Operands) NEqualsAny(any ...int64) bool {
for _, i := range any {
if o.I == i && o.T == 0 {
return true
}
}
return false
}
// NModEqualsAny returns true if o represents an integer equal to any of the arguments modulo mod.
func (o *Operands) NModEqualsAny(mod int64, any ...int64) bool {
modI := o.I % mod
for _, i := range any {
if modI == i && o.T == 0 {
return true
}
}
return false
}
// NInRange returns true if o represents an integer in the closed interval [from, to].
func (o *Operands) NInRange(from, to int64) bool {
return o.T == 0 && from <= o.I && o.I <= to
}
// NModInRange returns true if o represents an integer in the closed interval [from, to] modulo mod.
func (o *Operands) NModInRange(mod, from, to int64) bool {
modI := o.I % mod
return o.T == 0 && from <= modI && modI <= to
}
// NewOperands returns the operands for number.
func NewOperands(number interface{}) (*Operands, error) {
switch number := number.(type) {
case int:
return newOperandsInt64(int64(number)), nil
case int8:
return newOperandsInt64(int64(number)), nil
case int16:
return newOperandsInt64(int64(number)), nil
case int32:
return newOperandsInt64(int64(number)), nil
case int64:
return newOperandsInt64(number), nil
case string:
return newOperandsString(number)
case float32, float64:
return nil, fmt.Errorf("floats should be formatted into a string")
default:
return nil, fmt.Errorf("invalid type %T; expected integer or string", number)
}
}
func newOperandsInt64(i int64) *Operands {
if i < 0 {
i = -i
}
return &Operands{float64(i), i, 0, 0, 0, 0, 0}
}
func splitSignificandExponent(s string) (significand, exponent string) {
i := strings.IndexAny(s, "eE")
if i < 0 {
return s, ""
}
return s[:i], s[i+1:]
}
func shiftDecimalLeft(s string, n int) string {
if n <= 0 {
return s
}
i := strings.IndexRune(s, '.')
tilt := 0
if i < 0 {
i = len(s)
tilt = -1
}
switch {
case n == i:
return "0." + s[:i] + s[i+1+tilt:]
case n > i:
return "0." + strings.Repeat("0", n-i) + s[:i] + s[i+1+tilt:]
default:
return s[:i-n] + "." + s[i-n:i] + s[i+1+tilt:]
}
}
func shiftDecimalRight(s string, n int) string {
if n <= 0 {
return s
}
i := strings.IndexRune(s, '.')
if i < 0 {
return s + strings.Repeat("0", n)
}
switch rest := len(s) - i - 1; {
case n == rest:
return s[:i] + s[i+1:]
case n > rest:
return s[:i] + s[i+1:] + strings.Repeat("0", n-rest)
default:
return s[:i] + s[i+1:i+1+n] + "." + s[i+1+n:]
}
}
func applyExponent(s string, exponent int) string {
switch {
case exponent > 0:
return shiftDecimalRight(s, exponent)
case exponent < 0:
return shiftDecimalLeft(s, -exponent)
}
return s
}
func newOperandsString(s string) (*Operands, error) {
if s[0] == '-' {
s = s[1:]
}
ops := &Operands{}
var err error
ops.N, err = strconv.ParseFloat(s, 64)
if err != nil {
return nil, err
}
significand, exponent := splitSignificandExponent(s)
if exponent != "" {
// We are storing C as an int64 but only allowing
// numbers that fit into the bitsize of an int
// so C is safe to cast as a int later.
ops.C, err = strconv.ParseInt(exponent, 10, 0)
if err != nil {
return nil, err
}
}
value := applyExponent(significand, int(ops.C))
parts := strings.SplitN(value, ".", 2)
ops.I, err = strconv.ParseInt(parts[0], 10, 64)
if err != nil {
return nil, err
}
if len(parts) == 1 {
return ops, nil
}
fraction := parts[1]
ops.V = int64(len(fraction))
for i := ops.V - 1; i >= 0; i-- {
if fraction[i] != '0' {
ops.W = i + 1
break
}
}
if ops.V > 0 {
f, err := strconv.ParseInt(fraction, 10, 0)
if err != nil {
return nil, err
}
ops.F = f
}
if ops.W > 0 {
t, err := strconv.ParseInt(fraction[:ops.W], 10, 0)
if err != nil {
return nil, err
}
ops.T = t
}
return ops, nil
}
go-i18n-2.6.0/internal/plural/operands_test.go 0000664 0000000 0000000 00000005433 14774252036 0021234 0 ustar 00root root 0000000 0000000 package plural
import (
"reflect"
"testing"
)
func TestNewOperands(t *testing.T) {
tests := []struct {
input interface{}
ops *Operands
err bool
}{
{int64(0), &Operands{0.0, 0, 0, 0, 0, 0, 0}, false},
{int64(1), &Operands{1.0, 1, 0, 0, 0, 0, 0}, false},
{"0", &Operands{0.0, 0, 0, 0, 0, 0, 0}, false},
{"1", &Operands{1.0, 1, 0, 0, 0, 0, 0}, false},
{"1.0", &Operands{1.0, 1, 1, 0, 0, 0, 0}, false},
{"1.00", &Operands{1.0, 1, 2, 0, 0, 0, 0}, false},
{"1.3", &Operands{1.3, 1, 1, 1, 3, 3, 0}, false},
{"1.30", &Operands{1.3, 1, 2, 1, 30, 3, 0}, false},
{"1.03", &Operands{1.03, 1, 2, 2, 3, 3, 0}, false},
{"1.230", &Operands{1.23, 1, 3, 2, 230, 23, 0}, false},
{"20.0230", &Operands{20.023, 20, 4, 3, 230, 23, 0}, false},
{20.0230, nil, true},
{"1200", &Operands{1200, 1200, 0, 0, 0, 0, 0}, false},
{"1.2e3", &Operands{1200, 1200, 0, 0, 0, 0, 3}, false},
{"1.2E3", &Operands{1200, 1200, 0, 0, 0, 0, 3}, false},
{"1234", &Operands{1234, 1234, 0, 0, 0, 0, 0}, false},
{"1234e0", &Operands{1234, 1234, 0, 0, 0, 0, 0}, false},
{"123.4e1", &Operands{1234, 1234, 0, 0, 0, 0, 1}, false},
{"12.34e2", &Operands{1234, 1234, 0, 0, 0, 0, 2}, false},
{"1.234e3", &Operands{1234, 1234, 0, 0, 0, 0, 3}, false},
{"0.1234e4", &Operands{1234, 1234, 0, 0, 0, 0, 4}, false},
{"0.01234e5", &Operands{1234, 1234, 0, 0, 0, 0, 5}, false},
{"1234.0", &Operands{1234, 1234, 1, 0, 0, 0, 0}, false},
{"12340e-1", &Operands{1234, 1234, 1, 0, 0, 0, -1}, false},
{"1200.5", &Operands{1200.5, 1200, 1, 1, 5, 5, 0}, false},
{"1.2005e3", &Operands{1200.5, 1200, 1, 1, 5, 5, 3}, false},
{"1200e3", &Operands{1200000, 1200000, 0, 0, 0, 0, 3}, false},
{"0.0012340", &Operands{0.001234, 0, 7, 6, 12340, 1234, 0}, false},
{"0.012340e-1", &Operands{0.001234, 0, 7, 6, 12340, 1234, -1}, false},
{"0.12340e-2", &Operands{0.001234, 0, 7, 6, 12340, 1234, -2}, false},
{"1.2340e-3", &Operands{0.001234, 0, 7, 6, 12340, 1234, -3}, false},
{"12.340e-4", &Operands{0.001234, 0, 7, 6, 12340, 1234, -4}, false},
{"123.40e-5", &Operands{0.001234, 0, 7, 6, 12340, 1234, -5}, false},
{"1234.0e-6", &Operands{0.001234, 0, 7, 6, 12340, 1234, -6}, false},
{"12340e-7", &Operands{0.001234, 0, 7, 6, 12340, 1234, -7}, false},
}
for _, test := range tests {
ops, err := NewOperands(test.input)
if err != nil && !test.err {
t.Errorf("NewOperands(%#v) unexpected error: %s", test.input, err)
} else if err == nil && test.err {
t.Errorf("NewOperands(%#v) returned %#v; expected error", test.input, ops)
} else if !reflect.DeepEqual(ops, test.ops) {
t.Errorf("NewOperands(%#v) returned %#v; expected %#v", test.input, ops, test.ops)
}
}
}
func BenchmarkNewOperand(b *testing.B) {
for i := 0; i < b.N; i++ {
if _, err := NewOperands("1234.56780000"); err != nil {
b.Fatal(err)
}
}
}
go-i18n-2.6.0/internal/plural/rule.go 0000664 0000000 0000000 00000001634 14774252036 0017330 0 ustar 00root root 0000000 0000000 package plural
import (
"golang.org/x/text/language"
)
// Rule defines the CLDR plural rules for a language.
// http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html
// http://unicode.org/reports/tr35/tr35-numbers.html#Operands
type Rule struct {
PluralForms map[Form]struct{}
PluralFormFunc func(*Operands) Form
}
func addPluralRules(rules Rules, ids []string, ps *Rule) {
for _, id := range ids {
if id == "root" {
continue
}
tag := language.MustParse(id)
rules[tag] = ps
}
}
func newPluralFormSet(pluralForms ...Form) map[Form]struct{} {
set := make(map[Form]struct{}, len(pluralForms))
for _, plural := range pluralForms {
set[plural] = struct{}{}
}
return set
}
func intInRange(i, from, to int64) bool {
return from <= i && i <= to
}
func intEqualsAny(i int64, any ...int64) bool {
for _, a := range any {
if i == a {
return true
}
}
return false
}
go-i18n-2.6.0/internal/plural/rule_gen.go 0000664 0000000 0000000 00000046067 14774252036 0020172 0 ustar 00root root 0000000 0000000 // This file is generated by i18n/plural/codegen/generate.sh; DO NOT EDIT
package plural
// DefaultRules returns a map of Rules generated from CLDR language data.
func DefaultRules() Rules {
rules := Rules{}
addPluralRules(rules, []string{"bm", "bo", "dz", "hnj", "id", "ig", "ii", "in", "ja", "jbo", "jv", "jw", "kde", "kea", "km", "ko", "lkt", "lo", "ms", "my", "nqo", "osa", "root", "sah", "ses", "sg", "su", "th", "to", "tpi", "vi", "wo", "yo", "yue", "zh"}, &Rule{
PluralForms: newPluralFormSet(Other),
PluralFormFunc: func(ops *Operands) Form {
return Other
},
})
addPluralRules(rules, []string{"am", "as", "bn", "doi", "fa", "gu", "hi", "kn", "pcm", "zu"}, &Rule{
PluralForms: newPluralFormSet(One, Other),
PluralFormFunc: func(ops *Operands) Form {
// i = 0 or n = 1
if intEqualsAny(ops.I, 0) ||
ops.NEqualsAny(1) {
return One
}
return Other
},
})
addPluralRules(rules, []string{"ff", "hy", "kab"}, &Rule{
PluralForms: newPluralFormSet(One, Other),
PluralFormFunc: func(ops *Operands) Form {
// i = 0,1
if intEqualsAny(ops.I, 0, 1) {
return One
}
return Other
},
})
addPluralRules(rules, []string{"ast", "de", "en", "et", "fi", "fy", "gl", "ia", "io", "ji", "lij", "nl", "sc", "sv", "sw", "ur", "yi"}, &Rule{
PluralForms: newPluralFormSet(One, Other),
PluralFormFunc: func(ops *Operands) Form {
// i = 1 and v = 0
if intEqualsAny(ops.I, 1) && intEqualsAny(ops.V, 0) {
return One
}
return Other
},
})
addPluralRules(rules, []string{"si"}, &Rule{
PluralForms: newPluralFormSet(One, Other),
PluralFormFunc: func(ops *Operands) Form {
// n = 0,1 or i = 0 and f = 1
if ops.NEqualsAny(0, 1) ||
intEqualsAny(ops.I, 0) && intEqualsAny(ops.F, 1) {
return One
}
return Other
},
})
addPluralRules(rules, []string{"ak", "bho", "csw", "guw", "ln", "mg", "nso", "pa", "ti", "wa"}, &Rule{
PluralForms: newPluralFormSet(One, Other),
PluralFormFunc: func(ops *Operands) Form {
// n = 0..1
if ops.NInRange(0, 1) {
return One
}
return Other
},
})
addPluralRules(rules, []string{"tzm"}, &Rule{
PluralForms: newPluralFormSet(One, Other),
PluralFormFunc: func(ops *Operands) Form {
// n = 0..1 or n = 11..99
if ops.NInRange(0, 1) ||
ops.NInRange(11, 99) {
return One
}
return Other
},
})
addPluralRules(rules, []string{"af", "an", "asa", "az", "bal", "bem", "bez", "bg", "brx", "ce", "cgg", "chr", "ckb", "dv", "ee", "el", "eo", "eu", "fo", "fur", "gsw", "ha", "haw", "hu", "jgo", "jmc", "ka", "kaj", "kcg", "kk", "kkj", "kl", "ks", "ksb", "ku", "ky", "lb", "lg", "mas", "mgo", "ml", "mn", "mr", "nah", "nb", "nd", "ne", "nn", "nnh", "no", "nr", "ny", "nyn", "om", "or", "os", "pap", "ps", "rm", "rof", "rwk", "saq", "sd", "sdh", "seh", "sn", "so", "sq", "ss", "ssy", "st", "syr", "ta", "te", "teo", "tig", "tk", "tn", "tr", "ts", "ug", "uz", "ve", "vo", "vun", "wae", "xh", "xog"}, &Rule{
PluralForms: newPluralFormSet(One, Other),
PluralFormFunc: func(ops *Operands) Form {
// n = 1
if ops.NEqualsAny(1) {
return One
}
return Other
},
})
addPluralRules(rules, []string{"da"}, &Rule{
PluralForms: newPluralFormSet(One, Other),
PluralFormFunc: func(ops *Operands) Form {
// n = 1 or t != 0 and i = 0,1
if ops.NEqualsAny(1) ||
!intEqualsAny(ops.T, 0) && intEqualsAny(ops.I, 0, 1) {
return One
}
return Other
},
})
addPluralRules(rules, []string{"is"}, &Rule{
PluralForms: newPluralFormSet(One, Other),
PluralFormFunc: func(ops *Operands) Form {
// t = 0 and i % 10 = 1 and i % 100 != 11 or t % 10 = 1 and t % 100 != 11
if intEqualsAny(ops.T, 0) && intEqualsAny(ops.I%10, 1) && !intEqualsAny(ops.I%100, 11) ||
intEqualsAny(ops.T%10, 1) && !intEqualsAny(ops.T%100, 11) {
return One
}
return Other
},
})
addPluralRules(rules, []string{"mk"}, &Rule{
PluralForms: newPluralFormSet(One, Other),
PluralFormFunc: func(ops *Operands) Form {
// v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 1) && !intEqualsAny(ops.I%100, 11) ||
intEqualsAny(ops.F%10, 1) && !intEqualsAny(ops.F%100, 11) {
return One
}
return Other
},
})
addPluralRules(rules, []string{"ceb", "fil", "tl"}, &Rule{
PluralForms: newPluralFormSet(One, Other),
PluralFormFunc: func(ops *Operands) Form {
// v = 0 and i = 1,2,3 or v = 0 and i % 10 != 4,6,9 or v != 0 and f % 10 != 4,6,9
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I, 1, 2, 3) ||
intEqualsAny(ops.V, 0) && !intEqualsAny(ops.I%10, 4, 6, 9) ||
!intEqualsAny(ops.V, 0) && !intEqualsAny(ops.F%10, 4, 6, 9) {
return One
}
return Other
},
})
addPluralRules(rules, []string{"lv", "prg"}, &Rule{
PluralForms: newPluralFormSet(Zero, One, Other),
PluralFormFunc: func(ops *Operands) Form {
// n % 10 = 0 or n % 100 = 11..19 or v = 2 and f % 100 = 11..19
if ops.NModEqualsAny(10, 0) ||
ops.NModInRange(100, 11, 19) ||
intEqualsAny(ops.V, 2) && intInRange(ops.F%100, 11, 19) {
return Zero
}
// n % 10 = 1 and n % 100 != 11 or v = 2 and f % 10 = 1 and f % 100 != 11 or v != 2 and f % 10 = 1
if ops.NModEqualsAny(10, 1) && !ops.NModEqualsAny(100, 11) ||
intEqualsAny(ops.V, 2) && intEqualsAny(ops.F%10, 1) && !intEqualsAny(ops.F%100, 11) ||
!intEqualsAny(ops.V, 2) && intEqualsAny(ops.F%10, 1) {
return One
}
return Other
},
})
addPluralRules(rules, []string{"lag"}, &Rule{
PluralForms: newPluralFormSet(Zero, One, Other),
PluralFormFunc: func(ops *Operands) Form {
// n = 0
if ops.NEqualsAny(0) {
return Zero
}
// i = 0,1 and n != 0
if intEqualsAny(ops.I, 0, 1) && !ops.NEqualsAny(0) {
return One
}
return Other
},
})
addPluralRules(rules, []string{"blo"}, &Rule{
PluralForms: newPluralFormSet(Zero, One, Other),
PluralFormFunc: func(ops *Operands) Form {
// n = 0
if ops.NEqualsAny(0) {
return Zero
}
// n = 1
if ops.NEqualsAny(1) {
return One
}
return Other
},
})
addPluralRules(rules, []string{"ksh"}, &Rule{
PluralForms: newPluralFormSet(Zero, One, Other),
PluralFormFunc: func(ops *Operands) Form {
// n = 0
if ops.NEqualsAny(0) {
return Zero
}
// n = 1
if ops.NEqualsAny(1) {
return One
}
return Other
},
})
addPluralRules(rules, []string{"he", "iw"}, &Rule{
PluralForms: newPluralFormSet(One, Two, Other),
PluralFormFunc: func(ops *Operands) Form {
// i = 1 and v = 0 or i = 0 and v != 0
if intEqualsAny(ops.I, 1) && intEqualsAny(ops.V, 0) ||
intEqualsAny(ops.I, 0) && !intEqualsAny(ops.V, 0) {
return One
}
// i = 2 and v = 0
if intEqualsAny(ops.I, 2) && intEqualsAny(ops.V, 0) {
return Two
}
return Other
},
})
addPluralRules(rules, []string{"iu", "naq", "sat", "se", "sma", "smi", "smj", "smn", "sms"}, &Rule{
PluralForms: newPluralFormSet(One, Two, Other),
PluralFormFunc: func(ops *Operands) Form {
// n = 1
if ops.NEqualsAny(1) {
return One
}
// n = 2
if ops.NEqualsAny(2) {
return Two
}
return Other
},
})
addPluralRules(rules, []string{"shi"}, &Rule{
PluralForms: newPluralFormSet(One, Few, Other),
PluralFormFunc: func(ops *Operands) Form {
// i = 0 or n = 1
if intEqualsAny(ops.I, 0) ||
ops.NEqualsAny(1) {
return One
}
// n = 2..10
if ops.NInRange(2, 10) {
return Few
}
return Other
},
})
addPluralRules(rules, []string{"mo", "ro"}, &Rule{
PluralForms: newPluralFormSet(One, Few, Other),
PluralFormFunc: func(ops *Operands) Form {
// i = 1 and v = 0
if intEqualsAny(ops.I, 1) && intEqualsAny(ops.V, 0) {
return One
}
// v != 0 or n = 0 or n != 1 and n % 100 = 1..19
if !intEqualsAny(ops.V, 0) ||
ops.NEqualsAny(0) ||
!ops.NEqualsAny(1) && ops.NModInRange(100, 1, 19) {
return Few
}
return Other
},
})
addPluralRules(rules, []string{"bs", "hr", "sh", "sr"}, &Rule{
PluralForms: newPluralFormSet(One, Few, Other),
PluralFormFunc: func(ops *Operands) Form {
// v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 1) && !intEqualsAny(ops.I%100, 11) ||
intEqualsAny(ops.F%10, 1) && !intEqualsAny(ops.F%100, 11) {
return One
}
// v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14
if intEqualsAny(ops.V, 0) && intInRange(ops.I%10, 2, 4) && !intInRange(ops.I%100, 12, 14) ||
intInRange(ops.F%10, 2, 4) && !intInRange(ops.F%100, 12, 14) {
return Few
}
return Other
},
})
addPluralRules(rules, []string{"fr"}, &Rule{
PluralForms: newPluralFormSet(One, Many, Other),
PluralFormFunc: func(ops *Operands) Form {
// i = 0,1
if intEqualsAny(ops.I, 0, 1) {
return One
}
// e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5
if intEqualsAny(ops.C, 0) && !intEqualsAny(ops.I, 0) && intEqualsAny(ops.I%1000000, 0) && intEqualsAny(ops.V, 0) ||
!intInRange(ops.C, 0, 5) {
return Many
}
return Other
},
})
addPluralRules(rules, []string{"pt"}, &Rule{
PluralForms: newPluralFormSet(One, Many, Other),
PluralFormFunc: func(ops *Operands) Form {
// i = 0..1
if intInRange(ops.I, 0, 1) {
return One
}
// e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5
if intEqualsAny(ops.C, 0) && !intEqualsAny(ops.I, 0) && intEqualsAny(ops.I%1000000, 0) && intEqualsAny(ops.V, 0) ||
!intInRange(ops.C, 0, 5) {
return Many
}
return Other
},
})
addPluralRules(rules, []string{"ca", "it", "lld", "pt_PT", "scn", "vec"}, &Rule{
PluralForms: newPluralFormSet(One, Many, Other),
PluralFormFunc: func(ops *Operands) Form {
// i = 1 and v = 0
if intEqualsAny(ops.I, 1) && intEqualsAny(ops.V, 0) {
return One
}
// e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5
if intEqualsAny(ops.C, 0) && !intEqualsAny(ops.I, 0) && intEqualsAny(ops.I%1000000, 0) && intEqualsAny(ops.V, 0) ||
!intInRange(ops.C, 0, 5) {
return Many
}
return Other
},
})
addPluralRules(rules, []string{"es"}, &Rule{
PluralForms: newPluralFormSet(One, Many, Other),
PluralFormFunc: func(ops *Operands) Form {
// n = 1
if ops.NEqualsAny(1) {
return One
}
// e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5
if intEqualsAny(ops.C, 0) && !intEqualsAny(ops.I, 0) && intEqualsAny(ops.I%1000000, 0) && intEqualsAny(ops.V, 0) ||
!intInRange(ops.C, 0, 5) {
return Many
}
return Other
},
})
addPluralRules(rules, []string{"gd"}, &Rule{
PluralForms: newPluralFormSet(One, Two, Few, Other),
PluralFormFunc: func(ops *Operands) Form {
// n = 1,11
if ops.NEqualsAny(1, 11) {
return One
}
// n = 2,12
if ops.NEqualsAny(2, 12) {
return Two
}
// n = 3..10,13..19
if ops.NInRange(3, 10) || ops.NInRange(13, 19) {
return Few
}
return Other
},
})
addPluralRules(rules, []string{"sl"}, &Rule{
PluralForms: newPluralFormSet(One, Two, Few, Other),
PluralFormFunc: func(ops *Operands) Form {
// v = 0 and i % 100 = 1
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%100, 1) {
return One
}
// v = 0 and i % 100 = 2
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%100, 2) {
return Two
}
// v = 0 and i % 100 = 3..4 or v != 0
if intEqualsAny(ops.V, 0) && intInRange(ops.I%100, 3, 4) ||
!intEqualsAny(ops.V, 0) {
return Few
}
return Other
},
})
addPluralRules(rules, []string{"dsb", "hsb"}, &Rule{
PluralForms: newPluralFormSet(One, Two, Few, Other),
PluralFormFunc: func(ops *Operands) Form {
// v = 0 and i % 100 = 1 or f % 100 = 1
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%100, 1) ||
intEqualsAny(ops.F%100, 1) {
return One
}
// v = 0 and i % 100 = 2 or f % 100 = 2
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%100, 2) ||
intEqualsAny(ops.F%100, 2) {
return Two
}
// v = 0 and i % 100 = 3..4 or f % 100 = 3..4
if intEqualsAny(ops.V, 0) && intInRange(ops.I%100, 3, 4) ||
intInRange(ops.F%100, 3, 4) {
return Few
}
return Other
},
})
addPluralRules(rules, []string{"cs", "sk"}, &Rule{
PluralForms: newPluralFormSet(One, Few, Many, Other),
PluralFormFunc: func(ops *Operands) Form {
// i = 1 and v = 0
if intEqualsAny(ops.I, 1) && intEqualsAny(ops.V, 0) {
return One
}
// i = 2..4 and v = 0
if intInRange(ops.I, 2, 4) && intEqualsAny(ops.V, 0) {
return Few
}
// v != 0
if !intEqualsAny(ops.V, 0) {
return Many
}
return Other
},
})
addPluralRules(rules, []string{"pl"}, &Rule{
PluralForms: newPluralFormSet(One, Few, Many, Other),
PluralFormFunc: func(ops *Operands) Form {
// i = 1 and v = 0
if intEqualsAny(ops.I, 1) && intEqualsAny(ops.V, 0) {
return One
}
// v = 0 and i % 10 = 2..4 and i % 100 != 12..14
if intEqualsAny(ops.V, 0) && intInRange(ops.I%10, 2, 4) && !intInRange(ops.I%100, 12, 14) {
return Few
}
// v = 0 and i != 1 and i % 10 = 0..1 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 12..14
if intEqualsAny(ops.V, 0) && !intEqualsAny(ops.I, 1) && intInRange(ops.I%10, 0, 1) ||
intEqualsAny(ops.V, 0) && intInRange(ops.I%10, 5, 9) ||
intEqualsAny(ops.V, 0) && intInRange(ops.I%100, 12, 14) {
return Many
}
return Other
},
})
addPluralRules(rules, []string{"be"}, &Rule{
PluralForms: newPluralFormSet(One, Few, Many, Other),
PluralFormFunc: func(ops *Operands) Form {
// n % 10 = 1 and n % 100 != 11
if ops.NModEqualsAny(10, 1) && !ops.NModEqualsAny(100, 11) {
return One
}
// n % 10 = 2..4 and n % 100 != 12..14
if ops.NModInRange(10, 2, 4) && !ops.NModInRange(100, 12, 14) {
return Few
}
// n % 10 = 0 or n % 10 = 5..9 or n % 100 = 11..14
if ops.NModEqualsAny(10, 0) ||
ops.NModInRange(10, 5, 9) ||
ops.NModInRange(100, 11, 14) {
return Many
}
return Other
},
})
addPluralRules(rules, []string{"lt"}, &Rule{
PluralForms: newPluralFormSet(One, Few, Many, Other),
PluralFormFunc: func(ops *Operands) Form {
// n % 10 = 1 and n % 100 != 11..19
if ops.NModEqualsAny(10, 1) && !ops.NModInRange(100, 11, 19) {
return One
}
// n % 10 = 2..9 and n % 100 != 11..19
if ops.NModInRange(10, 2, 9) && !ops.NModInRange(100, 11, 19) {
return Few
}
// f != 0
if !intEqualsAny(ops.F, 0) {
return Many
}
return Other
},
})
addPluralRules(rules, []string{"ru", "uk"}, &Rule{
PluralForms: newPluralFormSet(One, Few, Many, Other),
PluralFormFunc: func(ops *Operands) Form {
// v = 0 and i % 10 = 1 and i % 100 != 11
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 1) && !intEqualsAny(ops.I%100, 11) {
return One
}
// v = 0 and i % 10 = 2..4 and i % 100 != 12..14
if intEqualsAny(ops.V, 0) && intInRange(ops.I%10, 2, 4) && !intInRange(ops.I%100, 12, 14) {
return Few
}
// v = 0 and i % 10 = 0 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 11..14
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 0) ||
intEqualsAny(ops.V, 0) && intInRange(ops.I%10, 5, 9) ||
intEqualsAny(ops.V, 0) && intInRange(ops.I%100, 11, 14) {
return Many
}
return Other
},
})
addPluralRules(rules, []string{"br"}, &Rule{
PluralForms: newPluralFormSet(One, Two, Few, Many, Other),
PluralFormFunc: func(ops *Operands) Form {
// n % 10 = 1 and n % 100 != 11,71,91
if ops.NModEqualsAny(10, 1) && !ops.NModEqualsAny(100, 11, 71, 91) {
return One
}
// n % 10 = 2 and n % 100 != 12,72,92
if ops.NModEqualsAny(10, 2) && !ops.NModEqualsAny(100, 12, 72, 92) {
return Two
}
// n % 10 = 3..4,9 and n % 100 != 10..19,70..79,90..99
if (ops.NModInRange(10, 3, 4) || ops.NModEqualsAny(10, 9)) && !(ops.NModInRange(100, 10, 19) || ops.NModInRange(100, 70, 79) || ops.NModInRange(100, 90, 99)) {
return Few
}
// n != 0 and n % 1000000 = 0
if !ops.NEqualsAny(0) && ops.NModEqualsAny(1000000, 0) {
return Many
}
return Other
},
})
addPluralRules(rules, []string{"mt"}, &Rule{
PluralForms: newPluralFormSet(One, Two, Few, Many, Other),
PluralFormFunc: func(ops *Operands) Form {
// n = 1
if ops.NEqualsAny(1) {
return One
}
// n = 2
if ops.NEqualsAny(2) {
return Two
}
// n = 0 or n % 100 = 3..10
if ops.NEqualsAny(0) ||
ops.NModInRange(100, 3, 10) {
return Few
}
// n % 100 = 11..19
if ops.NModInRange(100, 11, 19) {
return Many
}
return Other
},
})
addPluralRules(rules, []string{"ga"}, &Rule{
PluralForms: newPluralFormSet(One, Two, Few, Many, Other),
PluralFormFunc: func(ops *Operands) Form {
// n = 1
if ops.NEqualsAny(1) {
return One
}
// n = 2
if ops.NEqualsAny(2) {
return Two
}
// n = 3..6
if ops.NInRange(3, 6) {
return Few
}
// n = 7..10
if ops.NInRange(7, 10) {
return Many
}
return Other
},
})
addPluralRules(rules, []string{"gv"}, &Rule{
PluralForms: newPluralFormSet(One, Two, Few, Many, Other),
PluralFormFunc: func(ops *Operands) Form {
// v = 0 and i % 10 = 1
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 1) {
return One
}
// v = 0 and i % 10 = 2
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 2) {
return Two
}
// v = 0 and i % 100 = 0,20,40,60,80
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%100, 0, 20, 40, 60, 80) {
return Few
}
// v != 0
if !intEqualsAny(ops.V, 0) {
return Many
}
return Other
},
})
addPluralRules(rules, []string{"kw"}, &Rule{
PluralForms: newPluralFormSet(Zero, One, Two, Few, Many, Other),
PluralFormFunc: func(ops *Operands) Form {
// n = 0
if ops.NEqualsAny(0) {
return Zero
}
// n = 1
if ops.NEqualsAny(1) {
return One
}
// n % 100 = 2,22,42,62,82 or n % 1000 = 0 and n % 100000 = 1000..20000,40000,60000,80000 or n != 0 and n % 1000000 = 100000
if ops.NModEqualsAny(100, 2, 22, 42, 62, 82) ||
ops.NModEqualsAny(1000, 0) && (ops.NModInRange(100000, 1000, 20000) || ops.NModEqualsAny(100000, 40000, 60000, 80000)) ||
!ops.NEqualsAny(0) && ops.NModEqualsAny(1000000, 100000) {
return Two
}
// n % 100 = 3,23,43,63,83
if ops.NModEqualsAny(100, 3, 23, 43, 63, 83) {
return Few
}
// n != 1 and n % 100 = 1,21,41,61,81
if !ops.NEqualsAny(1) && ops.NModEqualsAny(100, 1, 21, 41, 61, 81) {
return Many
}
return Other
},
})
addPluralRules(rules, []string{"ar", "ars"}, &Rule{
PluralForms: newPluralFormSet(Zero, One, Two, Few, Many, Other),
PluralFormFunc: func(ops *Operands) Form {
// n = 0
if ops.NEqualsAny(0) {
return Zero
}
// n = 1
if ops.NEqualsAny(1) {
return One
}
// n = 2
if ops.NEqualsAny(2) {
return Two
}
// n % 100 = 3..10
if ops.NModInRange(100, 3, 10) {
return Few
}
// n % 100 = 11..99
if ops.NModInRange(100, 11, 99) {
return Many
}
return Other
},
})
addPluralRules(rules, []string{"cy"}, &Rule{
PluralForms: newPluralFormSet(Zero, One, Two, Few, Many, Other),
PluralFormFunc: func(ops *Operands) Form {
// n = 0
if ops.NEqualsAny(0) {
return Zero
}
// n = 1
if ops.NEqualsAny(1) {
return One
}
// n = 2
if ops.NEqualsAny(2) {
return Two
}
// n = 3
if ops.NEqualsAny(3) {
return Few
}
// n = 6
if ops.NEqualsAny(6) {
return Many
}
return Other
},
})
return rules
}
go-i18n-2.6.0/internal/plural/rule_gen_test.go 0000664 0000000 0000000 00000100553 14774252036 0021220 0 ustar 00root root 0000000 0000000 // This file is generated by i18n/plural/codegen/generate.sh; DO NOT EDIT
package plural
import "testing"
func TestBmBoDzHnjIdIgIiInJaJboJvJwKdeKeaKmKoLktLoMsMyNqoOsaRootSahSesSgSuThToTpiViWoYoYueZh(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, Other, []string{"0~15", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"bm", "bo", "dz", "hnj", "id", "ig", "ii", "in", "ja", "jbo", "jv", "jw", "kde", "kea", "km", "ko", "lkt", "lo", "ms", "my", "nqo", "osa", "root", "sah", "ses", "sg", "su", "th", "to", "tpi", "vi", "wo", "yo", "yue", "zh"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestAmAsBnDoiFaGuHiKnPcmZu(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, One, []string{"0", "1"})
tests = appendDecimalTests(tests, One, []string{"0.0~1.0", "0.00~0.04"})
tests = appendIntegerTests(tests, Other, []string{"2~17", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"1.1~2.6", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"am", "as", "bn", "doi", "fa", "gu", "hi", "kn", "pcm", "zu"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestFfHyKab(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, One, []string{"0", "1"})
tests = appendDecimalTests(tests, One, []string{"0.0~1.5"})
tests = appendIntegerTests(tests, Other, []string{"2~17", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"2.0~3.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"ff", "hy", "kab"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestAstDeEnEtFiFyGlIaIoJiLijNlScSvSwUrYi(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, One, []string{"1"})
tests = appendIntegerTests(tests, Other, []string{"0", "2~16", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"ast", "de", "en", "et", "fi", "fy", "gl", "ia", "io", "ji", "lij", "nl", "sc", "sv", "sw", "ur", "yi"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestSi(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, One, []string{"0", "1"})
tests = appendDecimalTests(tests, One, []string{"0.0", "0.1", "1.0", "0.00", "0.01", "1.00", "0.000", "0.001", "1.000", "0.0000", "0.0001", "1.0000"})
tests = appendIntegerTests(tests, Other, []string{"2~17", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.2~0.9", "1.1~1.8", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"si"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestAkBhoCswGuwLnMgNsoPaTiWa(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, One, []string{"0", "1"})
tests = appendDecimalTests(tests, One, []string{"0.0", "1.0", "0.00", "1.00", "0.000", "1.000", "0.0000", "1.0000"})
tests = appendIntegerTests(tests, Other, []string{"2~17", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.1~0.9", "1.1~1.7", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"ak", "bho", "csw", "guw", "ln", "mg", "nso", "pa", "ti", "wa"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestTzm(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, One, []string{"0", "1", "11~24"})
tests = appendDecimalTests(tests, One, []string{"0.0", "1.0", "11.0", "12.0", "13.0", "14.0", "15.0", "16.0", "17.0", "18.0", "19.0", "20.0", "21.0", "22.0", "23.0", "24.0"})
tests = appendIntegerTests(tests, Other, []string{"2~10", "100~106", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.1~0.9", "1.1~1.7", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"tzm"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestAfAnAsaAzBalBemBezBgBrxCeCggChrCkbDvEeElEoEuFoFurGswHaHawHuJgoJmcKaKajKcgKkKkjKlKsKsbKuKyLbLgMasMgoMlMnMrNahNbNdNeNnNnhNoNrNyNynOmOrOsPapPsRmRofRwkSaqSdSdhSehSnSoSqSsSsyStSyrTaTeTeoTigTkTnTrTsUgUzVeVoVunWaeXhXog(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, One, []string{"1"})
tests = appendDecimalTests(tests, One, []string{"1.0", "1.00", "1.000", "1.0000"})
tests = appendIntegerTests(tests, Other, []string{"0", "2~16", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.0~0.9", "1.1~1.6", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"af", "an", "asa", "az", "bal", "bem", "bez", "bg", "brx", "ce", "cgg", "chr", "ckb", "dv", "ee", "el", "eo", "eu", "fo", "fur", "gsw", "ha", "haw", "hu", "jgo", "jmc", "ka", "kaj", "kcg", "kk", "kkj", "kl", "ks", "ksb", "ku", "ky", "lb", "lg", "mas", "mgo", "ml", "mn", "mr", "nah", "nb", "nd", "ne", "nn", "nnh", "no", "nr", "ny", "nyn", "om", "or", "os", "pap", "ps", "rm", "rof", "rwk", "saq", "sd", "sdh", "seh", "sn", "so", "sq", "ss", "ssy", "st", "syr", "ta", "te", "teo", "tig", "tk", "tn", "tr", "ts", "ug", "uz", "ve", "vo", "vun", "wae", "xh", "xog"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestDa(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, One, []string{"1"})
tests = appendDecimalTests(tests, One, []string{"0.1~1.6"})
tests = appendIntegerTests(tests, Other, []string{"0", "2~16", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.0", "2.0~3.4", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"da"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestIs(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, One, []string{"1", "21", "31", "41", "51", "61", "71", "81", "101", "1001"})
tests = appendDecimalTests(tests, One, []string{"0.1", "1.0", "1.1", "2.1", "3.1", "4.1", "5.1", "6.1", "7.1", "10.1", "100.1", "1000.1"})
tests = appendIntegerTests(tests, Other, []string{"0", "2~16", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.0", "0.2~0.9", "1.2~1.8", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"is"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestMk(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, One, []string{"1", "21", "31", "41", "51", "61", "71", "81", "101", "1001"})
tests = appendDecimalTests(tests, One, []string{"0.1", "1.1", "2.1", "3.1", "4.1", "5.1", "6.1", "7.1", "10.1", "100.1", "1000.1"})
tests = appendIntegerTests(tests, Other, []string{"0", "2~16", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.0", "0.2~1.0", "1.2~1.7", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"mk"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestCebFilTl(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, One, []string{"0~3", "5", "7", "8", "10~13", "15", "17", "18", "20", "21", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, One, []string{"0.0~0.3", "0.5", "0.7", "0.8", "1.0~1.3", "1.5", "1.7", "1.8", "2.0", "2.1", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
tests = appendIntegerTests(tests, Other, []string{"4", "6", "9", "14", "16", "19", "24", "26", "104", "1004"})
tests = appendDecimalTests(tests, Other, []string{"0.4", "0.6", "0.9", "1.4", "1.6", "1.9", "2.4", "2.6", "10.4", "100.4", "1000.4"})
locales := []string{"ceb", "fil", "tl"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestLvPrg(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, Zero, []string{"0", "10~20", "30", "40", "50", "60", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Zero, []string{"0.0", "10.0", "11.0", "12.0", "13.0", "14.0", "15.0", "16.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
tests = appendIntegerTests(tests, One, []string{"1", "21", "31", "41", "51", "61", "71", "81", "101", "1001"})
tests = appendDecimalTests(tests, One, []string{"0.1", "1.0", "1.1", "2.1", "3.1", "4.1", "5.1", "6.1", "7.1", "10.1", "100.1", "1000.1"})
tests = appendIntegerTests(tests, Other, []string{"2~9", "22~29", "102", "1002"})
tests = appendDecimalTests(tests, Other, []string{"0.2~0.9", "1.2~1.9", "10.2", "100.2", "1000.2"})
locales := []string{"lv", "prg"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestLag(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, Zero, []string{"0"})
tests = appendDecimalTests(tests, Zero, []string{"0.0", "0.00", "0.000", "0.0000"})
tests = appendIntegerTests(tests, One, []string{"1"})
tests = appendDecimalTests(tests, One, []string{"0.1~1.6"})
tests = appendIntegerTests(tests, Other, []string{"2~17", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"2.0~3.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"lag"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestBlo(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, Zero, []string{"0"})
tests = appendDecimalTests(tests, Zero, []string{"0.0", "0.00", "0.000", "0.0000"})
tests = appendIntegerTests(tests, One, []string{"1"})
tests = appendDecimalTests(tests, One, []string{"1.0", "1.00", "1.000", "1.0000"})
tests = appendIntegerTests(tests, Other, []string{"2~17", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.1~0.9", "1.1~1.7", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"blo"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestKsh(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, Zero, []string{"0"})
tests = appendDecimalTests(tests, Zero, []string{"0.0", "0.00", "0.000", "0.0000"})
tests = appendIntegerTests(tests, One, []string{"1"})
tests = appendDecimalTests(tests, One, []string{"1.0", "1.00", "1.000", "1.0000"})
tests = appendIntegerTests(tests, Other, []string{"2~17", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.1~0.9", "1.1~1.7", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"ksh"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestHeIw(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, One, []string{"1"})
tests = appendDecimalTests(tests, One, []string{"0.0~0.9", "0.00~0.05"})
tests = appendIntegerTests(tests, Two, []string{"2"})
tests = appendIntegerTests(tests, Other, []string{"0", "3~17", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"1.0~2.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"he", "iw"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestIuNaqSatSeSmaSmiSmjSmnSms(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, One, []string{"1"})
tests = appendDecimalTests(tests, One, []string{"1.0", "1.00", "1.000", "1.0000"})
tests = appendIntegerTests(tests, Two, []string{"2"})
tests = appendDecimalTests(tests, Two, []string{"2.0", "2.00", "2.000", "2.0000"})
tests = appendIntegerTests(tests, Other, []string{"0", "3~17", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.0~0.9", "1.1~1.6", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"iu", "naq", "sat", "se", "sma", "smi", "smj", "smn", "sms"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestShi(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, One, []string{"0", "1"})
tests = appendDecimalTests(tests, One, []string{"0.0~1.0", "0.00~0.04"})
tests = appendIntegerTests(tests, Few, []string{"2~10"})
tests = appendDecimalTests(tests, Few, []string{"2.0", "3.0", "4.0", "5.0", "6.0", "7.0", "8.0", "9.0", "10.0", "2.00", "3.00", "4.00", "5.00", "6.00", "7.00", "8.00"})
tests = appendIntegerTests(tests, Other, []string{"11~26", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"1.1~1.9", "2.1~2.7", "10.1", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"shi"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestMoRo(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, One, []string{"1"})
tests = appendIntegerTests(tests, Few, []string{"0", "2~16", "101", "1001"})
tests = appendDecimalTests(tests, Few, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
tests = appendIntegerTests(tests, Other, []string{"20~35", "100", "1000", "10000", "100000", "1000000"})
locales := []string{"mo", "ro"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestBsHrShSr(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, One, []string{"1", "21", "31", "41", "51", "61", "71", "81", "101", "1001"})
tests = appendDecimalTests(tests, One, []string{"0.1", "1.1", "2.1", "3.1", "4.1", "5.1", "6.1", "7.1", "10.1", "100.1", "1000.1"})
tests = appendIntegerTests(tests, Few, []string{"2~4", "22~24", "32~34", "42~44", "52~54", "62", "102", "1002"})
tests = appendDecimalTests(tests, Few, []string{"0.2~0.4", "1.2~1.4", "2.2~2.4", "3.2~3.4", "4.2~4.4", "5.2", "10.2", "100.2", "1000.2"})
tests = appendIntegerTests(tests, Other, []string{"0", "5~19", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.0", "0.5~1.0", "1.5~2.0", "2.5~2.7", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"bs", "hr", "sh", "sr"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestFr(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, One, []string{"0", "1"})
tests = appendDecimalTests(tests, One, []string{"0.0~1.5"})
tests = appendIntegerTests(tests, Many, []string{"1000000", "1000000", "2000000", "3000000", "4000000", "5000000", "6000000"})
tests = appendDecimalTests(tests, Many, []string{"1.0000001e6", "1.1e6", "2.0000001e6", "2.1e6", "3.0000001e6", "3.1e6"})
tests = appendIntegerTests(tests, Other, []string{"2~17", "100", "1000", "10000", "100000", "1000", "2000", "3000", "4000", "5000", "6000"})
tests = appendDecimalTests(tests, Other, []string{"2.0~3.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0", "1.0001e3", "1.1e3", "2.0001e3", "2.1e3", "3.0001e3", "3.1e3"})
locales := []string{"fr"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestPt(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, One, []string{"0", "1"})
tests = appendDecimalTests(tests, One, []string{"0.0~1.5"})
tests = appendIntegerTests(tests, Many, []string{"1000000", "1000000", "2000000", "3000000", "4000000", "5000000", "6000000"})
tests = appendDecimalTests(tests, Many, []string{"1.0000001e6", "1.1e6", "2.0000001e6", "2.1e6", "3.0000001e6", "3.1e6"})
tests = appendIntegerTests(tests, Other, []string{"2~17", "100", "1000", "10000", "100000", "1000", "2000", "3000", "4000", "5000", "6000"})
tests = appendDecimalTests(tests, Other, []string{"2.0~3.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0", "1.0001e3", "1.1e3", "2.0001e3", "2.1e3", "3.0001e3", "3.1e3"})
locales := []string{"pt"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestCaItLldPt_ptScnVec(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, One, []string{"1"})
tests = appendIntegerTests(tests, Many, []string{"1000000", "1000000", "2000000", "3000000", "4000000", "5000000", "6000000"})
tests = appendDecimalTests(tests, Many, []string{"1.0000001e6", "1.1e6", "2.0000001e6", "2.1e6", "3.0000001e6", "3.1e6"})
tests = appendIntegerTests(tests, Other, []string{"0", "2~16", "100", "1000", "10000", "100000", "1000", "2000", "3000", "4000", "5000", "6000"})
tests = appendDecimalTests(tests, Other, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0", "1.0001e3", "1.1e3", "2.0001e3", "2.1e3", "3.0001e3", "3.1e3"})
locales := []string{"ca", "it", "lld", "pt_PT", "scn", "vec"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestEs(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, One, []string{"1"})
tests = appendDecimalTests(tests, One, []string{"1.0", "1.00", "1.000", "1.0000"})
tests = appendIntegerTests(tests, Many, []string{"1000000", "1000000", "2000000", "3000000", "4000000", "5000000", "6000000"})
tests = appendDecimalTests(tests, Many, []string{"1.0000001e6", "1.1e6", "2.0000001e6", "2.1e6", "3.0000001e6", "3.1e6"})
tests = appendIntegerTests(tests, Other, []string{"0", "2~16", "100", "1000", "10000", "100000", "1000", "2000", "3000", "4000", "5000", "6000"})
tests = appendDecimalTests(tests, Other, []string{"0.0~0.9", "1.1~1.6", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0", "1.0001e3", "1.1e3", "2.0001e3", "2.1e3", "3.0001e3", "3.1e3"})
locales := []string{"es"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestGd(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, One, []string{"1", "11"})
tests = appendDecimalTests(tests, One, []string{"1.0", "11.0", "1.00", "11.00", "1.000", "11.000", "1.0000"})
tests = appendIntegerTests(tests, Two, []string{"2", "12"})
tests = appendDecimalTests(tests, Two, []string{"2.0", "12.0", "2.00", "12.00", "2.000", "12.000", "2.0000"})
tests = appendIntegerTests(tests, Few, []string{"3~10", "13~19"})
tests = appendDecimalTests(tests, Few, []string{"3.0", "4.0", "5.0", "6.0", "7.0", "8.0", "9.0", "10.0", "13.0", "14.0", "15.0", "16.0", "17.0", "18.0", "19.0", "3.00"})
tests = appendIntegerTests(tests, Other, []string{"0", "20~34", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.0~0.9", "1.1~1.6", "10.1", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"gd"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestSl(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, One, []string{"1", "101", "201", "301", "401", "501", "601", "701", "1001"})
tests = appendIntegerTests(tests, Two, []string{"2", "102", "202", "302", "402", "502", "602", "702", "1002"})
tests = appendIntegerTests(tests, Few, []string{"3", "4", "103", "104", "203", "204", "303", "304", "403", "404", "503", "504", "603", "604", "703", "704", "1003"})
tests = appendDecimalTests(tests, Few, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
tests = appendIntegerTests(tests, Other, []string{"0", "5~19", "100", "1000", "10000", "100000", "1000000"})
locales := []string{"sl"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestDsbHsb(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, One, []string{"1", "101", "201", "301", "401", "501", "601", "701", "1001"})
tests = appendDecimalTests(tests, One, []string{"0.1", "1.1", "2.1", "3.1", "4.1", "5.1", "6.1", "7.1", "10.1", "100.1", "1000.1"})
tests = appendIntegerTests(tests, Two, []string{"2", "102", "202", "302", "402", "502", "602", "702", "1002"})
tests = appendDecimalTests(tests, Two, []string{"0.2", "1.2", "2.2", "3.2", "4.2", "5.2", "6.2", "7.2", "10.2", "100.2", "1000.2"})
tests = appendIntegerTests(tests, Few, []string{"3", "4", "103", "104", "203", "204", "303", "304", "403", "404", "503", "504", "603", "604", "703", "704", "1003"})
tests = appendDecimalTests(tests, Few, []string{"0.3", "0.4", "1.3", "1.4", "2.3", "2.4", "3.3", "3.4", "4.3", "4.4", "5.3", "5.4", "6.3", "6.4", "7.3", "7.4", "10.3", "100.3", "1000.3"})
tests = appendIntegerTests(tests, Other, []string{"0", "5~19", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.0", "0.5~1.0", "1.5~2.0", "2.5~2.7", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"dsb", "hsb"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestCsSk(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, One, []string{"1"})
tests = appendIntegerTests(tests, Few, []string{"2~4"})
tests = appendDecimalTests(tests, Many, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
tests = appendIntegerTests(tests, Other, []string{"0", "5~19", "100", "1000", "10000", "100000", "1000000"})
locales := []string{"cs", "sk"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestPl(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, One, []string{"1"})
tests = appendIntegerTests(tests, Few, []string{"2~4", "22~24", "32~34", "42~44", "52~54", "62", "102", "1002"})
tests = appendIntegerTests(tests, Many, []string{"0", "5~19", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"pl"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestBe(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, One, []string{"1", "21", "31", "41", "51", "61", "71", "81", "101", "1001"})
tests = appendDecimalTests(tests, One, []string{"1.0", "21.0", "31.0", "41.0", "51.0", "61.0", "71.0", "81.0", "101.0", "1001.0"})
tests = appendIntegerTests(tests, Few, []string{"2~4", "22~24", "32~34", "42~44", "52~54", "62", "102", "1002"})
tests = appendDecimalTests(tests, Few, []string{"2.0", "3.0", "4.0", "22.0", "23.0", "24.0", "32.0", "33.0", "102.0", "1002.0"})
tests = appendIntegerTests(tests, Many, []string{"0", "5~19", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Many, []string{"0.0", "5.0", "6.0", "7.0", "8.0", "9.0", "10.0", "11.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
tests = appendDecimalTests(tests, Other, []string{"0.1~0.9", "1.1~1.7", "10.1", "100.1", "1000.1"})
locales := []string{"be"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestLt(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, One, []string{"1", "21", "31", "41", "51", "61", "71", "81", "101", "1001"})
tests = appendDecimalTests(tests, One, []string{"1.0", "21.0", "31.0", "41.0", "51.0", "61.0", "71.0", "81.0", "101.0", "1001.0"})
tests = appendIntegerTests(tests, Few, []string{"2~9", "22~29", "102", "1002"})
tests = appendDecimalTests(tests, Few, []string{"2.0", "3.0", "4.0", "5.0", "6.0", "7.0", "8.0", "9.0", "22.0", "102.0", "1002.0"})
tests = appendDecimalTests(tests, Many, []string{"0.1~0.9", "1.1~1.7", "10.1", "100.1", "1000.1"})
tests = appendIntegerTests(tests, Other, []string{"0", "10~20", "30", "40", "50", "60", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.0", "10.0", "11.0", "12.0", "13.0", "14.0", "15.0", "16.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"lt"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestRuUk(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, One, []string{"1", "21", "31", "41", "51", "61", "71", "81", "101", "1001"})
tests = appendIntegerTests(tests, Few, []string{"2~4", "22~24", "32~34", "42~44", "52~54", "62", "102", "1002"})
tests = appendIntegerTests(tests, Many, []string{"0", "5~19", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"ru", "uk"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestBr(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, One, []string{"1", "21", "31", "41", "51", "61", "81", "101", "1001"})
tests = appendDecimalTests(tests, One, []string{"1.0", "21.0", "31.0", "41.0", "51.0", "61.0", "81.0", "101.0", "1001.0"})
tests = appendIntegerTests(tests, Two, []string{"2", "22", "32", "42", "52", "62", "82", "102", "1002"})
tests = appendDecimalTests(tests, Two, []string{"2.0", "22.0", "32.0", "42.0", "52.0", "62.0", "82.0", "102.0", "1002.0"})
tests = appendIntegerTests(tests, Few, []string{"3", "4", "9", "23", "24", "29", "33", "34", "39", "43", "44", "49", "103", "1003"})
tests = appendDecimalTests(tests, Few, []string{"3.0", "4.0", "9.0", "23.0", "24.0", "29.0", "33.0", "34.0", "103.0", "1003.0"})
tests = appendIntegerTests(tests, Many, []string{"1000000"})
tests = appendDecimalTests(tests, Many, []string{"1000000.0", "1000000.00", "1000000.000", "1000000.0000"})
tests = appendIntegerTests(tests, Other, []string{"0", "5~8", "10~20", "100", "1000", "10000", "100000"})
tests = appendDecimalTests(tests, Other, []string{"0.0~0.9", "1.1~1.6", "10.0", "100.0", "1000.0", "10000.0", "100000.0"})
locales := []string{"br"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestMt(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, One, []string{"1"})
tests = appendDecimalTests(tests, One, []string{"1.0", "1.00", "1.000", "1.0000"})
tests = appendIntegerTests(tests, Two, []string{"2"})
tests = appendDecimalTests(tests, Two, []string{"2.0", "2.00", "2.000", "2.0000"})
tests = appendIntegerTests(tests, Few, []string{"0", "3~10", "103~109", "1003"})
tests = appendDecimalTests(tests, Few, []string{"0.0", "3.0", "4.0", "5.0", "6.0", "7.0", "8.0", "9.0", "10.0", "103.0", "1003.0"})
tests = appendIntegerTests(tests, Many, []string{"11~19", "111~117", "1011"})
tests = appendDecimalTests(tests, Many, []string{"11.0", "12.0", "13.0", "14.0", "15.0", "16.0", "17.0", "18.0", "111.0", "1011.0"})
tests = appendIntegerTests(tests, Other, []string{"20~35", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.1~0.9", "1.1~1.7", "10.1", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"mt"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestGa(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, One, []string{"1"})
tests = appendDecimalTests(tests, One, []string{"1.0", "1.00", "1.000", "1.0000"})
tests = appendIntegerTests(tests, Two, []string{"2"})
tests = appendDecimalTests(tests, Two, []string{"2.0", "2.00", "2.000", "2.0000"})
tests = appendIntegerTests(tests, Few, []string{"3~6"})
tests = appendDecimalTests(tests, Few, []string{"3.0", "4.0", "5.0", "6.0", "3.00", "4.00", "5.00", "6.00", "3.000", "4.000", "5.000", "6.000", "3.0000", "4.0000", "5.0000", "6.0000"})
tests = appendIntegerTests(tests, Many, []string{"7~10"})
tests = appendDecimalTests(tests, Many, []string{"7.0", "8.0", "9.0", "10.0", "7.00", "8.00", "9.00", "10.00", "7.000", "8.000", "9.000", "10.000", "7.0000", "8.0000", "9.0000", "10.0000"})
tests = appendIntegerTests(tests, Other, []string{"0", "11~25", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.0~0.9", "1.1~1.6", "10.1", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"ga"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestGv(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, One, []string{"1", "11", "21", "31", "41", "51", "61", "71", "101", "1001"})
tests = appendIntegerTests(tests, Two, []string{"2", "12", "22", "32", "42", "52", "62", "72", "102", "1002"})
tests = appendIntegerTests(tests, Few, []string{"0", "20", "40", "60", "80", "100", "120", "140", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Many, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
tests = appendIntegerTests(tests, Other, []string{"3~10", "13~19", "23", "103", "1003"})
locales := []string{"gv"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestKw(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, Zero, []string{"0"})
tests = appendDecimalTests(tests, Zero, []string{"0.0", "0.00", "0.000", "0.0000"})
tests = appendIntegerTests(tests, One, []string{"1"})
tests = appendDecimalTests(tests, One, []string{"1.0", "1.00", "1.000", "1.0000"})
tests = appendIntegerTests(tests, Two, []string{"2", "22", "42", "62", "82", "102", "122", "142", "1000", "10000", "100000"})
tests = appendDecimalTests(tests, Two, []string{"2.0", "22.0", "42.0", "62.0", "82.0", "102.0", "122.0", "142.0", "1000.0", "10000.0", "100000.0"})
tests = appendIntegerTests(tests, Few, []string{"3", "23", "43", "63", "83", "103", "123", "143", "1003"})
tests = appendDecimalTests(tests, Few, []string{"3.0", "23.0", "43.0", "63.0", "83.0", "103.0", "123.0", "143.0", "1003.0"})
tests = appendIntegerTests(tests, Many, []string{"21", "41", "61", "81", "101", "121", "141", "161", "1001"})
tests = appendDecimalTests(tests, Many, []string{"21.0", "41.0", "61.0", "81.0", "101.0", "121.0", "141.0", "161.0", "1001.0"})
tests = appendIntegerTests(tests, Other, []string{"4~19", "100", "1004", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.1~0.9", "1.1~1.7", "10.0", "100.0", "1000.1", "1000000.0"})
locales := []string{"kw"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestArArs(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, Zero, []string{"0"})
tests = appendDecimalTests(tests, Zero, []string{"0.0", "0.00", "0.000", "0.0000"})
tests = appendIntegerTests(tests, One, []string{"1"})
tests = appendDecimalTests(tests, One, []string{"1.0", "1.00", "1.000", "1.0000"})
tests = appendIntegerTests(tests, Two, []string{"2"})
tests = appendDecimalTests(tests, Two, []string{"2.0", "2.00", "2.000", "2.0000"})
tests = appendIntegerTests(tests, Few, []string{"3~10", "103~110", "1003"})
tests = appendDecimalTests(tests, Few, []string{"3.0", "4.0", "5.0", "6.0", "7.0", "8.0", "9.0", "10.0", "103.0", "1003.0"})
tests = appendIntegerTests(tests, Many, []string{"11~26", "111", "1011"})
tests = appendDecimalTests(tests, Many, []string{"11.0", "12.0", "13.0", "14.0", "15.0", "16.0", "17.0", "18.0", "111.0", "1011.0"})
tests = appendIntegerTests(tests, Other, []string{"100~102", "200~202", "300~302", "400~402", "500~502", "600", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.1~0.9", "1.1~1.7", "10.1", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"ar", "ars"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestCy(t *testing.T) {
var tests []pluralFormTest
tests = appendIntegerTests(tests, Zero, []string{"0"})
tests = appendDecimalTests(tests, Zero, []string{"0.0", "0.00", "0.000", "0.0000"})
tests = appendIntegerTests(tests, One, []string{"1"})
tests = appendDecimalTests(tests, One, []string{"1.0", "1.00", "1.000", "1.0000"})
tests = appendIntegerTests(tests, Two, []string{"2"})
tests = appendDecimalTests(tests, Two, []string{"2.0", "2.00", "2.000", "2.0000"})
tests = appendIntegerTests(tests, Few, []string{"3"})
tests = appendDecimalTests(tests, Few, []string{"3.0", "3.00", "3.000", "3.0000"})
tests = appendIntegerTests(tests, Many, []string{"6"})
tests = appendDecimalTests(tests, Many, []string{"6.0", "6.00", "6.000", "6.0000"})
tests = appendIntegerTests(tests, Other, []string{"4", "5", "7~20", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.1~0.9", "1.1~1.7", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"cy"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
go-i18n-2.6.0/internal/plural/rule_test.go 0000664 0000000 0000000 00000004027 14774252036 0020366 0 ustar 00root root 0000000 0000000 package plural
import (
"strconv"
"strings"
"testing"
"golang.org/x/text/language"
)
type pluralFormTest struct {
num interface{}
form Form
}
func runTests(t *testing.T, pluralRuleID string, tests []pluralFormTest) {
if pluralRuleID == "root" {
return
}
pluralRules := DefaultRules()
tag := language.MustParse(pluralRuleID)
if rule := pluralRules.Rule(tag); rule != nil {
for _, test := range tests {
ops, err := NewOperands(test.num)
if err != nil {
t.Errorf("%s: NewOperands(%d) errored with %s", pluralRuleID, test.num, err)
break
}
if pluralForm := rule.PluralFormFunc(ops); pluralForm != test.form {
t.Errorf("%s: PluralFormFunc(%#v) returned %q, %v; expected %q", pluralRuleID, ops, pluralForm, err, test.form)
}
}
} else {
t.Errorf("could not find plural rule for locale %s", pluralRuleID)
}
}
func appendIntegerTests(tests []pluralFormTest, form Form, examples []string) []pluralFormTest {
for _, ex := range expandExamples(examples) {
i, err := strconv.ParseInt(ex, 10, 64)
if err != nil {
panic(err)
}
tests = append(tests, pluralFormTest{ex, form}, pluralFormTest{i, form})
}
return tests
}
func appendDecimalTests(tests []pluralFormTest, form Form, examples []string) []pluralFormTest {
for _, ex := range expandExamples(examples) {
tests = append(tests, pluralFormTest{ex, form})
}
return tests
}
func expandExamples(examples []string) []string {
var expanded []string
for _, ex := range examples {
if parts := strings.Split(ex, "~"); len(parts) == 2 {
for ex := parts[0]; ; ex = increment(ex) {
expanded = append(expanded, ex)
if ex == parts[1] {
break
}
}
} else {
expanded = append(expanded, ex)
}
}
return expanded
}
func increment(dec string) string {
runes := []rune(dec)
carry := true
for i := len(runes) - 1; carry && i >= 0; i-- {
switch runes[i] {
case '.':
continue
case '9':
runes[i] = '0'
default:
runes[i]++
carry = false
}
}
if carry {
runes = append([]rune{'1'}, runes...)
}
return string(runes)
}
go-i18n-2.6.0/internal/plural/rules.go 0000664 0000000 0000000 00000000765 14774252036 0017517 0 ustar 00root root 0000000 0000000 package plural
import "golang.org/x/text/language"
// Rules is a set of plural rules by language tag.
type Rules map[language.Tag]*Rule
// Rule returns the closest matching plural rule for the language tag
// or nil if no rule could be found.
func (r Rules) Rule(tag language.Tag) *Rule {
t := tag
for {
if rule := r[t]; rule != nil {
return rule
}
t = t.Parent()
if t.IsRoot() {
break
}
}
base, _ := tag.Base()
baseTag, _ := language.Parse(base.String())
return r[baseTag]
}
go-i18n-2.6.0/internal/plural/rules_test.go 0000664 0000000 0000000 00000002627 14774252036 0020555 0 ustar 00root root 0000000 0000000 package plural
import (
"testing"
"golang.org/x/text/language"
)
func TestRules(t *testing.T) {
expectedRule := &Rule{}
testCases := []struct {
name string
rules Rules
tag language.Tag
rule *Rule
}{
{
name: "exact match",
rules: Rules{
language.English: expectedRule,
language.Spanish: &Rule{},
},
tag: language.English,
rule: expectedRule,
},
{
name: "inexact match",
rules: Rules{
language.English: expectedRule,
},
tag: language.AmericanEnglish,
rule: expectedRule,
},
{
name: "portuguese doesn't match european portuguese",
rules: Rules{
language.EuropeanPortuguese: &Rule{},
},
tag: language.Portuguese,
rule: nil,
},
{
name: "european portuguese preferred",
rules: Rules{
language.Portuguese: &Rule{},
language.EuropeanPortuguese: expectedRule,
},
tag: language.EuropeanPortuguese,
rule: expectedRule,
},
{
name: "zh-Hans",
rules: Rules{
language.Chinese: expectedRule,
},
tag: language.SimplifiedChinese,
rule: expectedRule,
},
{
name: "zh-Hant",
rules: Rules{
language.Chinese: expectedRule,
},
tag: language.TraditionalChinese,
rule: expectedRule,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
if rule := testCase.rules.Rule(testCase.tag); rule != testCase.rule {
panic(rule)
}
})
}
}
go-i18n-2.6.0/internal/template.go 0000664 0000000 0000000 00000001472 14774252036 0016675 0 ustar 00root root 0000000 0000000 package internal
import (
"sync"
"github.com/nicksnyder/go-i18n/v2/i18n/template"
)
// Template stores the template for a string and a cached version of the parsed template if they are cacheable.
type Template struct {
Src string
LeftDelim string
RightDelim string
parseOnce sync.Once
parsedTemplate template.ParsedTemplate
parseError error
}
func (t *Template) Execute(parser template.Parser, data interface{}) (string, error) {
var pt template.ParsedTemplate
var err error
if parser.Cacheable() {
t.parseOnce.Do(func() {
t.parsedTemplate, t.parseError = parser.Parse(t.Src, t.LeftDelim, t.RightDelim)
})
pt, err = t.parsedTemplate, t.parseError
} else {
pt, err = parser.Parse(t.Src, t.LeftDelim, t.RightDelim)
}
if err != nil {
return "", err
}
return pt.Execute(data)
}
go-i18n-2.6.0/internal/template_test.go 0000664 0000000 0000000 00000003170 14774252036 0017731 0 ustar 00root root 0000000 0000000 package internal
import (
"strings"
"testing"
texttemplate "text/template"
"github.com/nicksnyder/go-i18n/v2/i18n/template"
)
func TestExecute(t *testing.T) {
tests := []struct {
template *Template
parser template.Parser
data interface{}
result string
err string
noallocs bool
}{
{
template: &Template{
Src: "hello",
},
result: "hello",
noallocs: true,
},
{
template: &Template{
Src: "hello {{.Noun}}",
},
data: map[string]string{
"Noun": "world",
},
result: "hello world",
},
{
template: &Template{
Src: "hello {{world}}",
},
parser: &template.TextParser{
Funcs: texttemplate.FuncMap{
"world": func() string {
return "world"
},
},
},
result: "hello world",
},
{
template: &Template{
Src: "hello {{",
},
err: "unclosed action",
noallocs: true,
},
}
for _, test := range tests {
t.Run(test.template.Src, func(t *testing.T) {
if test.parser == nil {
test.parser = &template.TextParser{}
}
result, err := test.template.Execute(test.parser, test.data)
if actual := str(err); !strings.Contains(str(err), test.err) {
t.Errorf("expected err %q to contain %q", actual, test.err)
}
if result != test.result {
t.Errorf("expected result %q; got %q", test.result, result)
}
allocs := testing.AllocsPerRun(10, func() {
_, _ = test.template.Execute(test.parser, test.data)
})
if test.noallocs && allocs > 0 {
t.Errorf("expected no allocations; got %f", allocs)
}
})
}
}
func str(err error) string {
if err == nil {
return ""
}
return err.Error()
}