├── testfiles ├── test1.txt ├── test2.txt ├── incorrect.json ├── incorrect.yaml ├── test1.yaml ├── test2.yaml ├── test1.json ├── test2.json └── README.md ├── .vscode ├── extensions.json └── settings.json ├── examples ├── 06-genders │ ├── README.md │ └── main.go ├── 01-basic-usage │ ├── README.md │ └── main.go ├── 07-pluralized-genders │ ├── README.md │ └── main.go ├── 02-variable-interpolation │ ├── README.md │ └── main.go ├── 04-default-pluralization │ ├── README.md │ └── main.go ├── 05-custom-pluralization │ ├── README.md │ └── main.go ├── 03-json-yaml-loaders │ ├── 01-from-strings │ │ ├── README.md │ │ └── main.go │ ├── 02-from-files │ │ ├── README.md │ │ ├── es │ │ │ ├── translations-02.yaml │ │ │ └── translations-01.yaml │ │ ├── en │ │ │ ├── translations-02.json │ │ │ └── translations-01.json │ │ └── main.go │ ├── 03-from-embed-fs │ │ ├── README.md │ │ ├── es │ │ │ ├── translations-02.yaml │ │ │ └── translations-01.yaml │ │ ├── en │ │ │ ├── translations-02.json │ │ │ └── translations-01.json │ │ └── main.go │ └── README.md ├── 09-advanced-example │ ├── .vscode │ │ └── settings.json │ ├── go.mod │ ├── README.md │ ├── go.sum │ ├── translations │ │ ├── en.yaml │ │ ├── pt.yaml │ │ ├── es.yaml │ │ └── fr.yaml │ ├── template.go │ ├── i18n.go │ ├── main.go │ └── views │ │ └── index.html └── 08-templating │ ├── go.mod │ ├── go.sum │ ├── README.md │ ├── i18n.go │ └── main.go ├── assets └── i18n-gopher.png ├── go.mod ├── CONTRIBUTORS.md ├── Taskfile.yml ├── read_file_from_fs.go ├── go.sum ├── execute_template.go ├── .gitignore ├── .github └── workflows │ └── tests.yml ├── execute_template_test.go ├── LICENSE ├── read_file_from_fs_test.go ├── types.go ├── CONTRIBUTING.md ├── loader_json.go ├── loader_yaml.go ├── loader_yaml_test.go ├── loader_json_test.go ├── README.md ├── i18n.go └── i18n_test.go /testfiles/test1.txt: -------------------------------------------------------------------------------- 1 | test 1 -------------------------------------------------------------------------------- /testfiles/test2.txt: -------------------------------------------------------------------------------- 1 | test 2 -------------------------------------------------------------------------------- /testfiles/incorrect.json: -------------------------------------------------------------------------------- 1 | incorrect json -------------------------------------------------------------------------------- /testfiles/incorrect.yaml: -------------------------------------------------------------------------------- 1 | incorrect yaml -------------------------------------------------------------------------------- /testfiles/test1.yaml: -------------------------------------------------------------------------------- 1 | - Key: hello 2 | Default: Hello -------------------------------------------------------------------------------- /testfiles/test2.yaml: -------------------------------------------------------------------------------- 1 | - Key: world 2 | Default: World -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["golang.go"] 3 | } -------------------------------------------------------------------------------- /examples/06-genders/README.md: -------------------------------------------------------------------------------- 1 | # Go Easy i18n 2 | 3 | Open `main.go` and follow the comments. 4 | -------------------------------------------------------------------------------- /testfiles/test1.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Key": "hello", 4 | "Default": "Hello" 5 | } 6 | ] -------------------------------------------------------------------------------- /testfiles/test2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Key": "world", 4 | "Default": "World" 5 | } 6 | ] -------------------------------------------------------------------------------- /assets/i18n-gopher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eduardolat/goeasyi18n/HEAD/assets/i18n-gopher.png -------------------------------------------------------------------------------- /examples/01-basic-usage/README.md: -------------------------------------------------------------------------------- 1 | # Go Easy i18n 2 | 3 | Open `main.go` and follow the comments. 4 | -------------------------------------------------------------------------------- /examples/07-pluralized-genders/README.md: -------------------------------------------------------------------------------- 1 | # Go Easy i18n 2 | 3 | Open `main.go` and follow the comments. 4 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/eduardolat/goeasyi18n 2 | 3 | go 1.19 4 | 5 | require gopkg.in/yaml.v3 v3.0.1 6 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | 3 | - [Eduardo Lat](https://github.com/eduardolat) - Main contributor. 4 | -------------------------------------------------------------------------------- /examples/02-variable-interpolation/README.md: -------------------------------------------------------------------------------- 1 | # Go Easy i18n 2 | 3 | Open `main.go` and follow the comments. 4 | -------------------------------------------------------------------------------- /examples/04-default-pluralization/README.md: -------------------------------------------------------------------------------- 1 | # Go Easy i18n 2 | 3 | Open `main.go` and follow the comments. 4 | -------------------------------------------------------------------------------- /examples/05-custom-pluralization/README.md: -------------------------------------------------------------------------------- 1 | # Go Easy i18n 2 | 3 | Open `main.go` and follow the comments. 4 | -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | tasks: 4 | test: 5 | cmd: go test . 6 | 7 | tidy: 8 | cmd: go mod tidy 9 | -------------------------------------------------------------------------------- /examples/03-json-yaml-loaders/01-from-strings/README.md: -------------------------------------------------------------------------------- 1 | # Go Easy i18n 2 | 3 | Open `main.go` and follow the comments. 4 | -------------------------------------------------------------------------------- /examples/03-json-yaml-loaders/02-from-files/README.md: -------------------------------------------------------------------------------- 1 | # Go Easy i18n 2 | 3 | Open `main.go` and follow the comments. 4 | -------------------------------------------------------------------------------- /examples/03-json-yaml-loaders/03-from-embed-fs/README.md: -------------------------------------------------------------------------------- 1 | # Go Easy i18n 2 | 3 | Open `main.go` and follow the comments. 4 | -------------------------------------------------------------------------------- /examples/09-advanced-example/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[html]": { 3 | "editor.formatOnSave": false 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /testfiles/README.md: -------------------------------------------------------------------------------- 1 | # Go Easy i18n Test Files 2 | 3 | On this folder you can find the test helper files for the Go Easy i18n package. 4 | -------------------------------------------------------------------------------- /examples/03-json-yaml-loaders/02-from-files/es/translations-02.yaml: -------------------------------------------------------------------------------- 1 | - Key: hello_admin 2 | Default: Hola {{.Name}}, eres un administrador 3 | -------------------------------------------------------------------------------- /examples/03-json-yaml-loaders/03-from-embed-fs/es/translations-02.yaml: -------------------------------------------------------------------------------- 1 | - Key: hello_admin 2 | Default: Hola {{.Name}}, eres un administrador, desde embed 3 | -------------------------------------------------------------------------------- /examples/03-json-yaml-loaders/02-from-files/es/translations-01.yaml: -------------------------------------------------------------------------------- 1 | - Key: hello_world 2 | Default: Hola Mundo 3 | 4 | - Key: hello_user 5 | Default: Hola {{.Name}} 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": false, 3 | 4 | "[go]": { 5 | "editor.formatOnSave": true, 6 | "editor.defaultFormatter": "golang.go" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/03-json-yaml-loaders/02-from-files/en/translations-02.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Key": "hello_admin", 4 | "Default": "Hello {{.Name}}, you are an admin" 5 | } 6 | ] 7 | -------------------------------------------------------------------------------- /examples/03-json-yaml-loaders/03-from-embed-fs/en/translations-02.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Key": "hello_admin", 4 | "Default": "Hello {{.Name}}, you are an admin, from embed" 5 | } 6 | ] 7 | -------------------------------------------------------------------------------- /examples/03-json-yaml-loaders/03-from-embed-fs/es/translations-01.yaml: -------------------------------------------------------------------------------- 1 | - Key: hello_world 2 | Default: Hola Mundo desde embed 3 | 4 | - Key: hello_user 5 | Default: Hola {{.Name}} desde embed 6 | -------------------------------------------------------------------------------- /examples/03-json-yaml-loaders/02-from-files/en/translations-01.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Key": "hello_world", 4 | "Default": "Hello World" 5 | }, 6 | { 7 | "Key": "hello_user", 8 | "Default": "Hello {{.Name}}" 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /examples/03-json-yaml-loaders/03-from-embed-fs/en/translations-01.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Key": "hello_world", 4 | "Default": "Hello World from embed" 5 | }, 6 | { 7 | "Key": "hello_user", 8 | "Default": "Hello {{.Name}} from embed" 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /examples/08-templating/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/eduardolat/goeasyi18n/examples/templating 2 | 3 | go 1.21.0 4 | 5 | require "github.com/eduardolat/goeasyi18n" v0.0.0 6 | replace "github.com/eduardolat/goeasyi18n" v0.0.0 => "../.." 7 | 8 | require gopkg.in/yaml.v3 v3.0.1 // indirect 9 | -------------------------------------------------------------------------------- /examples/09-advanced-example/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/eduardolat/goeasyi18n/examples/advanced 2 | 3 | go 1.21.0 4 | 5 | require "github.com/eduardolat/goeasyi18n" v0.0.0 6 | replace "github.com/eduardolat/goeasyi18n" v0.0.0 => "../.." 7 | 8 | require gopkg.in/yaml.v3 v3.0.1 // indirect 9 | -------------------------------------------------------------------------------- /read_file_from_fs.go: -------------------------------------------------------------------------------- 1 | package goeasyi18n 2 | 3 | import ( 4 | "io" 5 | "io/fs" 6 | ) 7 | 8 | func readFileFromFS(filesystem fs.FS, file string) ([]byte, error) { 9 | fileData, err := filesystem.Open(file) 10 | if err != nil { 11 | return nil, err 12 | } 13 | defer fileData.Close() 14 | 15 | return io.ReadAll(fileData) 16 | } 17 | -------------------------------------------------------------------------------- /examples/09-advanced-example/README.md: -------------------------------------------------------------------------------- 1 | # Go Easy i18n - Advanced example 2 | 3 | In this example we will see how to use all the features of the library combined to build a full translated website using only the standard library and Go Easy i18n. 4 | 5 | Feel free to run this example and play with it. 6 | 7 | Start from `main.go` and then explore the other files. -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 2 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 3 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 4 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 5 | -------------------------------------------------------------------------------- /examples/08-templating/go.sum: -------------------------------------------------------------------------------- 1 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 2 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 3 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 4 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 5 | -------------------------------------------------------------------------------- /examples/09-advanced-example/go.sum: -------------------------------------------------------------------------------- 1 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 2 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 3 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 4 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 5 | -------------------------------------------------------------------------------- /execute_template.go: -------------------------------------------------------------------------------- 1 | package goeasyi18n 2 | 3 | import ( 4 | "bytes" 5 | "html/template" 6 | ) 7 | 8 | func ExecuteTemplate(templateStr string, data any) string { 9 | tmpl := template.Must(template.New("template").Parse(templateStr)) 10 | 11 | b := new(bytes.Buffer) 12 | 13 | err := tmpl.Execute(b, data) 14 | if err != nil { 15 | return "" 16 | } 17 | 18 | return b.String() 19 | } 20 | -------------------------------------------------------------------------------- /examples/09-advanced-example/translations/en.yaml: -------------------------------------------------------------------------------- 1 | - Key: hello_world 2 | Default: Hello World 3 | 4 | - Key: hello_name 5 | Default: Hello {{.Name}} 6 | 7 | - Key: unread_emails 8 | One: You have one unread email. 9 | Many: You have {{.Count}} unread emails. 10 | 11 | - Key: friend_unread_emails 12 | OneMale: He ({{.Name}}) sent you an email. 13 | OneFemale: She ({{.Name}}) sent you an email. 14 | ManyMale: He ({{.Name}}) sent you {{.Count}} emails. 15 | ManyFemale: She ({{.Name}}) sent you {{.Count}} emails. 16 | -------------------------------------------------------------------------------- /examples/09-advanced-example/translations/pt.yaml: -------------------------------------------------------------------------------- 1 | - Key: hello_world 2 | Default: Olá Mundo 3 | 4 | - Key: hello_name 5 | Default: Olá {{.Name}} 6 | 7 | - Key: unread_emails 8 | One: Você tem um e-mail não lido. 9 | Many: Você tem {{.Count}} e-mails não lidos. 10 | 11 | - Key: friend_unread_emails 12 | OneMale: Ele ({{.Name}}) te enviou um e-mail. 13 | OneFemale: Ela ({{.Name}}) te enviou um e-mail. 14 | ManyMale: Ele ({{.Name}}) te enviou {{.Count}} e-mails. 15 | ManyFemale: Ela ({{.Name}}) te enviou {{.Count}} e-mails. 16 | -------------------------------------------------------------------------------- /examples/09-advanced-example/translations/es.yaml: -------------------------------------------------------------------------------- 1 | - Key: hello_world 2 | Default: Hola Mundo 3 | 4 | - Key: hello_name 5 | Default: Hola {{.Name}} 6 | 7 | - Key: unread_emails 8 | One: Tienes un correo sin leer. 9 | Many: Tienes {{.Count}} correos sin leer. 10 | 11 | - Key: friend_unread_emails 12 | OneMale: Él ({{.Name}}) te ha enviado un correo. 13 | OneFemale: Ella ({{.Name}}) te ha enviado un correo. 14 | ManyMale: Él ({{.Name}}) te ha enviado {{.Count}} correos. 15 | ManyFemale: Ella ({{.Name}}) te ha enviado {{.Count}} correos. 16 | -------------------------------------------------------------------------------- /examples/09-advanced-example/translations/fr.yaml: -------------------------------------------------------------------------------- 1 | - Key: hello_world 2 | Default: Bonjour Monde 3 | 4 | - Key: hello_name 5 | Default: Bonjour {{.Name}} 6 | 7 | - Key: unread_emails 8 | One: Vous avez un courrier non lu. 9 | Many: Vous avez {{.Count}} courriers non lus. 10 | 11 | - Key: friend_unread_emails 12 | OneMale: Il ({{.Name}}) vous a envoyé un courrier. 13 | OneFemale: Elle ({{.Name}}) vous a envoyé un courrier. 14 | ManyMale: Il ({{.Name}}) vous a envoyé {{.Count}} courriers. 15 | ManyFemale: Elle ({{.Name}}) vous a envoyé {{.Count}} courriers. 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | go-version: ['1.19.x', '1.20.x', '1.21.x'] 14 | platform: [ubuntu-latest] 15 | 16 | runs-on: ${{ matrix.platform }} 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | 21 | - name: Set up Go 22 | uses: actions/setup-go@v4 23 | with: 24 | go-version: ${{ matrix.go-version }} 25 | 26 | - name: Run tests 27 | run: go test -v . 28 | -------------------------------------------------------------------------------- /examples/08-templating/README.md: -------------------------------------------------------------------------------- 1 | # Go Easy i18n - Templating example 2 | 3 | The templating feature allows you to use the i18n instance in your `text/template` and `html/template` templates with ease 🔥. 4 | 5 | This is really useful if you want to use the i18n instance in your web application that uses `html/template` templates. 6 | 7 | ## The example 8 | 9 | This example is divided in 2 files: 10 | 11 | - [main.go](main.go): Here is the code of the templating example. 12 | 13 | - [i18n.go](i18n.go): Here is the code of the i18n instance (If you read the other examples you should be familiar with this). 14 | 15 | Just run the program using `go run .` and you will see the output of the example. 16 | 17 | Feel free to play with the code and explore the example. 18 | -------------------------------------------------------------------------------- /examples/09-advanced-example/template.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "html/template" 6 | "os" 7 | ) 8 | 9 | func ExecuteTemplate(templatePath string, data any) (string, error) { 10 | 11 | // Prepare func map 12 | translateFunc := i18n.NewTemplatingTranslateFunc() 13 | funcs := template.FuncMap{ 14 | "Translate": translateFunc, // You can use any name you want, for example: "T" 15 | } 16 | 17 | // Read template file 18 | fileContent, err := os.ReadFile(templatePath) 19 | if err != nil { 20 | return "", err 21 | } 22 | 23 | // Execute template 24 | tmpl := template.Must(template.New("test").Funcs(funcs).Parse(string(fileContent))) 25 | result := new(bytes.Buffer) 26 | tmpl.Execute(result, data) 27 | 28 | if err != nil { 29 | return "", err 30 | } 31 | 32 | return result.String(), nil 33 | } 34 | -------------------------------------------------------------------------------- /execute_template_test.go: -------------------------------------------------------------------------------- 1 | package goeasyi18n 2 | 3 | import "testing" 4 | 5 | func TestExecuteTemplate(t *testing.T) { 6 | 7 | t.Run("should execute a template", func(t *testing.T) { 8 | executed := ExecuteTemplate("Hello {{.Name}}", struct{ Name string }{Name: "World"}) 9 | expected := "Hello World" 10 | 11 | if executed != expected { 12 | t.Errorf("Expected %s, got %s", expected, executed) 13 | } 14 | }) 15 | 16 | t.Run("should execute a template with two interpolations", func(t *testing.T) { 17 | data := struct{ FirstName, SurName string }{FirstName: "John", SurName: "Doe"} 18 | executed := ExecuteTemplate("Hello {{.FirstName}} {{.SurName}}", data) 19 | expected := "Hello John Doe" 20 | 21 | if executed != expected { 22 | t.Errorf("Expected %s, got %s", expected, executed) 23 | } 24 | }) 25 | 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 eduardolat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/03-json-yaml-loaders/README.md: -------------------------------------------------------------------------------- 1 | # Go Easy i18n 2 | 3 | Go Easy i18n allows you to load your translations in different ways: 4 | 5 | ## Manually 6 | 7 | If you have a small set of translations, you can load them manually as shown in the [Basic Usage Example.](../01-basic-usage/main.go) 8 | 9 | ## From JSON/YAML as Bytes or Strings 10 | 11 | You can fetch your JSON/YAML translations from a database or a remote API and load them into the i18n instance as part of the startup of your program, look at the example to see how to do it: 12 | 13 | [Bytes/String Example](./01-from-strings/main.go) 14 | 15 | ## From JSON/YAML files in your server file system 16 | 17 | You can load your translations from JSON/YAML files in your file system, look at the example to see how to do it: 18 | 19 | [Files Example](./02-from-files/main.go) 20 | 21 | ## From JSON/YAML files in `fs.FS` (`embed.FS`) file system 22 | 23 | If you want to load your translations from JSON/YAML files embedded in your binary or some other implementation of the `fs.FS` interface, look at the example to see how to do it: 24 | 25 | [`embed.FS` Example](./03-from-embed-fs/main.go) 26 | -------------------------------------------------------------------------------- /examples/09-advanced-example/i18n.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | 6 | "github.com/eduardolat/goeasyi18n" 7 | ) 8 | 9 | var i18n *goeasyi18n.I18n 10 | 11 | //go:embed translations 12 | var translationsFS embed.FS 13 | 14 | func InitializeI18n() { 15 | i18n = goeasyi18n.NewI18n() 16 | 17 | enTranslations, err := goeasyi18n.LoadFromYamlFS( 18 | translationsFS, 19 | "translations/en.yaml", 20 | ) 21 | if err != nil { 22 | panic(err) 23 | } 24 | 25 | esTranslations, err := goeasyi18n.LoadFromYamlFS( 26 | translationsFS, 27 | "translations/es.yaml", 28 | ) 29 | if err != nil { 30 | panic(err) 31 | } 32 | 33 | ptTranslations, err := goeasyi18n.LoadFromYamlFS( 34 | translationsFS, 35 | "translations/pt.yaml", 36 | ) 37 | if err != nil { 38 | panic(err) 39 | } 40 | 41 | frTranslations, err := goeasyi18n.LoadFromYamlFS( 42 | translationsFS, 43 | "translations/fr.yaml", 44 | ) 45 | if err != nil { 46 | panic(err) 47 | } 48 | 49 | i18n.AddLanguage("en", enTranslations) 50 | i18n.AddLanguage("es", esTranslations) 51 | i18n.AddLanguage("pt", ptTranslations) 52 | i18n.AddLanguage("fr", frTranslations) 53 | } 54 | -------------------------------------------------------------------------------- /read_file_from_fs_test.go: -------------------------------------------------------------------------------- 1 | package goeasyi18n 2 | 3 | import ( 4 | "embed" 5 | "testing" 6 | ) 7 | 8 | //go:embed testfiles/* 9 | var readFSTestFiles embed.FS 10 | 11 | func TestReadFileFromFS(t *testing.T) { 12 | tests := []struct { 13 | filename string 14 | expectedData string 15 | expectedErr bool 16 | }{ 17 | { 18 | filename: "testfiles/test1.txt", 19 | expectedData: "test 1", 20 | expectedErr: false, 21 | }, 22 | { 23 | filename: "testfiles/test2.txt", 24 | expectedData: "test 2", 25 | expectedErr: false, 26 | }, 27 | { 28 | filename: "testfiles/not-exists.txt", 29 | expectedData: "", 30 | expectedErr: true, 31 | }, 32 | } 33 | 34 | for _, tt := range tests { 35 | t.Run(tt.filename, func(t *testing.T) { 36 | data, err := readFileFromFS(readFSTestFiles, tt.filename) 37 | 38 | if tt.expectedErr { 39 | if err == nil { 40 | t.Errorf("expected error, got nil") 41 | } 42 | } else { 43 | if err != nil { 44 | t.Errorf("didn't expect error, got %v", err) 45 | } 46 | } 47 | 48 | if string(data) != tt.expectedData { 49 | t.Errorf("expected data %q, got %q", tt.expectedData, string(data)) 50 | } 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package goeasyi18n 2 | 3 | type TranslateString struct { 4 | Key string 5 | Default string 6 | 7 | // For pluralization 8 | Zero string // Optional 9 | One string // Optional 10 | Two string // Optional 11 | Few string // Optional 12 | Many string // Optional 13 | 14 | // For genders 15 | Male string // Optional 16 | Female string // Optional 17 | NonBinary string // Optional 18 | 19 | // For pluralization with male gender 20 | ZeroMale string // Optional 21 | OneMale string // Optional 22 | TwoMale string // Optional 23 | FewMale string // Optional 24 | ManyMale string // Optional 25 | 26 | // For pluralization with female gender 27 | ZeroFemale string // Optional 28 | OneFemale string // Optional 29 | TwoFemale string // Optional 30 | FewFemale string // Optional 31 | ManyFemale string // Optional 32 | 33 | // For pluralization with non binary gender 34 | ZeroNonBinary string // Optional 35 | OneNonBinary string // Optional 36 | TwoNonBinary string // Optional 37 | FewNonBinary string // Optional 38 | ManyNonBinary string // Optional 39 | } 40 | 41 | type TranslateStrings []TranslateString 42 | 43 | type PluralizationFunc func(count int) string 44 | 45 | type Data map[string]any 46 | -------------------------------------------------------------------------------- /examples/03-json-yaml-loaders/01-from-strings/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/eduardolat/goeasyi18n" 7 | ) 8 | 9 | func main() { 10 | // 1. Create a new i18n instance 11 | i18n := goeasyi18n.NewI18n() 12 | 13 | // 2. Load your translations from a database, api, etc. 14 | enTranslationsString := `[ 15 | { 16 | "Key": "hello_world", 17 | "Default": "Hello World" 18 | } 19 | ]` 20 | esTranslationsBytes := []byte(`[ 21 | { 22 | "Key": "hello_world", 23 | "Default": "Hola Mundo" 24 | } 25 | ]`) 26 | 27 | // 3. Then you can load the translations from your source 28 | // In this case we are using JSON but you can also use YAML 29 | enTranslations, err := goeasyi18n.LoadFromJsonString(enTranslationsString) 30 | if err != nil { 31 | panic(err) 32 | } 33 | 34 | // You can also load from bytes instead of strings 35 | esTranslations, err := goeasyi18n.LoadFromJsonBytes(esTranslationsBytes) 36 | if err != nil { 37 | panic(err) 38 | } 39 | 40 | // 4. Add your languages with their translations 41 | i18n.AddLanguage("en", enTranslations) 42 | i18n.AddLanguage("es", esTranslations) 43 | 44 | // 5. Get the translations 45 | ten := i18n.T("en", "hello_world") 46 | tes := i18n.T("es", "hello_world") 47 | 48 | fmt.Println(ten) 49 | fmt.Println(tes) 50 | 51 | /* 52 | Prints: 53 | Hello World 54 | Hola Mundo 55 | */ 56 | } 57 | -------------------------------------------------------------------------------- /examples/09-advanced-example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | ) 7 | 8 | func handler(w http.ResponseWriter, r *http.Request) { 9 | // Get the lang query param, you are responsible in your app to get the lang 10 | // In this example is just passed as a query param but you can use a cookie 11 | // or an url segment or whatever you want 12 | lang := "en" // Default language 13 | if r.URL.Query().Get("lang") != "" { 14 | lang = r.URL.Query().Get("lang") 15 | } 16 | 17 | // Check if language is supported (you can return a 404 error if you want) 18 | hasLanguage := i18n.HasLanguage(lang) 19 | 20 | // Execute the template using the detected language 21 | templateData := map[string]any{ 22 | "Lang": lang, 23 | "HasLanguage": hasLanguage, 24 | } 25 | 26 | // Instead of using the {{Translate ...}} inside the template, you can also 27 | // translate the text here and pass it to the template for example: 28 | // someTranslation := i18n.Translate(lang, "some_translation_key") 29 | 30 | responseText, err := ExecuteTemplate("./views/index.html", templateData) 31 | if err != nil { 32 | http.Error(w, err.Error(), http.StatusInternalServerError) 33 | return 34 | } 35 | 36 | // Write the response 37 | w.Write([]byte(responseText)) 38 | } 39 | 40 | func main() { 41 | InitializeI18n() 42 | 43 | http.HandleFunc("/", handler) 44 | 45 | log.Println("Listening on http://localhost:9090") 46 | log.Fatal(http.ListenAndServe(":9090", nil)) 47 | } 48 | -------------------------------------------------------------------------------- /examples/02-variable-interpolation/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/eduardolat/goeasyi18n" 7 | ) 8 | 9 | func main() { 10 | // 1. Create a new i18n instance 11 | i18n := goeasyi18n.NewI18n() 12 | 13 | // 2. Create your translations 14 | // You can add any variables to your translations 15 | // Use the syntax {{.VariableName}} 16 | enTranslations := goeasyi18n.TranslateStrings{ 17 | { 18 | Key: "hello_message", 19 | Default: "Hello {{.Name}} {{.SurName}}, welcome to Go Easy i18n!", 20 | }, 21 | } 22 | 23 | esTranslations := goeasyi18n.TranslateStrings{ 24 | { 25 | Key: "hello_message", 26 | Default: "¡Hola {{.Name}} {{.SurName}}, bienvenido a Go Easy i18n!", 27 | }, 28 | } 29 | 30 | // 3. Add your languages with their translations 31 | i18n.AddLanguage("en", enTranslations) 32 | i18n.AddLanguage("es", esTranslations) 33 | 34 | // 4. Crete the options for the translation with the variables 35 | // The Data field is a map[string]any that contains the variables to be replaced 36 | options := goeasyi18n.Options{ 37 | Data: map[string]any{ 38 | "Name": "John", 39 | "SurName": "Doe", 40 | }, 41 | } 42 | 43 | // 5. Get the translations using the options (with the variables) 44 | t1 := i18n.T("en", "hello_message", options) 45 | t2 := i18n.T("es", "hello_message", options) 46 | 47 | fmt.Println(t1) 48 | fmt.Println(t2) 49 | 50 | /* 51 | Prints: 52 | Hello John Doe, welcome to Go Easy i18n! 53 | ¡Hola John Doe, bienvenido a Go Easy i18n! 54 | */ 55 | } 56 | -------------------------------------------------------------------------------- /examples/08-templating/i18n.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/eduardolat/goeasyi18n" 5 | ) 6 | 7 | var i18n *goeasyi18n.I18n 8 | 9 | func InitializeI18n() { 10 | i18n = goeasyi18n.NewI18n() 11 | 12 | enTranslations := goeasyi18n.TranslateStrings{ 13 | { 14 | Key: "hello_message", 15 | Default: "Hello {{.Name}}, welcome to Go Easy i18n!", 16 | }, 17 | { 18 | Key: "unread_messages", 19 | One: "You have {{.Qty}} unread message.", 20 | Many: "You have {{.Qty}} unread messages.", 21 | }, 22 | { 23 | Key: "friend_update", 24 | Male: "{{.Name}} updated his status.", 25 | Female: "{{.Name}} updated her status.", 26 | NonBinary: "{{.Name}} updated their status.", 27 | }, 28 | { 29 | Key: "friend_request", 30 | OneMale: "He sent you a friend request.", 31 | ManyFemale: "She sent you {{.Qty}} friend requests.", 32 | }, 33 | } 34 | 35 | esTranslations := goeasyi18n.TranslateStrings{ 36 | { 37 | Key: "hello_message", 38 | Default: "¡Hola {{.Name}}, bienvenido a Go Easy i18n!", 39 | }, 40 | { 41 | Key: "unread_messages", 42 | One: "Tienes {{.Qty}} mensaje sin leer.", 43 | Many: "Tienes {{.Qty}} mensajes sin leer.", 44 | }, 45 | { 46 | Key: "friend_update", 47 | Male: "{{.Name}} actualizó su estado.", 48 | Female: "{{.Name}} actualizó su estado.", 49 | NonBinary: "{{.Name}} actualizó su estado.", 50 | }, 51 | { 52 | Key: "friend_request", 53 | OneMale: "Él te envió una solicitud de amistad.", 54 | ManyFemale: "Ella te envió {{.Qty}} solicitudes de amistad.", 55 | }, 56 | } 57 | 58 | i18n.AddLanguage("en", enTranslations) 59 | i18n.AddLanguage("es", esTranslations) 60 | } 61 | -------------------------------------------------------------------------------- /examples/03-json-yaml-loaders/02-from-files/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/eduardolat/goeasyi18n" 7 | ) 8 | 9 | func main() { 10 | // 1. Create a new i18n instance 11 | i18n := goeasyi18n.NewI18n() 12 | 13 | // 2. Load your translations from JSON or YAML files 14 | // You can load one or more files like goeasyi18n.LoadFromJsonFiles("./en/t1.json", "./en/t2.json") 15 | // You can use glob patterns like goeasyi18n.LoadFromJsonFiles("./en/*.json") 16 | // All the translation files get merged 17 | 18 | // Load english translations from JSON files 19 | enTranslations, err := goeasyi18n.LoadFromJsonFiles("./en/*.json") 20 | if err != nil { 21 | panic(err) 22 | } 23 | 24 | // Load spanish translations from YAML files 25 | esTranslations, err := goeasyi18n.LoadFromYamlFiles("./es/*.yaml") 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | // 3. Add your languages with their translations 31 | i18n.AddLanguage("en", enTranslations) 32 | i18n.AddLanguage("es", esTranslations) 33 | 34 | // 4. Crete the options for the translations with/without interpolations 35 | options := goeasyi18n.Options{} 36 | optionsWithName := goeasyi18n.Options{ 37 | Data: map[string]string{ 38 | "Name": "John Doe", 39 | }, 40 | } 41 | 42 | // 5. Get the translations using the options (with the variables) 43 | ten1 := i18n.T("en", "hello_world", options) 44 | ten2 := i18n.T("en", "hello_user", optionsWithName) 45 | ten3 := i18n.T("en", "hello_admin", optionsWithName) 46 | 47 | tes1 := i18n.T("es", "hello_world", options) 48 | tes2 := i18n.T("es", "hello_user", optionsWithName) 49 | tes3 := i18n.T("es", "hello_admin", optionsWithName) 50 | 51 | fmt.Println(ten1) 52 | fmt.Println(ten2) 53 | fmt.Println(ten3) 54 | fmt.Println(tes1) 55 | fmt.Println(tes2) 56 | fmt.Println(tes3) 57 | 58 | /* 59 | Prints: 60 | Hello World 61 | Hello John Doe 62 | Hello John Doe, you are an admin 63 | Hola Mundo 64 | Hola John Doe 65 | Hola John Doe, eres un administrador 66 | */ 67 | } 68 | -------------------------------------------------------------------------------- /examples/04-default-pluralization/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/eduardolat/goeasyi18n" 7 | ) 8 | 9 | func main() { 10 | // 1. Create a new i18n instance 11 | i18n := goeasyi18n.NewI18n() 12 | 13 | // 2. Create your translations 14 | // If something goes wrong, the default value is used 15 | // The default pluralization only works for one and many keys 16 | // If the count (later in the Options) is 1, the one key is used 17 | // If the count (later in the Options) is greater than 1, the many key is used 18 | enTranslations := goeasyi18n.TranslateStrings{ 19 | { 20 | Key: "hello_emails", 21 | Default: "Hello, you have emails", 22 | One: "Hello, you have one email", 23 | Many: "Hello, you have {{.EmailQty}} emails", 24 | }, 25 | } 26 | 27 | esTranslations := goeasyi18n.TranslateStrings{ 28 | { 29 | Key: "hello_emails", 30 | Default: "Hola, tienes correos", 31 | One: "Hola, tienes un correo", 32 | Many: "Hola, tienes {{.EmailQty}} correos", 33 | }, 34 | } 35 | 36 | // 3. Add your languages with their translations 37 | i18n.AddLanguage("en", enTranslations) 38 | i18n.AddLanguage("es", esTranslations) 39 | 40 | // 4. Create the Options 41 | // The Count field is a *int that contains a number which is used to 42 | // select the correct pluralization key 43 | oneEmail := 1 // Get this value from your database or wherever you want 44 | oneEmailOptions := goeasyi18n.Options{ 45 | Count: &oneEmail, 46 | Data: map[string]any{ 47 | "EmailQty": oneEmail, 48 | }, 49 | } 50 | 51 | manyEmails := 5 // Get this value from your database or wherever you want 52 | manyEmailsOptions := goeasyi18n.Options{ 53 | Count: &manyEmails, 54 | Data: map[string]any{ 55 | "EmailQty": manyEmails, 56 | }, 57 | } 58 | 59 | // 5. You are done! 🎉 Just get that translations! 60 | ten1 := i18n.T("en", "hello_emails", oneEmailOptions) 61 | ten2 := i18n.T("en", "hello_emails", manyEmailsOptions) 62 | 63 | tes1 := i18n.T("es", "hello_emails", oneEmailOptions) 64 | tes2 := i18n.T("es", "hello_emails", manyEmailsOptions) 65 | 66 | fmt.Println(ten1) 67 | fmt.Println(ten2) 68 | fmt.Println(tes1) 69 | fmt.Println(tes2) 70 | 71 | /* 72 | Prints: 73 | Hello, you have one email 74 | Hello, you have 5 emails 75 | Hola, tienes un correo 76 | Hola, tienes 5 correos 77 | */ 78 | } 79 | -------------------------------------------------------------------------------- /examples/03-json-yaml-loaders/03-from-embed-fs/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | 7 | "github.com/eduardolat/goeasyi18n" 8 | ) 9 | 10 | //go:embed en/*.json 11 | var enFS embed.FS 12 | 13 | //go:embed es/*.yaml 14 | var esFS embed.FS 15 | 16 | func main() { 17 | // With the embed.FS feature you can load the translations from 18 | // any fs.FS implementation, like embed.FS. 19 | // 20 | // This allows you to bundle your translations with your app 21 | // in the same binary file. 22 | 23 | // 1. Create a new i18n instance 24 | i18n := goeasyi18n.NewI18n() 25 | 26 | // 2. Load your translations from JSON or YAML files inside the embed.FS 27 | // You can load one or more files like goeasyi18n.LoadFromJsonFS(fs, "en/t1.json", "en/t2.json") 28 | // You can use glob patterns like goeasyi18n.LoadFromJsonFS(fs, "en/*.json") 29 | // All the translation files get merged 30 | 31 | // Load english translations from JSON files 32 | enTranslations, err := goeasyi18n.LoadFromJsonFS(enFS, "en/*.json") 33 | if err != nil { 34 | panic(err) 35 | } 36 | 37 | // Load spanish translations from YAML files 38 | esTranslations, err := goeasyi18n.LoadFromYamlFS(esFS, "es/*.yaml") 39 | if err != nil { 40 | panic(err) 41 | } 42 | 43 | // 3. Add your languages with their translations 44 | i18n.AddLanguage("en", enTranslations) 45 | i18n.AddLanguage("es", esTranslations) 46 | 47 | // 4. Crete the options for the translations with/without interpolations 48 | options := goeasyi18n.Options{} 49 | optionsWithName := goeasyi18n.Options{ 50 | Data: map[string]string{ 51 | "Name": "John Doe", 52 | }, 53 | } 54 | 55 | // 5. Get the translations using the options (with the variables) 56 | ten1 := i18n.T("en", "hello_world", options) 57 | ten2 := i18n.T("en", "hello_user", optionsWithName) 58 | ten3 := i18n.T("en", "hello_admin", optionsWithName) 59 | 60 | tes1 := i18n.T("es", "hello_world", options) 61 | tes2 := i18n.T("es", "hello_user", optionsWithName) 62 | tes3 := i18n.T("es", "hello_admin", optionsWithName) 63 | 64 | fmt.Println(ten1) 65 | fmt.Println(ten2) 66 | fmt.Println(ten3) 67 | fmt.Println(tes1) 68 | fmt.Println(tes2) 69 | fmt.Println(tes3) 70 | 71 | /* 72 | Prints: 73 | Hello World from embed 74 | Hello John Doe from embed 75 | Hello John Doe, you are an admin, from embed 76 | Hola Mundo desde embed 77 | Hola John Doe desde embed 78 | Hola John Doe, eres un administrador, desde embed 79 | */ 80 | } 81 | -------------------------------------------------------------------------------- /examples/08-templating/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "text/template" 6 | ) 7 | 8 | /* 9 | The templating feature allows you to make translations inside your templates. 10 | It works with both text/template and html/template. 11 | 12 | To pass the language use: "lang" "xxx" 13 | To pass the key use: "key" "xxx" 14 | To pass the count use: "count" "xxx" 15 | To pass the gender use: "gender" "male/female/nonbinary" 16 | 17 | All other "key" "value" pairs will be converted to strings and passed to 18 | the translation as variables. 19 | */ 20 | 21 | const templateText = `Welcome to Go Easy i18n! 22 | 23 | {{Translate "lang" "en" "key" "hello_message" "Name" "John Doe"}} 24 | {{Translate "lang" "es" "key" "hello_message" "Name" "John Doe"}} 25 | 26 | {{Translate "lang" "en" "key" "unread_messages" "count" "1" "Qty" "1"}} 27 | {{Translate "lang" "en" "key" "unread_messages" "count" "10" "Qty" "10"}} 28 | {{Translate "lang" "es" "key" "unread_messages" "count" "1" "Qty" "1"}} 29 | {{Translate "lang" "es" "key" "unread_messages" "count" "10" "Qty" "10"}} 30 | 31 | {{Translate "lang" "en" "key" "friend_update" "gender" "male" "Name" "John"}} 32 | {{Translate "lang" "en" "key" "friend_update" "gender" "female" "Name" "Jane"}} 33 | {{Translate "lang" "en" "key" "friend_update" "gender" "nonbinary" "Name" "Jane"}} 34 | {{Translate "lang" "es" "key" "friend_update" "gender" "male" "Name" "John"}} 35 | {{Translate "lang" "es" "key" "friend_update" "gender" "female" "Name" "Jane"}} 36 | {{Translate "lang" "es" "key" "friend_update" "gender" "nonbinary" "Name" "Jane"}} 37 | 38 | {{Translate "lang" "en" "key" "friend_request" "gender" "male" "count" "1" "Qty" "1"}} 39 | {{Translate "lang" "en" "key" "friend_request" "gender" "female" "count" "10" "Qty" "10"}} 40 | {{Translate "lang" "es" "key" "friend_request" "gender" "male" "count" "1" "Qty" "1"}} 41 | {{Translate "lang" "es" "key" "friend_request" "gender" "female" "count" "10" "Qty" "10"}} 42 | ` 43 | 44 | func main() { 45 | // 1. Initialize the i18n instance 46 | InitializeI18n() 47 | 48 | // 2. Create function to pass to the template func map 49 | translateFunc := i18n.NewTemplatingTranslateFunc() 50 | 51 | // 3. Create the template and pass the created function to the func map 52 | tmpl := template.Must(template.New("test").Funcs(template.FuncMap{ 53 | "Translate": translateFunc, // You can use any name you want, for example: "T" 54 | }).Parse(templateText)) 55 | 56 | // 4. Execute the template 57 | tmpl.Execute(os.Stdout, nil) 58 | } 59 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Go Easy i18n 2 | 3 | Thank you for considering contributing to Go Easy i18n! Whether it's code or documentation, we appreciate your help. If you have any questions or need clarification on the contribution process, feel free to open an issue. Here are some guidelines to ensure a smooth and efficient contribution process. 4 | 5 | ## Code of Conduct 6 | 7 | Before contributing, please read and adhere to our Code of Conduct. We expect all contributors to maintain a respectful and harassment-free environment. 8 | 9 | ## Contribution Process 10 | 11 | 1. **Fork the Repository**: 12 | - Fork the main repository to your personal GitHub account. 13 | 14 | 2. **Clone the Repository**: 15 | - Clone your fork to your local machine. 16 | ```bash 17 | git clone https://github.com/[your_username]/goeasyi18n.git 18 | ``` 19 | 20 | 3. **Create a Branch**: 21 | - Create a new branch for your feature or fix. 22 | ```bash 23 | git checkout -b your-branch-name 24 | ``` 25 | 26 | 4. **Make Your Changes**: 27 | - Implement your changes, ensuring you follow the project's best practices and coding styles. 28 | 29 | 5. **Run Tests**: 30 | - Ensure that all tests pass. If you're introducing a new feature or change, you must provide tests that cover your changes. 31 | 32 | 6. **Commit and Push**: 33 | - Commit your changes in lowercase and push them to your fork. 34 | ```bash 35 | git add . 36 | git commit -m "brief description of changes" 37 | git push origin your-branch-name 38 | ``` 39 | 40 | 7. **Open a Pull Request (PR)**: 41 | - Go to the main repository and click on "New Pull Request". Select your branch and propose the PR. 42 | 43 | ## Best Practices for PRs 44 | 45 | - **Clear Description**: Provide a detailed description of the changes you are proposing. 46 | - **Related Issues**: If your PR relates to an existing issue, make sure to reference it. 47 | - **Small Commits**: Try to make small, specific commits; this facilitates review and the merge process. 48 | - **Document Your Changes**: If you're introducing a new feature or making significant changes, make sure to update the relevant documentation. 49 | 50 | ## Communication 51 | 52 | If you have questions, concerns, or need clarifications, please open an issue. We value community feedback and will try to respond as soon as possible. 53 | 54 | ## Acknowledgments 55 | 56 | Thank you for your interest in improving Go Easy i18n! Whether it's through code or documentation, your contribution is invaluable to the community. 57 | -------------------------------------------------------------------------------- /loader_json.go: -------------------------------------------------------------------------------- 1 | package goeasyi18n 2 | 3 | import ( 4 | "encoding/json" 5 | "io/fs" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | // LoadFromJsonBytes loads a list of TranslateString 11 | // from the provided JSON bytes. 12 | func LoadFromJsonBytes( 13 | jsonBytes []byte, 14 | ) (TranslateStrings, error) { 15 | var translateStrings TranslateStrings 16 | err := json.Unmarshal(jsonBytes, &translateStrings) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | return translateStrings, nil 22 | } 23 | 24 | // LoadFromJsonString loads a list of TranslateString 25 | // from the provided JSON string. 26 | func LoadFromJsonString( 27 | jsonString string, 28 | ) (TranslateStrings, error) { 29 | return LoadFromJsonBytes([]byte(jsonString)) 30 | } 31 | 32 | // LoadFromJsonFiles loads a list of TranslateString from 33 | // one or multiple JSON files, allowing glob patterns 34 | // like "path/to/files/*.json". 35 | func LoadFromJsonFiles( 36 | filesOrGlobs ...string, 37 | ) (TranslateStrings, error) { 38 | var allTranslateStrings TranslateStrings 39 | 40 | for _, pattern := range filesOrGlobs { 41 | matches, err := filepath.Glob(pattern) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | for _, file := range matches { 47 | byteValue, err := os.ReadFile(file) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | translateString, err := LoadFromJsonBytes(byteValue) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | allTranslateStrings = append(allTranslateStrings, translateString...) 58 | } 59 | } 60 | 61 | return allTranslateStrings, nil 62 | } 63 | 64 | // LoadFromJsonFS loads a list of TranslateString from 65 | // one or multiple JSON files located within a provided 66 | // filesystem (fs.FS), allowing glob patterns 67 | // like "path/to/files/*.json". 68 | func LoadFromJsonFS( 69 | fileSystem fs.FS, 70 | filesOrGlobs ...string, 71 | ) (TranslateStrings, error) { 72 | var allTranslateStrings TranslateStrings 73 | 74 | for _, pattern := range filesOrGlobs { 75 | matches, err := fs.Glob(fileSystem, pattern) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | if len(matches) == 0 { 81 | continue 82 | } 83 | 84 | for _, file := range matches { 85 | byteValue, err := readFileFromFS(fileSystem, file) 86 | if err != nil { 87 | return nil, err 88 | } 89 | 90 | translateString, err := LoadFromJsonBytes(byteValue) 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | allTranslateStrings = append(allTranslateStrings, translateString...) 96 | } 97 | } 98 | 99 | return allTranslateStrings, nil 100 | } 101 | -------------------------------------------------------------------------------- /loader_yaml.go: -------------------------------------------------------------------------------- 1 | package goeasyi18n 2 | 3 | import ( 4 | "encoding/json" 5 | "io/fs" 6 | "os" 7 | "path/filepath" 8 | 9 | "gopkg.in/yaml.v3" 10 | ) 11 | 12 | // LoadFromYamlBytes loads a list of TranslateString 13 | // from the provided YAML bytes. 14 | func LoadFromYamlBytes( 15 | yamlBytes []byte, 16 | ) (TranslateStrings, error) { 17 | // This function uses a bridge, it converts YAML to JSON before 18 | // parsing it as a TranslateString to avoid inconsistencies 19 | 20 | var parsedYaml any 21 | err := yaml.Unmarshal(yamlBytes, &parsedYaml) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | jsonBytes, err := json.Marshal(parsedYaml) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | var translateStrings TranslateStrings 32 | err = json.Unmarshal(jsonBytes, &translateStrings) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | return translateStrings, nil 38 | } 39 | 40 | // LoadFromYamlString loads a list of TranslateString 41 | // from the provided YAML string. 42 | func LoadFromYamlString( 43 | yamlString string, 44 | ) (TranslateStrings, error) { 45 | return LoadFromYamlBytes([]byte(yamlString)) 46 | } 47 | 48 | // LoadFromYamlFiles loads a list of TranslateString from 49 | // one or multiple YAML files, allowing glob patterns 50 | // like "path/to/files/*.yaml". 51 | func LoadFromYamlFiles( 52 | filesOrGlobs ...string, 53 | ) (TranslateStrings, error) { 54 | var allTranslateStrings TranslateStrings 55 | 56 | for _, pattern := range filesOrGlobs { 57 | matches, err := filepath.Glob(pattern) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | for _, file := range matches { 63 | byteValue, err := os.ReadFile(file) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | translateString, err := LoadFromYamlBytes(byteValue) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | allTranslateStrings = append(allTranslateStrings, translateString...) 74 | } 75 | } 76 | 77 | return allTranslateStrings, nil 78 | } 79 | 80 | // LoadFromYamlFS loads a list of TranslateString from 81 | // one or multiple YAML files located within a provided 82 | // filesystem (fs.FS), allowing glob patterns 83 | // like "path/to/files/*.yaml". 84 | func LoadFromYamlFS( 85 | fileSystem fs.FS, 86 | filesOrGlobs ...string, 87 | ) (TranslateStrings, error) { 88 | var allTranslateStrings TranslateStrings 89 | 90 | for _, pattern := range filesOrGlobs { 91 | matches, err := fs.Glob(fileSystem, pattern) 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | if len(matches) == 0 { 97 | continue 98 | } 99 | 100 | for _, file := range matches { 101 | byteValue, err := readFileFromFS(fileSystem, file) 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | translateString, err := LoadFromYamlBytes(byteValue) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | allTranslateStrings = append(allTranslateStrings, translateString...) 112 | } 113 | } 114 | 115 | return allTranslateStrings, nil 116 | } 117 | -------------------------------------------------------------------------------- /examples/07-pluralized-genders/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/eduardolat/goeasyi18n" 7 | ) 8 | 9 | /* 10 | You have come very far!! 👏👏👏 11 | Now that you know how to pluralize and manage gender in your translations, 12 | let's do something interesting... Let's combine both features! 13 | 14 | Combining both features you will gain acces to all these keys in your translations: 15 | 16 | // For only pluralization 17 | Zero, One, Two, Few, Many 18 | 19 | // For only genders management 20 | Male, Female, NonBinary 21 | 22 | // For pluralization with male gender 23 | ZeroMale, OneMale, TwoMale, FewMale, ManyMale 24 | 25 | // For pluralization with female gender 26 | ZeroFemale, OneFemale, TwoFemale, FewFemale, ManyFemale 27 | 28 | // For pluralization with non binary gender 29 | ZeroNonBinary, OneNonBinary, TwoNonBinary, FewNonBinary, ManyNonBinary 30 | */ 31 | 32 | func main() { 33 | // 1. Create a new i18n instance 34 | i18n := goeasyi18n.NewI18n() 35 | 36 | // 2. Create your translations 37 | // If something goes wrong, the default value is used 38 | enTranslations := goeasyi18n.TranslateStrings{ 39 | { 40 | Key: "friend_emails", 41 | Default: "Hello, your friend have emails", 42 | OneMale: "Hello, he has one email", 43 | ManyFemale: "Hello, she has {{.EmailQty}} emails", 44 | // You can add as many combinations as you want 45 | }, 46 | } 47 | 48 | esTranslations := goeasyi18n.TranslateStrings{ 49 | { 50 | Key: "friend_emails", 51 | Default: "Hola, tu amigo tiene correos", 52 | OneMale: "Hola, él tiene un correo", 53 | ManyFemale: "Hola, ella tiene {{.EmailQty}} correos", 54 | }, 55 | } 56 | 57 | // 3. Add your languages with their translations 58 | // Yoy can also add custom pluralization rules but for this 59 | // example we will use the default ones to keep it simple 60 | i18n.AddLanguage("en", enTranslations) 61 | i18n.AddLanguage("es", esTranslations) 62 | 63 | // 4. Create the Options 64 | // The Gender field is a *string that contains the gender to use 65 | // Here you can use male, female, nonbinary or non-binary 66 | oneInt := 1 67 | manyInt := 10 68 | maleText := "male" 69 | femaleText := "female" 70 | 71 | oneMaleOptions := goeasyi18n.Options{ 72 | Gender: &maleText, 73 | Count: &oneInt, 74 | Data: map[string]any{ 75 | "EmailQty": oneInt, 76 | }, 77 | } 78 | 79 | manyFemaleOptions := goeasyi18n.Options{ 80 | Gender: &femaleText, 81 | Count: &manyInt, 82 | Data: map[string]any{ 83 | "EmailQty": manyInt, 84 | }, 85 | } 86 | 87 | // 5. You are done! 🎉 Just get that translations! 88 | ten1 := i18n.T("en", "friend_emails", oneMaleOptions) 89 | ten2 := i18n.T("en", "friend_emails", manyFemaleOptions) 90 | 91 | tes1 := i18n.T("es", "friend_emails", oneMaleOptions) 92 | tes2 := i18n.T("es", "friend_emails", manyFemaleOptions) 93 | 94 | fmt.Println(ten1) 95 | fmt.Println(ten2) 96 | fmt.Println(tes1) 97 | fmt.Println(tes2) 98 | 99 | /* 100 | Prints: 101 | Hello, he has one email 102 | Hello, she has 10 emails 103 | Hola, él tiene un correo 104 | Hola, ella tiene 10 correos 105 | */ 106 | } 107 | -------------------------------------------------------------------------------- /examples/06-genders/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/eduardolat/goeasyi18n" 7 | ) 8 | 9 | /* 10 | Gender Handling: Why is it useful? 11 | 12 | Let's say your app has a feature that says, "John liked your post" or "Emily liked your post." 13 | In some languages, the verb "liked" might change based on the gender of the person who 14 | liked the post. 15 | 16 | Example: 17 | - English: "He liked your post" vs "She liked your post" 18 | - Spanish: "A él le gustó tu publicación" vs "A ella le gustó tu publicación" 19 | 20 | With gender-specific translations, you can easily adapt the sentence structure to fit the 21 | gender, making your app more linguistically accurate and inclusive. 22 | 23 | No need for messy if-else statements to handle gender variations. Just set it up once, and 24 | the library takes care of the rest! 25 | */ 26 | 27 | func main() { 28 | // 1. Create a new i18n instance 29 | i18n := goeasyi18n.NewI18n() 30 | 31 | // 2. Create your translations 32 | // If something goes wrong, the default value is used 33 | // The gender keys are Male, Female and NonBinary 34 | enTranslations := goeasyi18n.TranslateStrings{ 35 | { 36 | Key: "friend_emails", 37 | Default: "Hello, your friend have emails", 38 | Male: "Hello, he has emails", 39 | Female: "Hello, she has emails", 40 | NonBinary: "Hello, your friend have emails", 41 | }, 42 | } 43 | 44 | esTranslations := goeasyi18n.TranslateStrings{ 45 | { 46 | Key: "friend_emails", 47 | Default: "Hola, tu amigo tiene correos", 48 | Male: "Hola, él tiene correos", 49 | Female: "Hola, ella tiene correos", 50 | NonBinary: "Hola, tu amigue tiene correos", 51 | }, 52 | } 53 | 54 | // 3. Add your languages with their translations 55 | i18n.AddLanguage("en", enTranslations) 56 | i18n.AddLanguage("es", esTranslations) 57 | 58 | // 4. Create the Options 59 | // The Gender field is a *string that contains the gender to use 60 | // Here you can use male, female, nonbinary or non-binary 61 | maleText := "male" 62 | femaleText := "female" 63 | nonbinaryText := "nonbinary" 64 | 65 | maleOptions := goeasyi18n.Options{ 66 | Gender: &maleText, 67 | } 68 | 69 | femaleOptions := goeasyi18n.Options{ 70 | Gender: &femaleText, 71 | } 72 | 73 | nonbinaryOptions := goeasyi18n.Options{ 74 | Gender: &nonbinaryText, 75 | } 76 | 77 | // 5. You are done! 🎉 Just get that translations! 78 | ten1 := i18n.T("en", "friend_emails", maleOptions) 79 | ten2 := i18n.T("en", "friend_emails", femaleOptions) 80 | ten3 := i18n.T("en", "friend_emails", nonbinaryOptions) 81 | 82 | tes1 := i18n.T("es", "friend_emails", maleOptions) 83 | tes2 := i18n.T("es", "friend_emails", femaleOptions) 84 | tes3 := i18n.T("es", "friend_emails", nonbinaryOptions) 85 | 86 | fmt.Println(ten1) 87 | fmt.Println(ten2) 88 | fmt.Println(ten3) 89 | fmt.Println(tes1) 90 | fmt.Println(tes2) 91 | fmt.Println(tes3) 92 | 93 | /* 94 | Prints: 95 | Hello, he has emails 96 | Hello, she has emails 97 | Hello, your friend have emails 98 | Hola, él tiene correos 99 | Hola, ella tiene correos 100 | Hola, tu amigue tiene correos 101 | */ 102 | } 103 | -------------------------------------------------------------------------------- /examples/01-basic-usage/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/eduardolat/goeasyi18n" 7 | ) 8 | 9 | func main() { 10 | // 1. Create a new i18n instance 11 | // You can skip the goeasyi18n.Config{} entirely if you are 12 | // ok with the default values. 13 | i18n := goeasyi18n.NewI18n(goeasyi18n.Config{ 14 | // You can set the fallback language (optional) 15 | // The default value is "en" 16 | FallbackLanguageName: "en", 17 | 18 | // You can disable the consistency check (optional) 19 | // By default, if you add a translation for a language 20 | // that has not the same keys as the other languages, 21 | // the i18n instance will log warnings. 22 | DisableConsistencyCheck: false, 23 | }) 24 | 25 | // 2. Create your translations 26 | enTranslations := goeasyi18n.TranslateStrings{ 27 | { 28 | Key: "hello_message", 29 | Default: "Hello, welcome to Go Easy i18n!", 30 | }, 31 | } 32 | 33 | esTranslations := goeasyi18n.TranslateStrings{ 34 | { 35 | Key: "hello_message", 36 | Default: "¡Hola, bienvenido a Go Easy i18n!", 37 | }, 38 | } 39 | 40 | // 3. Add your languages with their translations 41 | // The name of the language can be anything you want 42 | // You can use simple strings like "en" or "es" 43 | // You you can use the full language name like "english" or "spanish" 44 | // You can even use the IETF Language Tag like "en-US" or "es-ES" 45 | i18n.AddLanguage("en", enTranslations) 46 | 47 | // If the language has the same keys as the other languages, 48 | // the i18n instance will log warnings and return a slice of 49 | // inconsistencies as strings. You can disable this behavior 50 | // in the i18n instance config. 51 | inconsistencies := i18n.AddLanguage("es", esTranslations) 52 | fmt.Printf("Inconsistencies: %v\n", inconsistencies) 53 | // (no inconsistencies) 54 | // Prints: Inconsistencies: [] 55 | 56 | // 4. You are done! 🎉 Just get that translations! 57 | t1 := i18n.Translate("en", "hello_message", goeasyi18n.Options{}) 58 | // Or you can use the T method (it's just an alias for Translate) 59 | // and you can skip the options if you don't need them 60 | t2 := i18n.T("es", "hello_message") 61 | 62 | fmt.Println(t1) 63 | fmt.Println(t2) 64 | 65 | /* 66 | Prints: 67 | Hello, welcome to Go Easy i18n! 68 | ¡Hola, bienvenido a Go Easy i18n! 69 | */ 70 | 71 | // 5. (Extra) You can check if a language exists in the i18n instance 72 | enExists := i18n.HasLanguage("en") 73 | xxExists := i18n.HasLanguage("xx") 74 | 75 | fmt.Printf("en exists: %v\n", enExists) 76 | fmt.Printf("xx exists: %v\n", xxExists) 77 | 78 | /* 79 | Prints: 80 | en exists: true 81 | xx exists: false 82 | */ 83 | 84 | // 6. (Extra) You can create a translate function for a specific language 85 | // to prevent passing the language name every time you want to translate 86 | // something 87 | translateEn := i18n.NewLangTranslateFunc("en") 88 | translateEs := i18n.NewLangTranslateFunc("es") 89 | 90 | // You can skip the options if you don't need them 91 | t3 := translateEn("hello_message", goeasyi18n.Options{}) 92 | t4 := translateEs("hello_message") 93 | 94 | fmt.Println(t3) 95 | fmt.Println(t4) 96 | 97 | /* 98 | Prints: 99 | Hello, welcome to Go Easy i18n! 100 | ¡Hola, bienvenido a Go Easy i18n! 101 | */ 102 | } 103 | -------------------------------------------------------------------------------- /examples/09-advanced-example/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Go Easy i18n - Website example 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 |
19 |

20 | Detected language: 21 | {{.Lang}} 22 |

23 | 24 |

25 | Go Easy i18n - Website example 26 |

27 | 28 | {{if not .HasLanguage}} 29 |
30 | Language {{.Lang}} not found. Using the fallback language. 31 |
32 | {{end}} 33 |
34 | 35 |
36 |
37 | 38 | 42 | 43 |
44 |
45 | 46 | 50 | 51 |
52 |
53 | 54 | 58 | 59 |
60 |
61 | 62 | 66 | 67 |
68 |
69 | 70 | 74 | 75 |
76 |
77 | 78 |
81 |

Translation with key "hello_world"

82 |

{{Translate "lang" .Lang "key" "hello_world"}}

83 | 84 |
85 | 86 |

Translation with key "hello_name"

87 |

{{Translate "lang" .Lang "key" "hello_name" "Name" "John Doe"}}

88 | 89 |
90 | 91 |

Translation with key "unread_emails"

92 |

{{Translate "lang" .Lang "key" "unread_emails" "count" "1" "Count" "1"}}

93 |

{{Translate "lang" .Lang "key" "unread_emails" "count" "10" "Count" "10"}}

94 | 95 |
96 | 97 |

Translation with key "friend_unread_emails"

98 |

{{Translate "lang" .Lang "key" "friend_unread_emails" "count" "1" "gender" "male" "Count" "1" "Name" "John"}}

99 |

{{Translate "lang" .Lang "key" "friend_unread_emails" "count" "10" "gender" "male" "Count" "10" "Name" "John"}}

100 |

{{Translate "lang" .Lang "key" "friend_unread_emails" "count" "1" "gender" "female" "Count" "1" "Name" "Jane"}}

101 |

{{Translate "lang" .Lang "key" "friend_unread_emails" "count" "10" "gender" "female" "Count" "10" "Name" "Jane"}}

102 |
103 | 104 |
105 |

106 | If you like 👍 this project, please give it a star on GitHub 107 |

108 | Star 109 |
110 | 111 | 112 | -------------------------------------------------------------------------------- /loader_yaml_test.go: -------------------------------------------------------------------------------- 1 | package goeasyi18n 2 | 3 | import ( 4 | "embed" 5 | "testing" 6 | ) 7 | 8 | func TestLoadFromYamlBytes(t *testing.T) { 9 | bytes := []byte("- Key: hello\n Default: Hello\n") 10 | strings, err := LoadFromYamlBytes(bytes) 11 | if err != nil { 12 | t.Errorf("Unexpected error: %v", err) 13 | } 14 | if len(strings) != 1 || strings[0].Key != "hello" { 15 | t.Errorf("Unexpected result: %v", strings) 16 | } 17 | if strings[0].Default != "Hello" { 18 | t.Errorf("Unexpected result: %v", strings) 19 | } 20 | } 21 | 22 | func TestLoadFromYamlString(t *testing.T) { 23 | strings, err := LoadFromYamlString("- Key: hello\n Default: Hello\n") 24 | if err != nil { 25 | t.Errorf("Unexpected error: %v", err) 26 | } 27 | if len(strings) != 1 || strings[0].Key != "hello" { 28 | t.Errorf("Unexpected result: %v", strings) 29 | } 30 | if strings[0].Default != "Hello" { 31 | t.Errorf("Unexpected result: %v", strings) 32 | } 33 | } 34 | 35 | func TestLoadFromYamlFiles(t *testing.T) { 36 | t.Run("load single file", func(t *testing.T) { 37 | strings, err := LoadFromYamlFiles("./testfiles/test1.yaml") 38 | if err != nil { 39 | t.Errorf("Unexpected error: %v", err) 40 | } 41 | if len(strings) != 1 || strings[0].Key != "hello" { 42 | t.Errorf("Unexpected result: %v", strings) 43 | } 44 | if strings[0].Default != "Hello" { 45 | t.Errorf("Unexpected result: %v", strings) 46 | } 47 | }) 48 | 49 | t.Run("load multiple files", func(t *testing.T) { 50 | strings, err := LoadFromYamlFiles( 51 | "./testfiles/test1.yaml", 52 | "./testfiles/test2.yaml", 53 | ) 54 | if err != nil { 55 | t.Errorf("Unexpected error: %v", err) 56 | } 57 | if len(strings) != 2 { 58 | t.Errorf("Unexpected result: %v", strings) 59 | } 60 | if strings[0].Key != "hello" { 61 | t.Errorf("Unexpected result: %v", strings) 62 | } 63 | if strings[1].Key != "world" { 64 | t.Errorf("Unexpected result: %v", strings) 65 | } 66 | }) 67 | 68 | t.Run("load with glob pattern", func(t *testing.T) { 69 | strings, err := LoadFromYamlFiles("./testfiles/test*.yaml") 70 | if err != nil { 71 | t.Errorf("Unexpected error: %v", err) 72 | } 73 | if len(strings) != 2 { 74 | t.Errorf("Unexpected result: %v", strings) 75 | } 76 | }) 77 | 78 | t.Run("handle incorrect yaml", func(t *testing.T) { 79 | _, err := LoadFromYamlFiles("./testfiles/incorrect.yaml") 80 | if err == nil { 81 | t.Errorf("Expected error, got nil") 82 | } 83 | }) 84 | 85 | t.Run("handle no match glob", func(t *testing.T) { 86 | _, err := LoadFromYamlFiles("./testfiles/nomatch*.yaml") 87 | if err != nil { 88 | t.Errorf("Unexpected error: %v", err) 89 | } 90 | }) 91 | } 92 | 93 | //go:embed testfiles/* 94 | var yamlTestFiles embed.FS 95 | 96 | func TestLoadFromYamlFS(t *testing.T) { 97 | t.Run("load single file", func(t *testing.T) { 98 | strings, err := LoadFromYamlFS( 99 | yamlTestFiles, 100 | "testfiles/test1.yaml", 101 | ) 102 | if err != nil { 103 | t.Errorf("Unexpected error: %v", err) 104 | } 105 | if len(strings) != 1 || strings[0].Key != "hello" { 106 | t.Errorf("Unexpected result: %v", strings) 107 | } 108 | if strings[0].Default != "Hello" { 109 | t.Errorf("Unexpected result: %v", strings) 110 | } 111 | }) 112 | 113 | t.Run("load multiple files", func(t *testing.T) { 114 | strings, err := LoadFromYamlFS( 115 | yamlTestFiles, 116 | "testfiles/test1.yaml", 117 | "testfiles/test2.yaml", 118 | ) 119 | if err != nil { 120 | t.Errorf("Unexpected error: %v", err) 121 | } 122 | if len(strings) != 2 { 123 | t.Errorf("Unexpected result: %v", strings) 124 | } 125 | if strings[0].Key != "hello" { 126 | t.Errorf("Unexpected result: %v", strings) 127 | } 128 | if strings[1].Key != "world" { 129 | t.Errorf("Unexpected result: %v", strings) 130 | } 131 | }) 132 | 133 | t.Run("load with glob pattern", func(t *testing.T) { 134 | strings, err := LoadFromYamlFS( 135 | yamlTestFiles, 136 | "testfiles/test*.yaml", 137 | ) 138 | if err != nil { 139 | t.Errorf("Unexpected error: %v", err) 140 | } 141 | if len(strings) != 2 { 142 | t.Errorf("Unexpected result: %v", strings) 143 | } 144 | }) 145 | 146 | t.Run("handle incorrect yaml", func(t *testing.T) { 147 | _, err := LoadFromYamlFS( 148 | yamlTestFiles, 149 | "testfiles/incorrect.yaml", 150 | ) 151 | if err == nil { 152 | t.Errorf("Expected error, got nil") 153 | } 154 | }) 155 | 156 | t.Run("handle no match glob", func(t *testing.T) { 157 | _, err := LoadFromYamlFS( 158 | yamlTestFiles, 159 | "testfiles/nomatch*.yaml", 160 | ) 161 | if err != nil { 162 | t.Errorf("Unexpected error: %v", err) 163 | } 164 | }) 165 | } 166 | -------------------------------------------------------------------------------- /loader_json_test.go: -------------------------------------------------------------------------------- 1 | package goeasyi18n 2 | 3 | import ( 4 | "embed" 5 | "testing" 6 | ) 7 | 8 | func TestLoadFromJsonBytes(t *testing.T) { 9 | bytes := []byte(`[{"Key": "hello", "Default": "Hello"}]`) 10 | strings, err := LoadFromJsonBytes(bytes) 11 | if err != nil { 12 | t.Errorf("Unexpected error: %v", err) 13 | } 14 | if len(strings) != 1 || strings[0].Key != "hello" { 15 | t.Errorf("Unexpected result: %v", strings) 16 | } 17 | if strings[0].Default != "Hello" { 18 | t.Errorf("Unexpected result: %v", strings) 19 | } 20 | } 21 | 22 | func TestLoadFromJsonString(t *testing.T) { 23 | strings, err := LoadFromJsonString(`[{"Key": "hello", "Default": "Hello"}]`) 24 | if err != nil { 25 | t.Errorf("Unexpected error: %v", err) 26 | } 27 | if len(strings) != 1 || strings[0].Key != "hello" { 28 | t.Errorf("Unexpected result: %v", strings) 29 | } 30 | if strings[0].Default != "Hello" { 31 | t.Errorf("Unexpected result: %v", strings) 32 | } 33 | } 34 | 35 | func TestLoadFromJsonFiles(t *testing.T) { 36 | t.Run("load single file", func(t *testing.T) { 37 | strings, err := LoadFromJsonFiles("./testfiles/test1.json") 38 | if err != nil { 39 | t.Errorf("Unexpected error: %v", err) 40 | } 41 | if len(strings) != 1 || strings[0].Key != "hello" { 42 | t.Errorf("Unexpected result: %v", strings) 43 | } 44 | if strings[0].Default != "Hello" { 45 | t.Errorf("Unexpected result: %v", strings) 46 | } 47 | }) 48 | 49 | t.Run("load multiple files", func(t *testing.T) { 50 | strings, err := LoadFromJsonFiles( 51 | "./testfiles/test1.json", 52 | "./testfiles/test2.json", 53 | ) 54 | if err != nil { 55 | t.Errorf("Unexpected error: %v", err) 56 | } 57 | if len(strings) != 2 { 58 | t.Errorf("Unexpected result: %v", strings) 59 | } 60 | if strings[0].Key != "hello" { 61 | t.Errorf("Unexpected result: %v", strings) 62 | } 63 | if strings[1].Key != "world" { 64 | t.Errorf("Unexpected result: %v", strings) 65 | } 66 | }) 67 | 68 | t.Run("load with glob pattern", func(t *testing.T) { 69 | strings, err := LoadFromJsonFiles("./testfiles/test*.json") 70 | if err != nil { 71 | t.Errorf("Unexpected error: %v", err) 72 | } 73 | if len(strings) != 2 { 74 | t.Errorf("Unexpected result: %v", strings) 75 | } 76 | }) 77 | 78 | t.Run("handle incorrect json", func(t *testing.T) { 79 | _, err := LoadFromJsonFiles("./testfiles/incorrect.json") 80 | if err == nil { 81 | t.Errorf("Expected error, got nil") 82 | } 83 | }) 84 | 85 | t.Run("handle no match glob", func(t *testing.T) { 86 | _, err := LoadFromJsonFiles("./testfiles/nomatch*.json") 87 | if err != nil { 88 | t.Errorf("Unexpected error: %v", err) 89 | } 90 | }) 91 | } 92 | 93 | //go:embed testfiles/* 94 | var jsonTestFiles embed.FS 95 | 96 | func TestLoadFromJsonFS(t *testing.T) { 97 | t.Run("load single file", func(t *testing.T) { 98 | strings, err := LoadFromJsonFS( 99 | jsonTestFiles, 100 | "testfiles/test1.json", 101 | ) 102 | if err != nil { 103 | t.Errorf("Unexpected error: %v", err) 104 | } 105 | if len(strings) != 1 || strings[0].Key != "hello" { 106 | t.Errorf("Unexpected result: %v", strings) 107 | } 108 | if strings[0].Default != "Hello" { 109 | t.Errorf("Unexpected result: %v", strings) 110 | } 111 | }) 112 | 113 | t.Run("load multiple files", func(t *testing.T) { 114 | strings, err := LoadFromJsonFS( 115 | jsonTestFiles, 116 | "testfiles/test1.json", 117 | "testfiles/test2.json", 118 | ) 119 | if err != nil { 120 | t.Errorf("Unexpected error: %v", err) 121 | } 122 | if len(strings) != 2 { 123 | t.Errorf("Unexpected result: %v", strings) 124 | } 125 | if strings[0].Key != "hello" { 126 | t.Errorf("Unexpected result: %v", strings) 127 | } 128 | if strings[1].Key != "world" { 129 | t.Errorf("Unexpected result: %v", strings) 130 | } 131 | }) 132 | 133 | t.Run("load with glob pattern", func(t *testing.T) { 134 | strings, err := LoadFromJsonFS( 135 | jsonTestFiles, 136 | "testfiles/test*.json", 137 | ) 138 | if err != nil { 139 | t.Errorf("Unexpected error: %v", err) 140 | } 141 | if len(strings) != 2 { 142 | t.Errorf("Unexpected result: %v", strings) 143 | } 144 | }) 145 | 146 | t.Run("handle incorrect json", func(t *testing.T) { 147 | _, err := LoadFromJsonFS( 148 | jsonTestFiles, 149 | "testfiles/incorrect.json", 150 | ) 151 | if err == nil { 152 | t.Errorf("Expected error, got nil") 153 | } 154 | }) 155 | 156 | t.Run("handle no match glob", func(t *testing.T) { 157 | _, err := LoadFromJsonFS( 158 | jsonTestFiles, 159 | "testfiles/nomatch*.json", 160 | ) 161 | if err != nil { 162 | t.Errorf("Unexpected error: %v", err) 163 | } 164 | }) 165 | } 166 | -------------------------------------------------------------------------------- /examples/05-custom-pluralization/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/eduardolat/goeasyi18n" 7 | ) 8 | 9 | /* 10 | Custom Pluralization: Why is it useful? 🤔 11 | 12 | Imagine you're building an app that shows the number of unread messages. 13 | In English, you'd say "1 unread message" and "2 unread messages" - note the "s" at the end. 14 | But what about languages where plural rules aren't so simple? 15 | 16 | With custom pluralization, you can easily handle these cases without writing complex if-else 17 | statements. Just define your plural rules once, and let the library do the heavy lifting! 18 | 19 | This makes your code cleaner and your app more linguistically accurate. Win-win! 20 | 21 | Don't get scared by the amount of code, it's just an example and in a real app you'll 22 | probably set up this only once and then use it everywhere. 23 | 24 | The available pluralization keys are: 25 | - Zero 26 | - One 27 | - Two 28 | - Few 29 | - Many 30 | 31 | Later in the translation strings you can use the keys to make your translations different 32 | depending on the count and the key returned by the custom pluralization function. 33 | 34 | Let's get started! 🚀 35 | */ 36 | 37 | // The custom pluralization works in the same way as the default pluralization 38 | // The only difference is that you can define your own pluralization rules 39 | // Start creating a function that returns a string and receives a int 40 | func MyCustomPluralization(count int) string { 41 | // The count variable is the same as the Count field in the Options 42 | // The string returned by this function will be used as the pluralization key 43 | if count == 0 { 44 | return "Zero" 45 | } 46 | if count == 1 { 47 | return "One" 48 | } 49 | if count == 2 { 50 | return "Two" 51 | } 52 | if count == 3 { 53 | return "Few" 54 | } 55 | return "Many" 56 | } 57 | 58 | func main() { 59 | // 1. Create a new i18n instance 60 | i18n := goeasyi18n.NewI18n() 61 | 62 | // 2. Create your translations 63 | // If something goes wrong, the default value is used 64 | // The default pluralization only works for one and many keys 65 | // If the count (later in the Options) is 1, the one key is used 66 | // If the count (later in the Options) is greater than 1, the many key is used 67 | enTranslations := goeasyi18n.TranslateStrings{ 68 | { 69 | Key: "hello_emails", 70 | Default: "Hello, you have emails", 71 | Zero: "Hello, you have no emails", 72 | One: "Hello, you have one email", 73 | Two: "Hello, you have two emails", 74 | Few: "Hello, you have three emails", 75 | Many: "Hello, you have {{.EmailQty}} emails", 76 | }, 77 | } 78 | 79 | esTranslations := goeasyi18n.TranslateStrings{ 80 | { 81 | Key: "hello_emails", 82 | Default: "Hola, tienes correos", 83 | Zero: "Hola, no tienes correos", 84 | One: "Hola, tienes un correo", 85 | Two: "Hola, tienes 2 correos", 86 | Few: "Hola, tienes 3 correos", 87 | Many: "Hola, tienes {{.EmailQty}} correos", 88 | }, 89 | } 90 | 91 | // 3. Add your languages with their translations 92 | i18n.AddLanguage("en", enTranslations) 93 | i18n.AddLanguage("es", esTranslations) 94 | 95 | // 4. Add your custom pluralization function 96 | // This method sets the pluralization function for the given language 97 | // In this case, we are setting the pluralization function for the "en" language 98 | // The "es" language will still use the default pluralization to see the differences 99 | i18n.SetPluralizationFunc("en", MyCustomPluralization) 100 | 101 | // 5. Create the Options (we are using a helper that lives at end of this file) 102 | zeroEmailOptions := MakeOptions(0) 103 | oneEmailOptions := MakeOptions(1) 104 | twoEmailOptions := MakeOptions(2) 105 | fewEmailsOptions := MakeOptions(3) 106 | manyEmailsOptions := MakeOptions(100) 107 | 108 | // 6. You are done! 🎉 Just get that translations! 109 | ten0 := i18n.T("en", "hello_emails", zeroEmailOptions) 110 | ten1 := i18n.T("en", "hello_emails", oneEmailOptions) 111 | ten2 := i18n.T("en", "hello_emails", twoEmailOptions) 112 | tenf := i18n.T("en", "hello_emails", fewEmailsOptions) 113 | tenm := i18n.T("en", "hello_emails", manyEmailsOptions) 114 | 115 | tes0 := i18n.T("es", "hello_emails", zeroEmailOptions) 116 | tes1 := i18n.T("es", "hello_emails", oneEmailOptions) 117 | tes2 := i18n.T("es", "hello_emails", twoEmailOptions) 118 | tesf := i18n.T("es", "hello_emails", fewEmailsOptions) 119 | tesm := i18n.T("es", "hello_emails", manyEmailsOptions) 120 | 121 | fmt.Println(ten0) 122 | fmt.Println(ten1) 123 | fmt.Println(ten2) 124 | fmt.Println(tenf) 125 | fmt.Println(tenm) 126 | fmt.Println(tes0) 127 | fmt.Println(tes1) 128 | fmt.Println(tes2) 129 | fmt.Println(tesf) 130 | fmt.Println(tesm) 131 | 132 | /* 133 | Prints: 134 | Hello, you have no emails 135 | Hello, you have one email 136 | Hello, you have two emails 137 | Hello, you have three emails 138 | Hello, you have 100 emails 139 | Hola, tienes 0 correos 140 | Hola, tienes un correo 141 | Hola, tienes 2 correos 142 | Hola, tienes 3 correos 143 | Hola, tienes 100 correos 144 | */ 145 | 146 | /* 147 | Note how the "en" language uses the custom pluralization function and the "es" language 148 | uses the default pluralization function. This is because we only set the custom 149 | pluralization function for the "en" language. 150 | 151 | You can also set the custom pluralization function for all the languages you want. 152 | 153 | The default pluralization function only checks if the count is 1, then returns 154 | the "One" key, otherwise it returns the "Many" key. Is that simple! 155 | */ 156 | } 157 | 158 | // Helper function to create the Options for pluralization 159 | func MakeOptions(count int) goeasyi18n.Options { 160 | options := goeasyi18n.Options{ 161 | Count: &count, 162 | Data: map[string]any{"EmailQty": count}, 163 | } 164 | return options 165 | } 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Tests status 4 | 5 | 6 | Latest releases 7 | 8 | 9 | Go package documentation 10 | 11 |

12 | 13 | # Go Easy i18n 14 | 15 | Go Easy i18n 16 | 17 |
18 | 19 | Effortlessly simple i18n for Go. Plurals, gender, and more made easy! 20 | 21 | - 🚀 Making Internationalization a Breeze in Go! 22 | - 🔥 Simple yet flexible i18n library for Go developers. 23 | - 📦 Supports pluralization, gender, and more with zero hassle! 24 | - 👩‍💻 Perfect for projects that need quick and easy localization. 25 | - 🚫 No weird or complicated usage. Just simple and easy to use. 26 | - 🌍 Go global, the easy way! 27 | 28 | ## Installation 📦 29 | 30 | ```bash 31 | go get github.com/eduardolat/goeasyi18n 32 | ``` 33 | 34 | ## QuickStart 🚀 35 | 36 | ```go 37 | package main 38 | 39 | import ( 40 | "fmt" 41 | 42 | "github.com/eduardolat/goeasyi18n" 43 | ) 44 | 45 | func main() { 46 | // 1. Create a new i18n instance 47 | // You can skip the goeasyi18n.Config{} entirely if you are 48 | // ok with the default values. 49 | i18n := goeasyi18n.NewI18n(goeasyi18n.Config{ 50 | FallbackLanguageName: "en", // It's optional, the default value is "en" 51 | DisableConsistencyCheck: false, // It's optional, the default value is false 52 | }) 53 | 54 | // 2. Add languages and its translations (can be loaded from a JSON/YAML file) 55 | // using bytes, strings, files or fs.FS (embed.FS) 56 | i18n.AddLanguage("en", goeasyi18n.TranslateStrings{ 57 | { 58 | Key: "hello_message", 59 | Default: "Hello, welcome to Go Easy i18n!", 60 | }, 61 | }) 62 | 63 | i18n.AddLanguage("es", goeasyi18n.TranslateStrings{ 64 | { 65 | Key: "hello_message", 66 | Default: "¡Hola, bienvenido a Go Easy i18n!", 67 | }, 68 | }) 69 | 70 | // 3. You are done! 🎉 Just get that translations! 71 | t1 := i18n.Translate("en", "hello_message", goeasyi18n.Options{}) 72 | t2 := i18n.Translate("es", "hello_message") // You can skip the options if you don't need them 73 | 74 | fmt.Println(t1) 75 | fmt.Println(t2) 76 | 77 | /* 78 | Prints: 79 | Hello, welcome to Go Easy i18n! 80 | ¡Hola, bienvenido a Go Easy i18n! 81 | */ 82 | } 83 | ``` 84 | 85 | ## Learn Go ***(Easy i18n)*** by Examples 📚 86 | 87 | This is the complete list of examples that covers **all** the features of Go Easy i18n from the most basic to the most advanced usage. If you want to be a pro, you should read them all! 88 | 89 | Combining these features you can go as simple or complex as you want. 90 | 91 | **Tip:** Read it in order, from top to bottom. 92 | 93 | - [Basic Usage](/examples/01-basic-usage/main.go) 94 | - [Variable Interpolation](/examples/02-variable-interpolation/main.go) 95 | - [Load translations from JSON/YAML](/examples/03-json-yaml-loaders/README.md) 96 | - [Default Pluralization](/examples/04-default-pluralization/main.go) 97 | - [Custom Pluralization](/examples/05-custom-pluralization/main.go) 98 | - [Genders](/examples/06-genders/main.go) 99 | - [Pluralized Genders](/examples/07-pluralized-genders/main.go) 100 | - [Templating Usage](/examples/08-templating/README.md) 101 | - [Advanced Example - Website](/examples/09-advanced-example/README.md) 102 | 103 | You are done! 🎉🎉🎉 104 | 105 | If you find this library useful, please 🙏 give it a star ⭐️ on [GitHub](https://github.com/eduardolat/goeasyi18n) and share it with your friends. 106 | 107 | ## Want to Contribute? 🌟 108 | 109 | We're thrilled you're considering contributing to Go Easy i18n! Your help is invaluable. Whether it's through code improvements, bug fixes, or enhancing documentation, every contribution counts. If you're unsure where to start or have any questions, feel free to open an issue. For detailed guidelines on making contributions, please read our [CONTRIBUTING.md](CONTRIBUTING.md) file. 110 | 111 | ## FAQ ❓ 112 | 113 | ### Why should I use this library? 114 | 115 | This library makes internationalization in Go extremely simple and flexible. It supports pluralization, gender, and more, all without any hassle. It's designed to be easy to use, so you can go global without breaking a sweat. 116 | 117 | ### Am i enforced to build my project in a specific way? 118 | 119 | No, this library is designed to be as flexible as possible. You can integrate it into your existing project structure without any major changes. 120 | 121 | ### How can i name my languages? 122 | 123 | You have complete freedom to name your languages as you see fit. For example, you could use URLs like example.com/en or example.com/en-US to identify languages. However, you're responsible for extracting this segment from the URL and using it in your application. 124 | 125 | ### How can i name my translation files? 126 | 127 | You can name your translation files however you like. The library is agnostic to file naming conventions. 128 | 129 | ### From where should i load my translations? 130 | 131 | Translations can be loaded from any JSON or YAML file. You have the flexibility to create your own database or any other mechanism that generates these files, and then load them into the library. 132 | 133 | ### Custom Pluralization: Why is it useful? 134 | 135 | Imagine you're building an app that shows the number of unread messages. 136 | In English, you'd say "1 unread message" and "2 unread messages" - note the "s" at the end. 137 | But what about languages where plural rules aren't so simple? 138 | 139 | With custom pluralization, you can easily handle these cases without writing complex if-else 140 | statements. Just define your plural rules once, and let the library do the heavy lifting! 141 | 142 | This makes your code cleaner and your app more linguistically accurate. Win-win! 143 | 144 | ### Gender Handling: Why is it useful? 145 | 146 | Let's say your app has a feature that says, "John liked your post" or "Emily liked your post." 147 | In some languages, the verb "liked" might change based on the gender of the person who 148 | liked the post. 149 | 150 | Example: 151 | - English: "He liked your post" vs "She liked your post" 152 | - Spanish: "A él le gustó tu publicación" vs "A ella le gustó tu publicación" 153 | 154 | With gender-specific translations, you can easily adapt the sentence structure to fit the 155 | gender, making your app more linguistically accurate and inclusive. 156 | 157 | No need for messy if-else statements to handle gender variations. Just set it up once, and 158 | the library takes care of the rest! 159 | -------------------------------------------------------------------------------- /i18n.go: -------------------------------------------------------------------------------- 1 | package goeasyi18n 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | type I18n struct { 11 | languages map[string]TranslateStrings 12 | pluralizationFuncs map[string]PluralizationFunc 13 | fallbackLanguageName string 14 | disableConsistencyCheck bool 15 | } 16 | 17 | // Config is used to configure the i18n object 18 | type Config struct { 19 | // Default: "en" 20 | FallbackLanguageName string 21 | // Default: false 22 | DisableConsistencyCheck bool 23 | } 24 | 25 | // NewI18n creates and returns a new i18n object 26 | func NewI18n(config ...Config) *I18n { 27 | var pickedConfig Config 28 | if len(config) > 0 { 29 | pickedConfig = config[0] 30 | } else { 31 | pickedConfig = Config{} 32 | } 33 | 34 | instance := I18n{ 35 | languages: make(map[string]TranslateStrings), 36 | pluralizationFuncs: make(map[string]PluralizationFunc), 37 | disableConsistencyCheck: pickedConfig.DisableConsistencyCheck, 38 | } 39 | 40 | if pickedConfig.FallbackLanguageName != "" { 41 | instance.fallbackLanguageName = pickedConfig.FallbackLanguageName 42 | } else { 43 | instance.fallbackLanguageName = "en" 44 | } 45 | 46 | return &instance 47 | } 48 | 49 | // DefaultPluralizationFunc is the function that is used if no 50 | // custom pluralization function is set for the language 51 | func DefaultPluralizationFunc(count int) string { 52 | if count == 1 { 53 | return "One" 54 | } 55 | return "Many" 56 | } 57 | 58 | // createGenderForm function to determinate the gender 59 | // and sanitize it 60 | func createGenderForm(input string) string { 61 | 62 | input = strings.ToLower(input) 63 | 64 | if input == "male" { 65 | return "Male" 66 | } 67 | 68 | if input == "female" { 69 | return "Female" 70 | } 71 | 72 | if input == "nonbinary" || input == "non-binary" { 73 | return "NonBinary" 74 | } 75 | 76 | // If the gender is not valid, return empty string 77 | // it causes the use of the Default form of the 78 | // translation 79 | return "" 80 | 81 | } 82 | 83 | // CheckLanguageConsistency checks if a language is consistent 84 | // with the other languages, it checks if the translations keys 85 | // are the same in all languages 86 | func (t *I18n) CheckLanguageConsistency( 87 | langNameToCheck string, 88 | ) (bool, []string) { 89 | langToCheck, exists := t.languages[langNameToCheck] 90 | if !exists { 91 | return false, []string{ 92 | "goeasyi18n: the language '" + langNameToCheck + "' doesn't exist", 93 | } 94 | } 95 | 96 | inconsistencies := []string{} 97 | 98 | for langName, lang := range t.languages { 99 | if langName == langNameToCheck { 100 | continue 101 | } 102 | 103 | // Check if the new language has more keys 104 | // than existing languages 105 | for _, translateStringToCheck := range langToCheck { 106 | found := false 107 | for _, translateString := range lang { 108 | if translateString.Key == translateStringToCheck.Key { 109 | found = true 110 | break 111 | } 112 | } 113 | if !found { 114 | inconsistencies = append( 115 | inconsistencies, 116 | fmt.Sprintf( 117 | "goeasyi18n: the language '%s' has the key '%s' that doesn't exist in '%s'", 118 | langNameToCheck, 119 | translateStringToCheck.Key, 120 | langName, 121 | ), 122 | ) 123 | } 124 | } 125 | 126 | // Check if the new language has less keys 127 | // than existing languages 128 | for _, translateString := range lang { 129 | found := false 130 | for _, translateStringToCheck := range langToCheck { 131 | if translateString.Key == translateStringToCheck.Key { 132 | found = true 133 | break 134 | } 135 | } 136 | if !found { 137 | inconsistencies = append( 138 | inconsistencies, 139 | fmt.Sprintf( 140 | "goeasyi18n: the language '%s' has the key '%s' that doesn't exist in '%s'", 141 | langName, 142 | translateString.Key, 143 | langNameToCheck, 144 | ), 145 | ) 146 | } 147 | } 148 | } 149 | 150 | isConsistent := len(inconsistencies) == 0 151 | return isConsistent, inconsistencies 152 | } 153 | 154 | // AddLanguage adds a language to the i18n object with its translations 155 | // and after that it check if the language is consistent with 156 | // the other languages (can be disabled with the config) 157 | // 158 | // It returns a slice of errors as strings if the language is 159 | // not consistent with the other languages and the consistency 160 | // check is enabled 161 | func (t *I18n) AddLanguage( 162 | languageName string, 163 | translateStrings TranslateStrings, 164 | ) []string { 165 | t.languages[languageName] = translateStrings 166 | t.SetPluralizationFunc(languageName, DefaultPluralizationFunc) 167 | 168 | if t.disableConsistencyCheck == false { 169 | isConsistent, errors := t.CheckLanguageConsistency(languageName) 170 | if isConsistent == false { 171 | errorMsg := strings.Join(errors, "\n") 172 | fmt.Println(errorMsg) 173 | } 174 | return errors 175 | } 176 | 177 | return nil 178 | } 179 | 180 | // HasLanguage checks if a language is available (if is loaded) 181 | func (t *I18n) HasLanguage(languageName string) bool { 182 | _, ok := t.languages[languageName] 183 | return ok 184 | } 185 | 186 | // SetPluralizationFunc sets the pluralization function for a language 187 | func (t *I18n) SetPluralizationFunc(languageName string, fn PluralizationFunc) { 188 | t.pluralizationFuncs[languageName] = fn 189 | } 190 | 191 | // Options are the additional options for the Translate function 192 | type Options struct { 193 | Data any 194 | Count *int 195 | Gender *string // male, female, nonbinary, non-binary (case insensitive) 196 | } 197 | 198 | // Translate translates a string in a specific language using a key 199 | // and additional optional options 200 | func (t *I18n) Translate( 201 | languageName string, 202 | translateKey string, 203 | options ...Options, 204 | ) string { 205 | // Initialize options if not provided 206 | var pickedOptions Options 207 | if len(options) > 0 { 208 | pickedOptions = options[0] 209 | } else { 210 | pickedOptions = Options{} 211 | } 212 | 213 | // Get lang and fallback if not found 214 | lang, okLang := t.languages[languageName] 215 | fallbackLang, okFallbackLang := t.languages[t.fallbackLanguageName] 216 | if !okLang && !okFallbackLang { 217 | return "" 218 | } 219 | if !okLang { 220 | copy(lang, fallbackLang) 221 | languageName = t.fallbackLanguageName 222 | } 223 | 224 | // Get the translate string from key or fallback if not found 225 | var translateString TranslateString 226 | for _, ts := range lang { 227 | if ts.Key == translateKey { 228 | translateString = ts 229 | break 230 | } 231 | } 232 | if translateString.Key == "" { 233 | for _, ts := range fallbackLang { 234 | if ts.Key == translateKey { 235 | translateString = ts 236 | break 237 | } 238 | } 239 | } 240 | if translateString.Key == "" { 241 | return "" 242 | } 243 | 244 | // Get the string key to be used 245 | mode := "Default" // Default - Pluralized - Gendered - PluralizedGendered 246 | if pickedOptions.Count != nil && pickedOptions.Gender != nil { 247 | mode = "PluralizedGendered" 248 | } 249 | if mode == "Default" && pickedOptions.Count != nil { 250 | mode = "Pluralized" 251 | } 252 | if mode == "Default" && pickedOptions.Gender != nil { 253 | mode = "Gendered" 254 | } 255 | 256 | // Get the plural and gender forms to be used if needed 257 | var pluralForm, genderForm string 258 | if mode == "Pluralized" || mode == "PluralizedGendered" { 259 | pluralizationFunc := t.pluralizationFuncs[languageName] 260 | pluralForm = pluralizationFunc(*pickedOptions.Count) 261 | } 262 | if mode == "Gendered" || mode == "PluralizedGendered" { 263 | genderForm = createGenderForm(*pickedOptions.Gender) 264 | } 265 | 266 | // Get the string key to be used 267 | stringKey := "Default" 268 | if mode == "Pluralized" { 269 | stringKey = pluralForm 270 | } 271 | if mode == "Gendered" && genderForm != "" { 272 | stringKey = genderForm 273 | } 274 | if mode == "PluralizedGendered" && genderForm != "" { 275 | stringKey = pluralForm + genderForm 276 | } 277 | 278 | // Get the translation 279 | translation := translateString.Default 280 | reflected := reflect.ValueOf(translateString) 281 | field := reflected.FieldByName(stringKey) 282 | if field.IsValid() { 283 | translation = field.String() 284 | } 285 | 286 | // Execute the template 287 | if pickedOptions.Data != nil { 288 | translation = ExecuteTemplate(translation, pickedOptions.Data) 289 | } 290 | 291 | return translation 292 | } 293 | 294 | // T is a shortcut for Translate 295 | func (t *I18n) T( 296 | languageName string, 297 | translateKey string, 298 | options ...Options, 299 | ) string { 300 | return t.Translate(languageName, translateKey, options...) 301 | } 302 | 303 | // NewTemplatingTranslateFunc creates a function that can be used in text/template and html/template. 304 | // Just pass the created function to template.FuncMap. 305 | // 306 | // Example: 307 | // 308 | // tempTemplate := template.New("main").Funcs( 309 | // 310 | // template.FuncMap{ 311 | // // 👇 "Translate" could be just "T" (for simplicity) or any other name you want. 312 | // "Translate": i18n.NewTemplatingTranslateFunc(), 313 | // }, 314 | // 315 | // ) 316 | // 317 | // Then you can use it in your template like this: 318 | // 319 | // {{ Translate "lang" "en" "key" "hello_emails" "gender" "nonbinary" "count" "100" "SomeData" "Anything" }} 320 | // 321 | // The format is key-value based and the order doesn't matter. 322 | // This is the format: 323 | // 324 | // {{ Translate "key1" "value1" "key2" "value2" ... }} 325 | // 326 | // Arguments: 327 | // 328 | // - "lang" "en": Language code (e.g., "en", "es"). 329 | // 330 | // - "key" "hello_emails": Translation key. 331 | // 332 | // - "gender" "nonbinary": Gender for the translation (optional). 333 | // 334 | // - "count" "100": Count for pluralization (optional). 335 | // 336 | // - Additional key-value pairs will be added to the Data map. 337 | // 338 | // Arguments are passed in pairs. The first item in each pair is the key, and the second is the value. 339 | // 340 | // Key-Value Explanation: 341 | // 342 | // - Each argument is processed as a pair: the first string is considered the key and the second string is the value. 343 | // 344 | // - For example, in "lang" "en", "lang" is the key and "en" is the value. 345 | // 346 | // As you can imagine, "lang", "key", "gender", and "count" are reserved keys. 347 | // You can use any other key you want to pass data to translation. 348 | // 349 | // Note: All arguments are strings. The function will attempt to convert "count" to an integer. 350 | func (t *I18n) NewTemplatingTranslateFunc() func(args ...interface{}) string { 351 | return func(args ...interface{}) string { 352 | var lang, key string 353 | var gender *string 354 | var count *int 355 | data := make(Data) 356 | 357 | for i := 0; i < len(args); i += 2 { 358 | if i+1 >= len(args) { 359 | break 360 | } 361 | 362 | keyStr, ok1 := args[i].(string) 363 | valueStr, ok2 := args[i+1].(string) 364 | 365 | if !ok1 || !ok2 { 366 | continue 367 | } 368 | 369 | switch keyStr { 370 | case "lang": 371 | lang = valueStr 372 | case "key": 373 | key = valueStr 374 | case "count": 375 | intVal, err := strconv.Atoi(valueStr) 376 | if err == nil { 377 | count = &intVal 378 | } 379 | case "gender": 380 | gender = &valueStr 381 | default: 382 | data[keyStr] = valueStr 383 | } 384 | } 385 | 386 | options := Options{ 387 | Count: count, 388 | Gender: gender, 389 | Data: data, 390 | } 391 | 392 | return t.Translate(lang, key, options) 393 | } 394 | } 395 | 396 | // NewLangTranslateFunc creates a function to translate a string in a specific language 397 | // without the need to pass the language name every time. 398 | func (t *I18n) NewLangTranslateFunc( 399 | languageName string, 400 | ) func( 401 | translateKey string, 402 | options ...Options, 403 | ) string { 404 | return func(translateKey string, options ...Options) string { 405 | return t.Translate(languageName, translateKey, options...) 406 | } 407 | } 408 | -------------------------------------------------------------------------------- /i18n_test.go: -------------------------------------------------------------------------------- 1 | package goeasyi18n 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "html/template" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestTranslate(t *testing.T) { 12 | t.Run("simple general tests", func(t *testing.T) { 13 | i18n := NewI18n() 14 | 15 | // Add English translations 16 | i18n.AddLanguage("en", TranslateStrings{ 17 | TranslateString{ 18 | Key: "welcome", 19 | Default: "Welcome", 20 | Male: "Welcome, sir", 21 | Female: "Welcome, ma'am", 22 | }, 23 | TranslateString{ 24 | Key: "emails", 25 | One: "You have one email", 26 | Many: "You have many emails", 27 | }, 28 | TranslateString{ 29 | Key: "greetings", 30 | Default: "Hello {{.Name}}", 31 | }, 32 | }) 33 | 34 | // Add Spanish translations 35 | i18n.AddLanguage("es", TranslateStrings{ 36 | TranslateString{ 37 | Key: "welcome", 38 | Default: "Bienvenido", 39 | Male: "Bienvenido, señor", 40 | Female: "Bienvenida, señora", 41 | }, 42 | TranslateString{ 43 | Key: "emails", 44 | One: "Tienes un correo", 45 | Many: "Tienes muchos correos", 46 | }, 47 | TranslateString{ 48 | Key: "greetings", 49 | Default: "Hola {{.Name}}", 50 | }, 51 | }) 52 | 53 | tests := []struct { 54 | lang string 55 | key string 56 | options Options 57 | expected string 58 | }{ 59 | {"en", "welcome", Options{}, "Welcome"}, 60 | {"en", "welcome", Options{Gender: createPtr("male")}, "Welcome, sir"}, 61 | {"en", "welcome", Options{Gender: createPtr("female")}, "Welcome, ma'am"}, 62 | {"en", "emails", Options{Count: createPtr(1)}, "You have one email"}, 63 | {"en", "emails", Options{Count: createPtr(5)}, "You have many emails"}, 64 | {"es", "welcome", Options{}, "Bienvenido"}, 65 | {"es", "welcome", Options{Gender: createPtr("Male")}, "Bienvenido, señor"}, 66 | {"es", "welcome", Options{Gender: createPtr("Female")}, "Bienvenida, señora"}, 67 | {"es", "emails", Options{Count: createPtr(1)}, "Tienes un correo"}, 68 | {"es", "emails", Options{Count: createPtr(5)}, "Tienes muchos correos"}, 69 | // Test fallback language 70 | {"xxx", "welcome", Options{}, "Welcome"}, 71 | // Test fallback key 72 | {"en", "xxx", Options{}, ""}, 73 | // Test data interpolation 74 | {"en", "greetings", Options{Data: map[string]string{"Name": "John"}}, "Hello John"}, 75 | {"es", "greetings", Options{Data: map[string]string{"Name": "John"}}, "Hola John"}, 76 | } 77 | 78 | for _, test := range tests { 79 | t.Run(test.key, func(t *testing.T) { 80 | got := i18n.Translate(test.lang, test.key, test.options) 81 | if got != test.expected { 82 | t.Errorf("expected %s; got %s", test.expected, got) 83 | } 84 | }) 85 | } 86 | }) 87 | 88 | t.Run("method HasLanguage should work", func(t *testing.T) { 89 | i18n := NewI18n() 90 | i18n.AddLanguage("en", TranslateStrings{}) 91 | i18n.AddLanguage("es", TranslateStrings{}) 92 | 93 | if !i18n.HasLanguage("en") { 94 | t.Errorf("expected language en to exist") 95 | } 96 | 97 | if !i18n.HasLanguage("es") { 98 | t.Errorf("expected language es to exist") 99 | } 100 | 101 | if i18n.HasLanguage("xxx") { 102 | t.Errorf("expected language xxx to not exist") 103 | } 104 | }) 105 | 106 | t.Run("english should be the default fallback language even if multiple langs are added", func(t *testing.T) { 107 | i18n := NewI18n() 108 | 109 | i18n.AddLanguage("en", TranslateStrings{ 110 | TranslateString{ 111 | Key: "welcome", 112 | Default: "Welcome", 113 | }, 114 | }) 115 | 116 | i18n.AddLanguage("es", TranslateStrings{ 117 | TranslateString{ 118 | Key: "welcome", 119 | Default: "Bienvenido", 120 | }, 121 | }) 122 | 123 | got := i18n.Translate("xxx", "welcome") 124 | expected := "Welcome" 125 | 126 | if got != expected { 127 | t.Errorf("expected %s; got %s", expected, got) 128 | } 129 | }) 130 | 131 | t.Run("the custom fallback language should be used if set", func(t *testing.T) { 132 | i18n := NewI18n(Config{FallbackLanguageName: "es"}) 133 | 134 | i18n.AddLanguage("en", TranslateStrings{ 135 | TranslateString{ 136 | Key: "welcome", 137 | Default: "Welcome", 138 | }, 139 | }) 140 | 141 | i18n.AddLanguage("es", TranslateStrings{ 142 | TranslateString{ 143 | Key: "welcome", 144 | Default: "Bienvenido", 145 | }, 146 | }) 147 | 148 | got := i18n.Translate("xxx", "welcome") 149 | expected := "Bienvenido" 150 | 151 | if got != expected { 152 | t.Errorf("expected %s; got %s", expected, got) 153 | } 154 | }) 155 | 156 | t.Run("should fallback complex use case", func(t *testing.T) { 157 | i18n := NewI18n() 158 | 159 | i18n.AddLanguage("en", TranslateStrings{ 160 | TranslateString{ 161 | Key: "welcomefallbacked", 162 | ManyMale: "Welcome, you have {{.EmailQty}} emails, sir {{.Name}}", 163 | }, 164 | }) 165 | 166 | got := i18n.Translate("xxx", "welcomefallbacked", Options{ 167 | Count: createPtr(5), 168 | Gender: createPtr("male"), 169 | Data: map[string]string{ 170 | "EmailQty": "5", 171 | "Name": "John", 172 | }, 173 | }) 174 | expected := "Welcome, you have 5 emails, sir John" 175 | 176 | if got != expected { 177 | t.Errorf("expected %s; got %s", expected, got) 178 | } 179 | }) 180 | 181 | t.Run("the pluralization should work with default options (only one and many)", func(t *testing.T) { 182 | i18n := NewI18n() 183 | 184 | i18n.AddLanguage("en", TranslateStrings{ 185 | TranslateString{ 186 | Key: "print_emails", 187 | Default: "You have emails", 188 | One: "You have one email", 189 | Many: "You have {{.EmailQty}} emails", 190 | }, 191 | }) 192 | 193 | tests := []struct { 194 | lang string 195 | key string 196 | options Options 197 | expected string 198 | }{ 199 | {"en", "print_emails", Options{}, "You have emails"}, 200 | {"en", "print_emails", Options{Count: createPtr(0), Data: Data{"EmailQty": 0}}, "You have 0 emails"}, 201 | {"en", "print_emails", Options{Count: createPtr(1), Data: Data{"EmailQty": 1}}, "You have one email"}, 202 | {"en", "print_emails", Options{Count: createPtr(5), Data: Data{"EmailQty": 5}}, "You have 5 emails"}, 203 | } 204 | 205 | for _, test := range tests { 206 | t.Run(test.key, func(t *testing.T) { 207 | got := i18n.Translate(test.lang, test.key, test.options) 208 | if got != test.expected { 209 | t.Errorf("expected %s; got %s", test.expected, got) 210 | } 211 | }) 212 | } 213 | }) 214 | 215 | t.Run("the pluralization should work with custom pluralization function", func(t *testing.T) { 216 | i18n := NewI18n() 217 | 218 | myPluralizationFn := func(count int) string { 219 | if count == 0 { 220 | return "Zero" 221 | } 222 | if count == 1 { 223 | return "One" 224 | } 225 | if count == 2 { 226 | return "Two" 227 | } 228 | if count == 3 { 229 | return "Few" 230 | } 231 | return "Many" 232 | } 233 | 234 | i18n.AddLanguage("en", TranslateStrings{ 235 | TranslateString{ 236 | Key: "print_emails", 237 | Default: "You have emails", 238 | Zero: "You have no emails", 239 | One: "You have one email", 240 | Two: "You have two emails", 241 | Few: "You have three emails", 242 | Many: "You have {{.EmailQty}} emails", 243 | }, 244 | }) 245 | 246 | i18n.SetPluralizationFunc("en", myPluralizationFn) 247 | 248 | tests := []struct { 249 | lang string 250 | key string 251 | options Options 252 | expected string 253 | }{ 254 | {"en", "print_emails", Options{}, "You have emails"}, 255 | {"en", "print_emails", Options{Count: createPtr(0), Data: Data{"EmailQty": 0}}, "You have no emails"}, 256 | {"en", "print_emails", Options{Count: createPtr(1), Data: Data{"EmailQty": 1}}, "You have one email"}, 257 | {"en", "print_emails", Options{Count: createPtr(2), Data: Data{"EmailQty": 2}}, "You have two emails"}, 258 | {"en", "print_emails", Options{Count: createPtr(3), Data: Data{"EmailQty": 3}}, "You have three emails"}, 259 | {"en", "print_emails", Options{Count: createPtr(100), Data: Data{"EmailQty": 100}}, "You have 100 emails"}, 260 | } 261 | 262 | for _, test := range tests { 263 | t.Run(test.key, func(t *testing.T) { 264 | got := i18n.Translate(test.lang, test.key, test.options) 265 | if got != test.expected { 266 | t.Errorf("expected %s; got %s", test.expected, got) 267 | } 268 | }) 269 | } 270 | }) 271 | 272 | t.Run("should handle gendered translations in multiple languages", func(t *testing.T) { 273 | i18n := NewI18n() 274 | 275 | i18n.AddLanguage("en", TranslateStrings{ 276 | TranslateString{ 277 | Key: "hello_message", 278 | Default: "Hello", 279 | Male: "Hello sir", 280 | Female: "Hello ma'am", 281 | NonBinary: "Hello friend", 282 | }, 283 | }) 284 | 285 | i18n.AddLanguage("es", TranslateStrings{ 286 | TranslateString{ 287 | Key: "hello_message", 288 | Default: "Hola", 289 | Male: "Hola amigo", 290 | Female: "Hola amiga", 291 | NonBinary: "Hola amigue", 292 | }, 293 | }) 294 | 295 | tests := []struct { 296 | lang string 297 | key string 298 | options Options 299 | expected string 300 | }{ 301 | {"en", "hello_message", Options{}, "Hello"}, 302 | {"en", "hello_message", Options{Gender: createPtr("somethingelse")}, "Hello"}, 303 | {"en", "hello_message", Options{Gender: createPtr("male")}, "Hello sir"}, 304 | {"en", "hello_message", Options{Gender: createPtr("female")}, "Hello ma'am"}, 305 | {"en", "hello_message", Options{Gender: createPtr("nonbinary")}, "Hello friend"}, 306 | {"en", "hello_message", Options{Gender: createPtr("non-binary")}, "Hello friend"}, 307 | {"es", "hello_message", Options{}, "Hola"}, 308 | {"es", "hello_message", Options{Gender: createPtr("somethingelse")}, "Hola"}, 309 | {"es", "hello_message", Options{Gender: createPtr("male")}, "Hola amigo"}, 310 | {"es", "hello_message", Options{Gender: createPtr("female")}, "Hola amiga"}, 311 | {"es", "hello_message", Options{Gender: createPtr("nonbinary")}, "Hola amigue"}, 312 | {"es", "hello_message", Options{Gender: createPtr("non-binary")}, "Hola amigue"}, 313 | } 314 | 315 | for _, test := range tests { 316 | t.Run(test.key, func(t *testing.T) { 317 | got := i18n.Translate(test.lang, test.key, test.options) 318 | if got != test.expected { 319 | t.Errorf("expected %s; got %s", test.expected, got) 320 | } 321 | }) 322 | } 323 | }) 324 | 325 | t.Run("should handle complex pluralized-gendered translations in multiple languages", func(t *testing.T) { 326 | i18n := NewI18n() 327 | 328 | myPluralizationFn := func(count int) string { 329 | if count == 0 { 330 | return "Zero" 331 | } 332 | if count == 1 { 333 | return "One" 334 | } 335 | if count == 2 { 336 | return "Two" 337 | } 338 | if count == 3 { 339 | return "Few" 340 | } 341 | return "Many" 342 | } 343 | 344 | i18n.AddLanguage("en", TranslateStrings{ 345 | TranslateString{ 346 | Key: "hello_emails", 347 | Default: "Hello, you have emails", 348 | ZeroMale: "Hello, you have no emails, sir", 349 | OneMale: "Hello, you have one email, sir", 350 | TwoMale: "Hello, you have two emails, sir", 351 | FewMale: "Hello, you have three emails, sir", 352 | ManyMale: "Hello, you have {{.EmailQty}} emails, sir", 353 | ZeroFemale: "Hello, you have no emails, ma'am", 354 | OneFemale: "Hello, you have one email, ma'am", 355 | TwoFemale: "Hello, you have two emails, ma'am", 356 | FewFemale: "Hello, you have three emails, ma'am", 357 | ManyFemale: "Hello, you have {{.EmailQty}} emails, ma'am", 358 | ZeroNonBinary: "Hello, you have no emails, friend", 359 | OneNonBinary: "Hello, you have one email, friend", 360 | TwoNonBinary: "Hello, you have two emails, friend", 361 | FewNonBinary: "Hello, you have three emails, friend", 362 | ManyNonBinary: "Hello, you have {{.EmailQty}} emails, friend", 363 | }, 364 | }) 365 | 366 | i18n.AddLanguage("es", TranslateStrings{ 367 | TranslateString{ 368 | Key: "hello_emails", 369 | Default: "Hola, tienes correos", 370 | ZeroMale: "Hola, no tienes correos, amigo", 371 | OneMale: "Hola, tienes un correo, amigo", 372 | TwoMale: "Hola, tienes dos correos, amigo", 373 | FewMale: "Hola, tienes tres correos, amigo", 374 | ManyMale: "Hola, tienes {{.EmailQty}} correos, amigo", 375 | ZeroFemale: "Hola, no tienes correos, amiga", 376 | OneFemale: "Hola, tienes un correo, amiga", 377 | TwoFemale: "Hola, tienes dos correos, amiga", 378 | FewFemale: "Hola, tienes tres correos, amiga", 379 | ManyFemale: "Hola, tienes {{.EmailQty}} correos, amiga", 380 | ZeroNonBinary: "Hola, no tienes correos, amigue", 381 | OneNonBinary: "Hola, tienes un correo, amigue", 382 | TwoNonBinary: "Hola, tienes dos correos, amigue", 383 | FewNonBinary: "Hola, tienes tres correos, amigue", 384 | ManyNonBinary: "Hola, tienes {{.EmailQty}} correos, amigue", 385 | }, 386 | }) 387 | 388 | i18n.SetPluralizationFunc("en", myPluralizationFn) 389 | i18n.SetPluralizationFunc("es", myPluralizationFn) 390 | 391 | tests := []struct { 392 | lang string 393 | key string 394 | options Options 395 | expected string 396 | }{ 397 | {"en", "hello_emails", Options{}, "Hello, you have emails"}, 398 | {"en", "hello_emails", Options{Gender: createPtr("male"), Count: createPtr(0)}, "Hello, you have no emails, sir"}, 399 | {"en", "hello_emails", Options{Gender: createPtr("male"), Count: createPtr(1)}, "Hello, you have one email, sir"}, 400 | {"en", "hello_emails", Options{Gender: createPtr("male"), Count: createPtr(2)}, "Hello, you have two emails, sir"}, 401 | {"en", "hello_emails", Options{Gender: createPtr("male"), Count: createPtr(3)}, "Hello, you have three emails, sir"}, 402 | {"en", "hello_emails", Options{Gender: createPtr("male"), Count: createPtr(100), Data: Data{"EmailQty": 100}}, "Hello, you have 100 emails, sir"}, 403 | {"en", "hello_emails", Options{Gender: createPtr("female"), Count: createPtr(0)}, "Hello, you have no emails, ma'am"}, 404 | {"en", "hello_emails", Options{Gender: createPtr("female"), Count: createPtr(1)}, "Hello, you have one email, ma'am"}, 405 | {"en", "hello_emails", Options{Gender: createPtr("female"), Count: createPtr(2)}, "Hello, you have two emails, ma'am"}, 406 | {"en", "hello_emails", Options{Gender: createPtr("female"), Count: createPtr(3)}, "Hello, you have three emails, ma'am"}, 407 | {"en", "hello_emails", Options{Gender: createPtr("female"), Count: createPtr(100), Data: Data{"EmailQty": 100}}, "Hello, you have 100 emails, ma'am"}, 408 | {"en", "hello_emails", Options{Gender: createPtr("nonbinary"), Count: createPtr(0)}, "Hello, you have no emails, friend"}, 409 | {"en", "hello_emails", Options{Gender: createPtr("nonbinary"), Count: createPtr(1)}, "Hello, you have one email, friend"}, 410 | {"en", "hello_emails", Options{Gender: createPtr("nonbinary"), Count: createPtr(2)}, "Hello, you have two emails, friend"}, 411 | {"en", "hello_emails", Options{Gender: createPtr("nonbinary"), Count: createPtr(3)}, "Hello, you have three emails, friend"}, 412 | {"en", "hello_emails", Options{Gender: createPtr("nonbinary"), Count: createPtr(100), Data: Data{"EmailQty": 100}}, "Hello, you have 100 emails, friend"}, 413 | {"es", "hello_emails", Options{}, "Hola, tienes correos"}, 414 | {"es", "hello_emails", Options{Gender: createPtr("male"), Count: createPtr(0)}, "Hola, no tienes correos, amigo"}, 415 | {"es", "hello_emails", Options{Gender: createPtr("male"), Count: createPtr(1)}, "Hola, tienes un correo, amigo"}, 416 | {"es", "hello_emails", Options{Gender: createPtr("male"), Count: createPtr(2)}, "Hola, tienes dos correos, amigo"}, 417 | {"es", "hello_emails", Options{Gender: createPtr("male"), Count: createPtr(3)}, "Hola, tienes tres correos, amigo"}, 418 | {"es", "hello_emails", Options{Gender: createPtr("male"), Count: createPtr(100), Data: Data{"EmailQty": 100}}, "Hola, tienes 100 correos, amigo"}, 419 | {"es", "hello_emails", Options{Gender: createPtr("female"), Count: createPtr(0)}, "Hola, no tienes correos, amiga"}, 420 | {"es", "hello_emails", Options{Gender: createPtr("female"), Count: createPtr(1)}, "Hola, tienes un correo, amiga"}, 421 | {"es", "hello_emails", Options{Gender: createPtr("female"), Count: createPtr(2)}, "Hola, tienes dos correos, amiga"}, 422 | {"es", "hello_emails", Options{Gender: createPtr("female"), Count: createPtr(3)}, "Hola, tienes tres correos, amiga"}, 423 | {"es", "hello_emails", Options{Gender: createPtr("female"), Count: createPtr(100), Data: Data{"EmailQty": 100}}, "Hola, tienes 100 correos, amiga"}, 424 | {"es", "hello_emails", Options{Gender: createPtr("nonbinary"), Count: createPtr(0)}, "Hola, no tienes correos, amigue"}, 425 | {"es", "hello_emails", Options{Gender: createPtr("nonbinary"), Count: createPtr(1)}, "Hola, tienes un correo, amigue"}, 426 | {"es", "hello_emails", Options{Gender: createPtr("nonbinary"), Count: createPtr(2)}, "Hola, tienes dos correos, amigue"}, 427 | {"es", "hello_emails", Options{Gender: createPtr("nonbinary"), Count: createPtr(3)}, "Hola, tienes tres correos, amigue"}, 428 | {"es", "hello_emails", Options{Gender: createPtr("nonbinary"), Count: createPtr(100), Data: Data{"EmailQty": 100}}, "Hola, tienes 100 correos, amigue"}, 429 | } 430 | 431 | for _, test := range tests { 432 | t.Run(test.key, func(t *testing.T) { 433 | got := i18n.Translate(test.lang, test.key, test.options) 434 | if got != test.expected { 435 | t.Errorf("expected %s; got %s", test.expected, got) 436 | } 437 | }) 438 | } 439 | }) 440 | 441 | t.Run("should return empty strings on edge incorrect cases", func(t *testing.T) { 442 | i18n := NewI18n() 443 | 444 | i18n.AddLanguage("en", TranslateStrings{ 445 | TranslateString{ 446 | Key: "hello_message", 447 | Default: "Hello", 448 | Male: "Hello sir", 449 | }, 450 | }) 451 | 452 | i18n.AddLanguage("es", TranslateStrings{ 453 | TranslateString{ 454 | Key: "hello_message", 455 | Default: "Hola", 456 | Male: "Hola amigo", 457 | }, 458 | }) 459 | 460 | tests := []struct { 461 | lang string 462 | key string 463 | options Options 464 | expected string 465 | }{ 466 | {"xxx", "xxx", Options{}, ""}, 467 | {"en", "xxx", Options{}, ""}, 468 | {"es", "xxx", Options{}, ""}, 469 | } 470 | 471 | for _, test := range tests { 472 | t.Run(test.key, func(t *testing.T) { 473 | got := i18n.Translate(test.lang, test.key, test.options) 474 | if got != test.expected { 475 | t.Errorf("expected %s; got %s", test.expected, got) 476 | } 477 | }) 478 | } 479 | }) 480 | 481 | t.Run("T should be alias for Translate", func(t *testing.T) { 482 | i18n := NewI18n() 483 | 484 | i18n.AddLanguage("en", TranslateStrings{ 485 | TranslateString{ 486 | Key: "welcome", 487 | Default: "Welcome", 488 | }, 489 | }) 490 | 491 | got := i18n.Translate("en", "welcome") 492 | gotT := i18n.T("en", "welcome") 493 | expected := "Welcome" 494 | 495 | if got != expected { 496 | t.Errorf("expected %s; got %s", expected, got) 497 | } 498 | 499 | if gotT != expected { 500 | t.Errorf("expected %s; got %s", expected, gotT) 501 | } 502 | }) 503 | 504 | t.Run("test basic templating function", func(t *testing.T) { 505 | i18n := NewI18n() 506 | 507 | i18n.AddLanguage("en", TranslateStrings{ 508 | TranslateString{ 509 | Key: "welcome", 510 | Default: "Welcome", 511 | }, 512 | }) 513 | 514 | templateFunc := i18n.NewTemplatingTranslateFunc() 515 | 516 | result := execI18nTemplate(templateFunc, `{{Translate "lang" "en" "key" "welcome"}}`) 517 | 518 | if result != "Welcome" { 519 | t.Errorf("expected %s; got %s", "Welcome", result) 520 | } 521 | }) 522 | 523 | t.Run("test templating function with multiple interpolations", func(t *testing.T) { 524 | i18n := NewI18n() 525 | 526 | i18n.AddLanguage("en", TranslateStrings{ 527 | TranslateString{ 528 | Key: "welcome", 529 | Default: "Welcome {{.Name}} {{.SurName}}", 530 | }, 531 | }) 532 | 533 | templateFunc := i18n.NewTemplatingTranslateFunc() 534 | 535 | result := execI18nTemplate(templateFunc, `{{Translate "lang" "en" "key" "welcome" "Name" "John" "SurName" "Doe" }}`) 536 | expected := "Welcome John Doe" 537 | 538 | if result != expected { 539 | t.Errorf("expected %s; got %s", expected, result) 540 | } 541 | }) 542 | 543 | t.Run("test complex templating function", func(t *testing.T) { 544 | i18n := NewI18n() 545 | 546 | i18n.AddLanguage("en", TranslateStrings{ 547 | TranslateString{ 548 | Key: "welcome_emails", 549 | Default: "Welcome, you have emails", 550 | One: "Welcome, you have one email", 551 | Many: "Welcome, you have {{.EmailQty}} emails", 552 | OneMale: "Welcome, you have one email, sir", 553 | ManyMale: "Welcome, you have {{.EmailQty}} emails, sir", 554 | OneFemale: "Welcome, you have one email, madam", 555 | ManyFemale: "Welcome, you have {{.EmailQty}} emails, madam", 556 | }, 557 | }) 558 | 559 | i18n.AddLanguage("es", TranslateStrings{ 560 | TranslateString{ 561 | Key: "welcome_emails", 562 | Default: "Bienvenido, tienes correos", 563 | One: "Bienvenido, tienes un correo", 564 | Many: "Bienvenido, tienes {{.EmailQty}} correos", 565 | OneMale: "Bienvenido, tienes un correo, amigo", 566 | ManyMale: "Bienvenido, tienes {{.EmailQty}} correos, amigo", 567 | OneFemale: "Bienvenida, tienes un correo, amiga", 568 | ManyFemale: "Bienvenida, tienes {{.EmailQty}} correos, amiga", 569 | }, 570 | }) 571 | 572 | templateFunc := i18n.NewTemplatingTranslateFunc() 573 | 574 | tests := []struct { 575 | templateText string 576 | expected string 577 | }{ 578 | {`{{Translate "lang" "en" "key" "welcome_emails"}}`, "Welcome, you have emails"}, 579 | {`{{Translate "lang" "en" "key" "welcome_emails" "count" "1"}}`, "Welcome, you have one email"}, 580 | {`{{Translate "lang" "en" "key" "welcome_emails" "count" "5" "EmailQty" "5"}}`, "Welcome, you have 5 emails"}, 581 | {`{{Translate "lang" "en" "key" "welcome_emails" "count" "1" "gender" "male"}}`, "Welcome, you have one email, sir"}, 582 | {`{{Translate "lang" "en" "key" "welcome_emails" "count" "5" "gender" "male" "EmailQty" "5"}}`, "Welcome, you have 5 emails, sir"}, 583 | {`{{Translate "lang" "en" "key" "welcome_emails" "count" "1" "gender" "female"}}`, "Welcome, you have one email, madam"}, 584 | {`{{Translate "lang" "en" "key" "welcome_emails" "count" "5" "gender" "female" "EmailQty" "5"}}`, "Welcome, you have 5 emails, madam"}, 585 | {`{{Translate "lang" "es" "key" "welcome_emails"}}`, "Bienvenido, tienes correos"}, 586 | {`{{Translate "lang" "es" "key" "welcome_emails" "count" "1"}}`, "Bienvenido, tienes un correo"}, 587 | {`{{Translate "lang" "es" "key" "welcome_emails" "count" "5" "EmailQty" "5"}}`, "Bienvenido, tienes 5 correos"}, 588 | {`{{Translate "lang" "es" "key" "welcome_emails" "count" "1" "gender" "male"}}`, "Bienvenido, tienes un correo, amigo"}, 589 | {`{{Translate "lang" "es" "key" "welcome_emails" "count" "5" "gender" "male" "EmailQty" "5"}}`, "Bienvenido, tienes 5 correos, amigo"}, 590 | {`{{Translate "lang" "es" "key" "welcome_emails" "count" "1" "gender" "female"}}`, "Bienvenida, tienes un correo, amiga"}, 591 | {`{{Translate "lang" "es" "key" "welcome_emails" "count" "5" "gender" "female" "EmailQty" "5"}}`, "Bienvenida, tienes 5 correos, amiga"}, 592 | } 593 | 594 | for i, test := range tests { 595 | t.Run(fmt.Sprintf("complex template test - %v", i), func(t *testing.T) { 596 | got := execI18nTemplate(templateFunc, test.templateText) 597 | if got != test.expected { 598 | t.Errorf("expected %s; got %s", test.expected, got) 599 | } 600 | }) 601 | } 602 | }) 603 | 604 | t.Run("test translate function creation for specific lang", func(t *testing.T) { 605 | i18n := NewI18n() 606 | 607 | i18n.AddLanguage("en", TranslateStrings{ 608 | TranslateString{ 609 | Key: "welcome", 610 | Default: "Welcome", 611 | }, 612 | }) 613 | 614 | i18n.AddLanguage("es", TranslateStrings{ 615 | TranslateString{ 616 | Key: "welcome", 617 | Default: "Bienvenido", 618 | }, 619 | }) 620 | 621 | enTranslateFunc := i18n.NewLangTranslateFunc("en") 622 | esTranslateFunc := i18n.NewLangTranslateFunc("es") 623 | 624 | tests := []struct { 625 | translateFunc func(translateKey string, options ...Options) string 626 | expected string 627 | }{ 628 | {enTranslateFunc, "Welcome"}, 629 | {esTranslateFunc, "Bienvenido"}, 630 | } 631 | 632 | for i, test := range tests { 633 | t.Run(fmt.Sprintf("translate function creation test - %v", i), func(t *testing.T) { 634 | got := test.translateFunc("welcome") 635 | if got != test.expected { 636 | t.Errorf("expected %s; got %s", test.expected, got) 637 | } 638 | }) 639 | } 640 | }) 641 | 642 | t.Run("test inconcistencies at the moment of adding a new language", func(t *testing.T) { 643 | i18n := NewI18n() 644 | 645 | errors := i18n.AddLanguage("en", TranslateStrings{ 646 | TranslateString{ 647 | Key: "welcome", 648 | Default: "Welcome", 649 | }, 650 | TranslateString{ 651 | Key: "english_only_key", 652 | Default: "English only key", 653 | }, 654 | }) 655 | 656 | if len(errors) != 0 { 657 | t.Errorf("expected no errors; got %v", errors) 658 | } 659 | 660 | errors = i18n.AddLanguage("es", TranslateStrings{ 661 | TranslateString{ 662 | Key: "welcome", 663 | Default: "Bienvenido", 664 | }, 665 | TranslateString{ 666 | Key: "spanish_only_key", 667 | Default: "Key solo de español", 668 | }, 669 | }) 670 | 671 | if len(errors) != 2 { 672 | t.Errorf("expected 2 errors; got %v", errors) 673 | } 674 | 675 | firstError := errors[0] 676 | secondError := errors[1] 677 | 678 | if strings.Contains(firstError, "spanish_only_key") == false { 679 | t.Errorf("expected error to contain spanish_only_key; got %v", firstError) 680 | } 681 | 682 | if strings.Contains(secondError, "english_only_key") == false { 683 | t.Errorf("expected error to contain english_only_key; got %v", secondError) 684 | } 685 | }) 686 | 687 | t.Run("test inconcistencies can be disabled", func(t *testing.T) { 688 | i18n := NewI18n(Config{ 689 | DisableConsistencyCheck: true, 690 | }) 691 | 692 | errors := i18n.AddLanguage("en", TranslateStrings{ 693 | TranslateString{ 694 | Key: "welcome", 695 | Default: "Welcome", 696 | }, 697 | TranslateString{ 698 | Key: "english_only_key", 699 | Default: "English only key", 700 | }, 701 | }) 702 | 703 | if len(errors) != 0 { 704 | t.Errorf("expected no errors; got %v", errors) 705 | } 706 | 707 | errors = i18n.AddLanguage("es", TranslateStrings{ 708 | TranslateString{ 709 | Key: "welcome", 710 | Default: "Bienvenido", 711 | }, 712 | TranslateString{ 713 | Key: "spanish_only_key", 714 | Default: "Key solo de español", 715 | }, 716 | }) 717 | 718 | if len(errors) != 0 { 719 | t.Errorf("expected 0 errors; got %v", errors) 720 | } 721 | }) 722 | } 723 | 724 | // Function to create pointer to a value 725 | func createPtr[T string | int](s T) *T { 726 | return &s 727 | } 728 | 729 | // Function to create template and execute it 730 | func execI18nTemplate(fn func(...any) string, templateText string) string { 731 | tmpl, err := template.New("main").Funcs( 732 | template.FuncMap{ 733 | "Translate": fn, 734 | }, 735 | ).Parse(templateText) 736 | 737 | if err != nil { 738 | return fmt.Sprintf("Error parsing template: %v", err) 739 | } 740 | 741 | result := new(bytes.Buffer) 742 | err = tmpl.Execute(result, nil) 743 | if err != nil { 744 | return fmt.Sprintf("Error executing template: %v", err) 745 | } 746 | 747 | return result.String() 748 | } 749 | --------------------------------------------------------------------------------