├── .github ├── dependabot.yml └── workflows │ ├── bearer.yml │ ├── codeql.yml │ ├── go.yml │ └── goreleaser.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yaml ├── LICENSE ├── README.md ├── _example ├── go.mod ├── go.sum ├── i18n │ ├── localize │ │ ├── de.yaml │ │ ├── en.yaml │ │ └── fr.yaml │ └── localizeJSON │ │ ├── de.json │ │ ├── en.json │ │ └── zh.json └── main.go ├── constant.go ├── embed.go ├── embed_test.go ├── ginContext.go ├── ginI18n.go ├── go.mod ├── go.sum ├── i18n.go ├── i18n_test.go ├── interface.go ├── option.go ├── testdata ├── localize │ ├── de.yaml │ ├── en.yaml │ └── fr.yaml └── localizeJSON │ ├── de.json │ ├── en.json │ └── zh.json └── type.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | - package-ecosystem: gomod 8 | directory: / 9 | schedule: 10 | interval: weekly 11 | -------------------------------------------------------------------------------- /.github/workflows/bearer.yml: -------------------------------------------------------------------------------- 1 | name: Bearer PR Check 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened] 6 | 7 | permissions: 8 | contents: read 9 | pull-requests: write 10 | 11 | jobs: 12 | rule_check: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | 18 | - uses: reviewdog/action-setup@v1 19 | with: 20 | reviewdog_version: latest 21 | 22 | - name: Run Report 23 | id: report 24 | uses: bearer/bearer-action@v2 25 | with: 26 | format: rdjson 27 | output: rd.json 28 | diff: true 29 | 30 | - name: Run reviewdog 31 | if: always() 32 | env: 33 | REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | run: | 35 | cat rd.json | reviewdog -f=rdjson -reporter=github-pr-review 36 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [master] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [master] 20 | schedule: 21 | - cron: "41 23 * * 6" 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: ["go"] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | - name: Perform CodeQL Analysis 54 | uses: github/codeql-action/analyze@v3 55 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | 18 | - name: Setup go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version: "^1" 22 | 23 | - name: Setup golangci-lint 24 | uses: golangci/golangci-lint-action@v7 25 | with: 26 | version: v2.0 27 | args: --verbose 28 | 29 | - name: Bearer 30 | uses: bearer/bearer-action@v2 31 | with: 32 | diff: true 33 | 34 | test: 35 | strategy: 36 | matrix: 37 | os: [ubuntu-latest] 38 | go: [1.23, 1.24] 39 | include: 40 | - os: ubuntu-latest 41 | go-build: ~/.cache/go-build 42 | name: ${{ matrix.os }} @ Go ${{ matrix.go }} 43 | runs-on: ${{ matrix.os }} 44 | env: 45 | GO111MODULE: on 46 | GOPROXY: https://proxy.golang.org 47 | steps: 48 | - name: Set up Go ${{ matrix.go }} 49 | uses: actions/setup-go@v5 50 | with: 51 | go-version: ${{ matrix.go }} 52 | 53 | - name: Checkout Code 54 | uses: actions/checkout@v4 55 | with: 56 | ref: ${{ github.ref }} 57 | 58 | - uses: actions/cache@v4 59 | with: 60 | path: | 61 | ${{ matrix.go-build }} 62 | ~/go/pkg/mod 63 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 64 | restore-keys: | 65 | ${{ runner.os }}-go- 66 | - name: Run Tests 67 | run: | 68 | go test -v -covermode=atomic -coverprofile=coverage.out 69 | 70 | - name: Upload coverage to Codecov 71 | uses: codecov/codecov-action@v5 72 | with: 73 | flags: ${{ matrix.os }},go-${{ matrix.go }} 74 | -------------------------------------------------------------------------------- /.github/workflows/goreleaser.yml: -------------------------------------------------------------------------------- 1 | name: Goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Setup go 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version-file: go.mod 24 | check-latest: true 25 | 26 | - name: Run GoReleaser 27 | uses: goreleaser/goreleaser-action@v6 28 | with: 29 | # either 'goreleaser' (default) or 'goreleaser-pro' 30 | distribution: goreleaser 31 | version: latest 32 | args: release --clean 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # goland 15 | .idea 16 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | default: none 4 | enable: 5 | - bodyclose 6 | - dogsled 7 | - dupl 8 | - errcheck 9 | - exhaustive 10 | - gochecknoinits 11 | - goconst 12 | - gocritic 13 | - gocyclo 14 | - goprintffuncname 15 | - gosec 16 | - govet 17 | - ineffassign 18 | - lll 19 | - misspell 20 | - nakedret 21 | - noctx 22 | - nolintlint 23 | - rowserrcheck 24 | - staticcheck 25 | - unconvert 26 | - unparam 27 | - unused 28 | - whitespace 29 | exclusions: 30 | generated: lax 31 | presets: 32 | - comments 33 | - common-false-positives 34 | - legacy 35 | - std-error-handling 36 | paths: 37 | - third_party$ 38 | - builtin$ 39 | - examples$ 40 | formatters: 41 | enable: 42 | - gofmt 43 | - gofumpt 44 | - goimports 45 | exclusions: 46 | generated: lax 47 | paths: 48 | - third_party$ 49 | - builtin$ 50 | - examples$ 51 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | builds: 2 | - # If true, skip the build. 3 | # Useful for library projects. 4 | # Default is false 5 | skip: true 6 | 7 | changelog: 8 | use: github 9 | groups: 10 | - title: Features 11 | regexp: "^.*feat[(\\w)]*:+.*$" 12 | order: 0 13 | - title: "Bug fixes" 14 | regexp: "^.*fix[(\\w)]*:+.*$" 15 | order: 1 16 | - title: "Enhancements" 17 | regexp: "^.*chore[(\\w)]*:+.*$" 18 | order: 2 19 | - title: "Refactor" 20 | regexp: "^.*refactor[(\\w)]*:+.*$" 21 | order: 3 22 | - title: "Build process updates" 23 | regexp: ^.*?(build|ci)(\(.+\))??!?:.+$ 24 | order: 4 25 | - title: "Documentation updates" 26 | regexp: ^.*?docs?(\(.+\))??!?:.+$ 27 | order: 4 28 | - title: Others 29 | order: 999 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Gin-Gonic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # i18n 2 | 3 | [![Run Tests](https://github.com/gin-contrib/i18n/actions/workflows/go.yml/badge.svg)](https://github.com/gin-contrib/i18n/actions/workflows/go.yml) 4 | [![CodeQL](https://github.com/gin-contrib/i18n/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/gin-contrib/i18n/actions/workflows/codeql-analysis.yml) 5 | [![codecov](https://codecov.io/gh/gin-contrib/i18n/branch/master/graph/badge.svg?token=QNMN3KM28Y)](https://codecov.io/gh/gin-contrib/i18n) 6 | [![GoDoc](https://godoc.org/github.com/gin-contrib/i18n?status.svg)](https://godoc.org/github.com/gin-contrib/i18n) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/gin-contrib/i18n)](https://goreportcard.com/report/github.com/gin-contrib/i18n) 8 | 9 | ## Usage 10 | 11 | Download and install it: 12 | 13 | ```sh 14 | go get github.com/gin-contrib/i18n 15 | ``` 16 | 17 | Import it in your code: 18 | 19 | ```go 20 | import ginI18n "github.com/gin-contrib/i18n" 21 | ``` 22 | 23 | Canonical example: 24 | 25 | ```go 26 | package main 27 | 28 | import ( 29 | "log" 30 | "net/http" 31 | 32 | ginI18n "github.com/gin-contrib/i18n" 33 | "github.com/gin-gonic/gin" 34 | "github.com/nicksnyder/go-i18n/v2/i18n" 35 | ) 36 | 37 | func main() { 38 | // new gin engine 39 | gin.SetMode(gin.ReleaseMode) 40 | router := gin.New() 41 | 42 | // apply i18n middleware 43 | router.Use(ginI18n.Localize()) 44 | 45 | router.GET("/", func(ctx *gin.Context) { 46 | ctx.String(http.StatusOK, ginI18n.MustGetMessage(ctx, "welcome")) 47 | }) 48 | 49 | router.GET("/messageId/:name", func(context *gin.Context) { 50 | context.String(http.StatusOK, MustGetMessage(context, &i18n.LocalizeConfig{ 51 | MessageID: "welcomeWithName", 52 | TemplateData: map[string]string{ 53 | "name": context.Param("name"), 54 | }, 55 | })) 56 | }) 57 | 58 | router.GET("/messageType/:name", func(context *gin.Context) { 59 | context.String(http.StatusOK, MustGetMessage(context, &i18n.LocalizeConfig{ 60 | DefaultMessage: &i18n.Message{ 61 | ID: "welcomeWithName", 62 | }, 63 | TemplateData: map[string]string{ 64 | "name": context.Param("name"), 65 | }, 66 | })) 67 | }) 68 | 69 | router.GET("/exist/:lang", func(ctx *gin.Context) { 70 | ctx.String(http.StatusOK, "%v", ginI18n.HasLang(ctx, ctx.Param("lang"))) 71 | }) 72 | 73 | // get the default and current language 74 | router.GET("/lang/default", func(context *gin.Context) { 75 | context.String(http.StatusOK, "%s", GetDefaultLanguage(context).String()) 76 | }) 77 | 78 | // get the current language 79 | router.GET("/lang/current", func(context *gin.Context) { 80 | context.String(http.StatusOK, "%s", GetCurrentLanguage(context).String()) 81 | }) 82 | 83 | if err := router.Run(":8080"); err != nil { 84 | log.Fatal(err) 85 | } 86 | } 87 | ``` 88 | 89 | Customized Bundle 90 | 91 | ```go 92 | package main 93 | 94 | import ( 95 | "encoding/json" 96 | "log" 97 | "net/http" 98 | 99 | ginI18n "github.com/gin-contrib/i18n" 100 | "github.com/gin-gonic/gin" 101 | "github.com/nicksnyder/go-i18n/v2/i18n" 102 | "golang.org/x/text/language" 103 | ) 104 | 105 | func main() { 106 | // new gin engine 107 | gin.SetMode(gin.ReleaseMode) 108 | router := gin.New() 109 | 110 | // apply i18n middleware 111 | router.Use(ginI18n.Localize(ginI18n.WithBundle(&ginI18n.BundleCfg{ 112 | RootPath: "./testdata/localizeJSON", 113 | AcceptLanguage: []language.Tag{language.German, language.English}, 114 | DefaultLanguage: language.English, 115 | UnmarshalFunc: json.Unmarshal, 116 | FormatBundleFile: "json", 117 | }))) 118 | 119 | router.GET("/", func(ctx *gin.Context) { 120 | ctx.String(http.StatusOK, ginI18n.MustGetMessage(ctx, "welcome")) 121 | }) 122 | 123 | router.GET("/messageId/:name", func(context *gin.Context) { 124 | context.String(http.StatusOK, MustGetMessage(context, &i18n.LocalizeConfig{ 125 | MessageID: "welcomeWithName", 126 | TemplateData: map[string]string{ 127 | "name": context.Param("name"), 128 | }, 129 | })) 130 | }) 131 | 132 | router.GET("/messageType/:name", func(context *gin.Context) { 133 | context.String(http.StatusOK, MustGetMessage(context, &i18n.LocalizeConfig{ 134 | DefaultMessage: &i18n.Message{ 135 | ID: "welcomeWithName", 136 | }, 137 | TemplateData: map[string]string{ 138 | "name": context.Param("name"), 139 | }, 140 | })) 141 | }) 142 | 143 | router.GET("/exist/:lang", func(ctx *gin.Context) { 144 | ctx.String(http.StatusOK, "%v", ginI18n.HasLang(ctx, ctx.Param("lang"))) 145 | }) 146 | 147 | // get the default and current language 148 | router.GET("/lang/default", func(context *gin.Context) { 149 | context.String(http.StatusOK, "%s", GetDefaultLanguage(context).String()) 150 | }) 151 | 152 | // get the current language 153 | router.GET("/lang/current", func(context *gin.Context) { 154 | context.String(http.StatusOK, "%s", GetCurrentLanguage(context).String()) 155 | }) 156 | 157 | if err := router.Run(":8080"); err != nil { 158 | log.Fatal(err) 159 | } 160 | } 161 | ``` 162 | 163 | Customized Get Language Handler 164 | 165 | ```go 166 | package main 167 | 168 | import ( 169 | "log" 170 | "net/http" 171 | 172 | ginI18n "github.com/gin-contrib/i18n" 173 | "github.com/gin-gonic/gin" 174 | "github.com/nicksnyder/go-i18n/v2/i18n" 175 | ) 176 | 177 | func main() { 178 | // new gin engine 179 | gin.SetMode(gin.ReleaseMode) 180 | router := gin.New() 181 | 182 | // apply i18n middleware 183 | router.Use(ginI18n.Localize( 184 | ginI18n.WithGetLngHandle( 185 | func(context *gin.Context, defaultLng string) string { 186 | lng := context.Query("lng") 187 | if lng == "" { 188 | return defaultLng 189 | } 190 | return lng 191 | }, 192 | ), 193 | )) 194 | 195 | router.GET("/", func(ctx *gin.Context) { 196 | ctx.String(http.StatusOK, ginI18n.MustGetMessage(ctx, "welcome")) 197 | }) 198 | 199 | router.GET("/messageId/:name", func(context *gin.Context) { 200 | context.String(http.StatusOK, MustGetMessage(context, &i18n.LocalizeConfig{ 201 | MessageID: "welcomeWithName", 202 | TemplateData: map[string]string{ 203 | "name": context.Param("name"), 204 | }, 205 | })) 206 | }) 207 | 208 | router.GET("/messageType/:name", func(context *gin.Context) { 209 | context.String(http.StatusOK, MustGetMessage(context, &i18n.LocalizeConfig{ 210 | DefaultMessage: &i18n.Message{ 211 | ID: "welcomeWithName", 212 | }, 213 | TemplateData: map[string]string{ 214 | "name": context.Param("name"), 215 | }, 216 | })) 217 | }) 218 | 219 | router.GET("/exist/:lang", func(ctx *gin.Context) { 220 | ctx.String(http.StatusOK, "%v", ginI18n.HasLang(ctx, ctx.Param("lang"))) 221 | }) 222 | 223 | // get the default and current language 224 | router.GET("/lang/default", func(context *gin.Context) { 225 | context.String(http.StatusOK, "%s", GetDefaultLanguage(context).String()) 226 | }) 227 | 228 | // get the current language 229 | router.GET("/lang/current", func(context *gin.Context) { 230 | context.String(http.StatusOK, "%s", GetCurrentLanguage(context).String()) 231 | }) 232 | 233 | if err := router.Run(":8080"); err != nil { 234 | log.Fatal(err) 235 | } 236 | } 237 | ``` 238 | 239 | ## License 240 | 241 | This project is under MIT License. See the [LICENSE](LICENSE) file for the full license text. 242 | -------------------------------------------------------------------------------- /_example/go.mod: -------------------------------------------------------------------------------- 1 | module example 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/gin-contrib/i18n v1.0.0 9 | github.com/gin-gonic/gin v1.10.0 10 | github.com/nicksnyder/go-i18n/v2 v2.5.1 11 | golang.org/x/text v0.23.0 12 | ) 13 | 14 | require ( 15 | github.com/bytedance/sonic v1.13.2 // indirect 16 | github.com/bytedance/sonic/loader v0.2.4 // indirect 17 | github.com/cloudwego/base64x v0.1.5 // indirect 18 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 19 | github.com/gin-contrib/sse v1.0.0 // indirect 20 | github.com/go-playground/locales v0.14.1 // indirect 21 | github.com/go-playground/universal-translator v0.18.1 // indirect 22 | github.com/go-playground/validator/v10 v10.26.0 // indirect 23 | github.com/goccy/go-json v0.10.5 // indirect 24 | github.com/json-iterator/go v1.1.12 // indirect 25 | github.com/klauspost/cpuid/v2 v2.2.10 // indirect 26 | github.com/leodido/go-urn v1.4.0 // indirect 27 | github.com/mattn/go-isatty v0.0.20 // indirect 28 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 29 | github.com/modern-go/reflect2 v1.0.2 // indirect 30 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 31 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 32 | github.com/ugorji/go/codec v1.2.12 // indirect 33 | golang.org/x/arch v0.15.0 // indirect 34 | golang.org/x/crypto v0.36.0 // indirect 35 | golang.org/x/net v0.38.0 // indirect 36 | golang.org/x/sys v0.31.0 // indirect 37 | google.golang.org/protobuf v1.36.6 // indirect 38 | gopkg.in/yaml.v3 v3.0.1 // indirect 39 | ) 40 | 41 | replace github.com/gin-contrib/i18n => ../ 42 | -------------------------------------------------------------------------------- /_example/go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= 2 | github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 3 | github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= 4 | github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= 5 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 6 | github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= 7 | github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= 8 | github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= 9 | github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 10 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= 15 | github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= 16 | github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= 17 | github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= 18 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= 19 | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 20 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 21 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 22 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 23 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 24 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 25 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 26 | github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= 27 | github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= 28 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 29 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 30 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 31 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 32 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 33 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 34 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 35 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 36 | github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= 37 | github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 38 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 39 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 40 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 41 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 42 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 43 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 44 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 45 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 46 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 47 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 48 | github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk= 49 | github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ= 50 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 51 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 52 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 53 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 54 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 55 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 56 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 57 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 58 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 59 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 60 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 61 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 62 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 63 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 64 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 65 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 66 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 67 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 68 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 69 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 70 | golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw= 71 | golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= 72 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 73 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 74 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 75 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 76 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 77 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 78 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 79 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 80 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 81 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 82 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 83 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 84 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 85 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 86 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 87 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 88 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 89 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 90 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 91 | -------------------------------------------------------------------------------- /_example/i18n/localize/de.yaml: -------------------------------------------------------------------------------- 1 | welcome: hallo 2 | welcomeWithName: hallo {{ .name }} 3 | -------------------------------------------------------------------------------- /_example/i18n/localize/en.yaml: -------------------------------------------------------------------------------- 1 | welcome: hello 2 | welcomeWithName: hello {{ .name }} 3 | -------------------------------------------------------------------------------- /_example/i18n/localize/fr.yaml: -------------------------------------------------------------------------------- 1 | welcome: bonjour 2 | welcomeWithName: bonjour {{ .name }} -------------------------------------------------------------------------------- /_example/i18n/localizeJSON/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome": "hallo", 3 | "welcomeWithName": "hallo {{ .name }}" 4 | } 5 | -------------------------------------------------------------------------------- /_example/i18n/localizeJSON/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome": "hello", 3 | "welcomeWithName": "hello {{ .name }}" 4 | } 5 | -------------------------------------------------------------------------------- /_example/i18n/localizeJSON/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome": "你好", 3 | "welcomeWithName": "你好 {{ .name }}" 4 | } 5 | -------------------------------------------------------------------------------- /_example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | "encoding/json" 6 | "log" 7 | "net/http" 8 | 9 | ginI18n "github.com/gin-contrib/i18n" 10 | "github.com/gin-gonic/gin" 11 | "github.com/nicksnyder/go-i18n/v2/i18n" 12 | "golang.org/x/text/language" 13 | ) 14 | 15 | //go:embed i18n/localizeJSON/* 16 | var fs embed.FS 17 | 18 | func main() { 19 | // new gin engine 20 | gin.SetMode(gin.ReleaseMode) 21 | router := gin.New() 22 | 23 | // apply i18n middleware 24 | router.Use(ginI18n.Localize(ginI18n.WithBundle(&ginI18n.BundleCfg{ 25 | DefaultLanguage: language.English, 26 | FormatBundleFile: "json", 27 | AcceptLanguage: []language.Tag{language.English, language.German, language.Chinese}, 28 | RootPath: "./i18n/localizeJSON/", 29 | UnmarshalFunc: json.Unmarshal, 30 | // After commenting this line, use defaultLoader 31 | // it will be loaded from the file 32 | Loader: &ginI18n.EmbedLoader{ 33 | FS: fs, 34 | }, 35 | }))) 36 | 37 | router.GET("/", func(ctx *gin.Context) { 38 | ctx.String(http.StatusOK, ginI18n.MustGetMessage(ctx, "welcome")) 39 | }) 40 | 41 | router.GET("/:name", func(ctx *gin.Context) { 42 | ctx.String(http.StatusOK, ginI18n.MustGetMessage( 43 | ctx, 44 | &i18n.LocalizeConfig{ 45 | MessageID: "welcomeWithName", 46 | TemplateData: map[string]string{ 47 | "name": ctx.Param("name"), 48 | }, 49 | })) 50 | }) 51 | 52 | if err := router.Run(":8080"); err != nil { 53 | log.Fatal(err) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /constant.go: -------------------------------------------------------------------------------- 1 | package i18n 2 | 3 | import ( 4 | "os" 5 | 6 | "golang.org/x/text/language" 7 | "gopkg.in/yaml.v3" 8 | ) 9 | 10 | const ( 11 | defaultFormatBundleFile = "yaml" 12 | defaultRootPath = "./testdata/localize" 13 | ) 14 | 15 | var ( 16 | defaultLanguage = language.English 17 | defaultUnmarshalFunc = yaml.Unmarshal 18 | defaultAcceptLanguage = []language.Tag{ 19 | defaultLanguage, 20 | language.German, 21 | language.French, 22 | } 23 | 24 | defaultLoader = LoaderFunc(os.ReadFile) 25 | 26 | defaultBundleConfig = &BundleCfg{ 27 | RootPath: defaultRootPath, 28 | AcceptLanguage: defaultAcceptLanguage, 29 | FormatBundleFile: defaultFormatBundleFile, 30 | DefaultLanguage: defaultLanguage, 31 | UnmarshalFunc: defaultUnmarshalFunc, 32 | Loader: defaultLoader, 33 | } 34 | ) 35 | -------------------------------------------------------------------------------- /embed.go: -------------------------------------------------------------------------------- 1 | package i18n 2 | 3 | import ( 4 | "embed" 5 | ) 6 | 7 | type EmbedLoader struct { 8 | FS embed.FS 9 | } 10 | 11 | func (c *EmbedLoader) LoadMessage(path string) ([]byte, error) { 12 | return c.FS.ReadFile(path) 13 | } 14 | -------------------------------------------------------------------------------- /embed_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.16 2 | // +build go1.16 3 | 4 | package i18n 5 | 6 | import ( 7 | "context" 8 | "embed" 9 | "encoding/json" 10 | "net/http" 11 | "net/http/httptest" 12 | "testing" 13 | 14 | "github.com/gin-gonic/gin" 15 | "github.com/nicksnyder/go-i18n/v2/i18n" 16 | "golang.org/x/text/language" 17 | ) 18 | 19 | type server struct { 20 | *gin.Engine 21 | } 22 | 23 | func newEmbedServer(middleware ...gin.HandlerFunc) *server { 24 | server := &server{gin.New()} 25 | server.Use(middleware...) 26 | 27 | server.GET("/", func(context *gin.Context) { 28 | context.String(http.StatusOK, MustGetMessage(context, "welcome")) 29 | }) 30 | 31 | server.GET("/:name", func(context *gin.Context) { 32 | context.String(http.StatusOK, MustGetMessage(context, &i18n.LocalizeConfig{ 33 | MessageID: "welcomeWithName", 34 | TemplateData: map[string]string{ 35 | "name": context.Param("name"), 36 | }, 37 | })) 38 | }) 39 | 40 | return server 41 | } 42 | 43 | func (s *server) request(lng language.Tag, name string) string { 44 | path := "/" + name 45 | ctx := context.Background() 46 | req, _ := http.NewRequestWithContext(ctx, "GET", path, nil) 47 | req.Header.Add("Accept-Language", lng.String()) 48 | 49 | w := httptest.NewRecorder() 50 | s.ServeHTTP(w, req) 51 | 52 | return w.Body.String() 53 | } 54 | 55 | var ( 56 | //go:embed testdata/localizeJSON/* 57 | fs embed.FS 58 | 59 | s = newEmbedServer(Localize(WithBundle(&BundleCfg{ 60 | DefaultLanguage: language.English, 61 | FormatBundleFile: "json", 62 | AcceptLanguage: []language.Tag{language.English, language.German, language.Chinese}, 63 | RootPath: "./testdata/localizeJSON/", 64 | UnmarshalFunc: json.Unmarshal, 65 | // After commenting this line, use defaultLoader 66 | // it will be loaded from the file 67 | Loader: &EmbedLoader{fs}, 68 | }))) 69 | ) 70 | 71 | func TestEmbedLoader(t *testing.T) { 72 | type args struct { 73 | lng language.Tag 74 | name string 75 | } 76 | tests := []struct { 77 | name string 78 | args args 79 | want string 80 | }{ 81 | { 82 | name: "hello world", 83 | args: args{ 84 | name: "", 85 | lng: language.English, 86 | }, 87 | want: "hello", 88 | }, 89 | { 90 | name: "hello alex", 91 | args: args{ 92 | name: "", 93 | lng: language.Chinese, 94 | }, 95 | want: "你好", 96 | }, 97 | { 98 | name: "hello alex", 99 | args: args{ 100 | name: "alex", 101 | lng: language.English, 102 | }, 103 | want: "hello alex", 104 | }, 105 | { 106 | name: "hello alex german", 107 | args: args{ 108 | name: "alex", 109 | lng: language.Chinese, 110 | }, 111 | want: "你好 alex", 112 | }, 113 | } 114 | for _, tt := range tests { 115 | t.Run(tt.name, func(t *testing.T) { 116 | got := s.request(tt.args.lng, tt.args.name) 117 | if got != tt.want { 118 | t.Errorf("makeRequest() = %v, want %v", got, tt.want) 119 | } 120 | }) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ginContext.go: -------------------------------------------------------------------------------- 1 | package i18n 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | // defaultGetLngHandler ... 6 | func defaultGetLngHandler(context *gin.Context, defaultLng string) string { 7 | if context == nil || context.Request == nil { 8 | return defaultLng 9 | } 10 | 11 | lng := context.GetHeader("Accept-Language") 12 | if lng != "" { 13 | return lng 14 | } 15 | 16 | lng = context.Query("lng") 17 | if lng == "" { 18 | return defaultLng 19 | } 20 | 21 | return lng 22 | } 23 | -------------------------------------------------------------------------------- /ginI18n.go: -------------------------------------------------------------------------------- 1 | // Package i18n ginI18nImpl is an implementation of the GinI18n interface, providing 2 | // localization support for Gin applications. It uses the go-i18n library 3 | // to manage and retrieve localized messages. 4 | // 5 | // Fields: 6 | // - bundle: The i18n.Bundle containing the localization messages. 7 | // - localizerByLng: A map of language tags to their corresponding localizers. 8 | // - defaultLanguage: The default language tag to use for localization. 9 | // - getLngHandler: A handler function to retrieve the language tag from the Gin context. 10 | // 11 | // Methods: 12 | // - GetMessage: Retrieves a localized message based on the provided context and parameter. 13 | // - MustGetMessage: Retrieves a localized message and returns an empty string if retrieval fails. 14 | // - HasLang: Checks if a specific language is supported. 15 | // - GetCurrentLanguage: Retrieves the current language based on the Gin context.. 16 | // - GetDefaultLanguage: Retrieves the default language 17 | // - SetBundle: Sets the i18n.Bundle configuration. 18 | // - SetGetLngHandler: Sets the handler function to retrieve the language tag from the Gin context. 19 | // - loadMessageFiles: Loads all localization files into the bundle. 20 | // - loadMessageFile: Loads a single localization file into the bundle. 21 | // - setLocalizerByLng: Sets the localizers for each accepted language. 22 | // - newLocalizer: Creates a new localizer for a given language. 23 | // - getLocalizerByLng: Retrieves the localizer for a given language. 24 | // - getLocalizeConfig: Converts the parameter into an i18n.LocalizeConfig. 25 | package i18n 26 | 27 | import ( 28 | "errors" 29 | "fmt" 30 | "path" 31 | 32 | "github.com/gin-gonic/gin" 33 | "github.com/nicksnyder/go-i18n/v2/i18n" 34 | "golang.org/x/text/language" 35 | ) 36 | 37 | // GinI18n is an interface that defines methods for internationalization (i18n) in a Gin web framework context. 38 | // It provides methods to get localized messages and configure the i18n bundle and language handler. 39 | var _ GinI18n = (*ginI18nImpl)(nil) 40 | 41 | type ginI18nImpl struct { 42 | bundle *i18n.Bundle 43 | localizerByLng map[string]*i18n.Localizer 44 | defaultLanguage language.Tag 45 | getLngHandler GetLngHandler 46 | } 47 | 48 | // GetDefaultLanguage retrieves the default language tag for the application. 49 | // 50 | // This method returns the default language tag that is used when no specific 51 | // language is specified by the client or in the context. 52 | // 53 | // Parameters: 54 | // - ctx: The Gin context from which to retrieve the message. 55 | // 56 | // Returns: 57 | // - language.Tag: The default language tag. 58 | func (i *ginI18nImpl) GetDefaultLanguage() language.Tag { 59 | return i.defaultLanguage 60 | } 61 | 62 | // GetCurrentLanguage retrieves the current language tag from the Gin context. 63 | // 64 | // This method extracts the language tag from the Gin context using the provided 65 | // `getLngHandler` function. It uses this handler to obtain the language tag for 66 | // the current request. If the language is not provided, it returns the default language. 67 | // 68 | // Parameters: 69 | // - ctx: The Gin context from which to retrieve the message. 70 | // 71 | // Returns: 72 | // - language.Tag: The language tag based on the context (either the specified language or the default language). 73 | func (i *ginI18nImpl) GetCurrentLanguage(context *gin.Context) language.Tag { 74 | return language.Make(i.getLngHandler(context, i.defaultLanguage.String())) 75 | } 76 | 77 | // HasLang checks whether the specified language is supported by the application. 78 | // 79 | // This method checks if a language tag is available in the localizer map (`localizerByLng`), 80 | // which stores localizers for all supported languages. If the language is supported, 81 | // it returns `true`; otherwise, it returns `false`. 82 | // 83 | // Parameters: 84 | // - ctx: The Gin context from which to retrieve the message. 85 | // - language (string): The language tag (e.g., "en", "zh") to check. 86 | // 87 | // Returns: 88 | // - bool: `true` if the language is supported, otherwise `false`. 89 | func (i *ginI18nImpl) HasLang(language string) bool { 90 | if _, exist := i.localizerByLng[language]; exist { 91 | return true 92 | } 93 | 94 | return false 95 | } 96 | 97 | // GetMessage retrieves a localized message based on the provided context and parameter. 98 | // If the message cannot be retrieved, it returns an empty string. 99 | // 100 | // Parameters: 101 | // - ctx: The Gin context from which to retrieve the message. 102 | // - param: The parameter used to fetch the localized message. 103 | // 104 | // Returns: 105 | // - string: The localized message or an empty string if retrieval fails. 106 | // - error: An error if the message retrieval fails. 107 | func (i *ginI18nImpl) GetMessage(ctx *gin.Context, param interface{}) (string, error) { 108 | lng := i.getLngHandler(ctx, i.defaultLanguage.String()) 109 | localizer := i.getLocalizerByLng(lng) 110 | 111 | localizeConfig, err := i.getLocalizeConfig(param) 112 | if err != nil { 113 | return "", err 114 | } 115 | 116 | message, err := localizer.Localize(localizeConfig) 117 | if err != nil { 118 | return "", err 119 | } 120 | 121 | return message, nil 122 | } 123 | 124 | // MustGetMessage retrieves a localized message based on the provided context and parameter. 125 | // If the message cannot be retrieved, it returns an empty string. 126 | // This method panics if the message retrieval fails. 127 | // 128 | // Parameters: 129 | // - ctx: The Gin context from which to retrieve the message. 130 | // - param: The parameter used to fetch the localized message. 131 | // 132 | // Returns: 133 | // - string: The localized message or an empty string if retrieval fails. 134 | func (i *ginI18nImpl) MustGetMessage(ctx *gin.Context, param interface{}) string { 135 | message, _ := i.GetMessage(ctx, param) 136 | return message 137 | } 138 | 139 | // SetBundle initializes the i18n bundle with the provided configuration. 140 | // It sets the default language, registers the unmarshal function for the bundle files, 141 | // loads the message files, and sets the localizer based on the accepted languages. 142 | // 143 | // Parameters: 144 | // - cfg: A pointer to a BundleCfg struct that contains the configuration for the bundle. 145 | func (i *ginI18nImpl) SetBundle(cfg *BundleCfg) { 146 | bundle := i18n.NewBundle(cfg.DefaultLanguage) 147 | bundle.RegisterUnmarshalFunc(cfg.FormatBundleFile, cfg.UnmarshalFunc) 148 | 149 | i.bundle = bundle 150 | i.defaultLanguage = cfg.DefaultLanguage 151 | 152 | i.loadMessageFiles(cfg) 153 | i.setLocalizerByLng(cfg.AcceptLanguage) 154 | } 155 | 156 | // SetGetLngHandler sets the handler function that will be used to get the language. 157 | // The handler should be a function that implements the GetLngHandler interface. 158 | // 159 | // Parameters: 160 | // 161 | // handler - a function that implements the GetLngHandler interface 162 | func (i *ginI18nImpl) SetGetLngHandler(handler GetLngHandler) { 163 | i.getLngHandler = handler 164 | } 165 | 166 | // loadMessageFiles load all file localize to bundle 167 | func (i *ginI18nImpl) loadMessageFiles(config *BundleCfg) { 168 | for _, lng := range config.AcceptLanguage { 169 | src := path.Join(config.RootPath, lng.String()) + "." + config.FormatBundleFile 170 | if err := i.loadMessageFile(config, src); err != nil { 171 | panic(err) 172 | } 173 | } 174 | } 175 | 176 | func (i *ginI18nImpl) loadMessageFile(config *BundleCfg, src string) error { 177 | buf, err := config.Loader.LoadMessage(src) 178 | if err != nil { 179 | return err 180 | } 181 | 182 | if _, err = i.bundle.ParseMessageFileBytes(buf, src); err != nil { 183 | return err 184 | } 185 | return nil 186 | } 187 | 188 | // setLocalizerByLng set localizer by language 189 | func (i *ginI18nImpl) setLocalizerByLng(acceptLanguage []language.Tag) { 190 | i.localizerByLng = map[string]*i18n.Localizer{} 191 | for _, lng := range acceptLanguage { 192 | lngStr := lng.String() 193 | i.localizerByLng[lngStr] = i.newLocalizer(lngStr) 194 | } 195 | 196 | // set defaultLanguage if it isn't exist 197 | defaultLng := i.defaultLanguage.String() 198 | if _, hasDefaultLng := i.localizerByLng[defaultLng]; !hasDefaultLng { 199 | i.localizerByLng[defaultLng] = i.newLocalizer(defaultLng) 200 | } 201 | } 202 | 203 | // newLocalizer create a localizer by language 204 | func (i *ginI18nImpl) newLocalizer(lng string) *i18n.Localizer { 205 | lngDefault := i.defaultLanguage.String() 206 | lngs := []string{ 207 | lng, 208 | } 209 | 210 | if lng != lngDefault { 211 | lngs = append(lngs, lngDefault) 212 | } 213 | 214 | localizer := i18n.NewLocalizer( 215 | i.bundle, 216 | lngs..., 217 | ) 218 | return localizer 219 | } 220 | 221 | // getLocalizerByLng get localizer by language 222 | func (i *ginI18nImpl) getLocalizerByLng(lng string) *i18n.Localizer { 223 | localizer, hasValue := i.localizerByLng[lng] 224 | if hasValue { 225 | return localizer 226 | } 227 | 228 | return i.localizerByLng[i.defaultLanguage.String()] 229 | } 230 | 231 | func (i *ginI18nImpl) getLocalizeConfig(param interface{}) (*i18n.LocalizeConfig, error) { 232 | switch paramValue := param.(type) { 233 | case string: 234 | localizeConfig := &i18n.LocalizeConfig{ 235 | MessageID: paramValue, 236 | } 237 | return localizeConfig, nil 238 | case *i18n.Message: 239 | localizeConfig := &i18n.LocalizeConfig{ 240 | DefaultMessage: paramValue, 241 | } 242 | return localizeConfig, nil 243 | case i18n.Message: 244 | localizeConfig := &i18n.LocalizeConfig{ 245 | DefaultMessage: ¶mValue, 246 | } 247 | return localizeConfig, nil 248 | case *i18n.LocalizeConfig: 249 | return paramValue, nil 250 | case i18n.LocalizeConfig: 251 | return ¶mValue, nil 252 | } 253 | 254 | msg := fmt.Sprintf("un supported localize param: %v", param) 255 | return nil, errors.New(msg) 256 | } 257 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gin-contrib/i18n 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.10.0 7 | github.com/nicksnyder/go-i18n/v2 v2.5.1 8 | golang.org/x/text v0.23.0 9 | gopkg.in/yaml.v3 v3.0.1 10 | ) 11 | 12 | require ( 13 | github.com/bytedance/sonic v1.13.2 // indirect 14 | github.com/bytedance/sonic/loader v0.2.4 // indirect 15 | github.com/cloudwego/base64x v0.1.5 // indirect 16 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 17 | github.com/gin-contrib/sse v1.0.0 // indirect 18 | github.com/go-playground/locales v0.14.1 // indirect 19 | github.com/go-playground/universal-translator v0.18.1 // indirect 20 | github.com/go-playground/validator/v10 v10.26.0 // indirect 21 | github.com/goccy/go-json v0.10.5 // indirect 22 | github.com/json-iterator/go v1.1.12 // indirect 23 | github.com/klauspost/cpuid/v2 v2.2.10 // indirect 24 | github.com/leodido/go-urn v1.4.0 // indirect 25 | github.com/mattn/go-isatty v0.0.20 // indirect 26 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 27 | github.com/modern-go/reflect2 v1.0.2 // indirect 28 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 29 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 30 | github.com/ugorji/go/codec v1.2.12 // indirect 31 | golang.org/x/arch v0.15.0 // indirect 32 | golang.org/x/crypto v0.36.0 // indirect 33 | golang.org/x/net v0.38.0 // indirect 34 | golang.org/x/sys v0.31.0 // indirect 35 | google.golang.org/protobuf v1.36.6 // indirect 36 | ) 37 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= 2 | github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 3 | github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= 4 | github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= 5 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 6 | github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= 7 | github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= 8 | github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= 9 | github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 10 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= 15 | github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= 16 | github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= 17 | github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= 18 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= 19 | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 20 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 21 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 22 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 23 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 24 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 25 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 26 | github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= 27 | github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= 28 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 29 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 30 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 31 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 32 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 33 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 34 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 35 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 36 | github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= 37 | github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 38 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 39 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 40 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 41 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 42 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 43 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 44 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 45 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 46 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 47 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 48 | github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk= 49 | github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ= 50 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 51 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 52 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 53 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 54 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 55 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 56 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 57 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 58 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 59 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 60 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 61 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 62 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 63 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 64 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 65 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 66 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 67 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 68 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 69 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 70 | golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw= 71 | golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= 72 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 73 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 74 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 75 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 76 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 77 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 78 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 79 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 80 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 81 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 82 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 83 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 84 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 85 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 86 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 87 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 88 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 89 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 90 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 91 | -------------------------------------------------------------------------------- /i18n.go: -------------------------------------------------------------------------------- 1 | package i18n 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "golang.org/x/text/language" 6 | ) 7 | 8 | // newI18n ... 9 | func newI18n(opts ...Option) GinI18n { 10 | // init ins 11 | ins := &ginI18nImpl{} 12 | 13 | // set ins property from opts 14 | for _, opt := range opts { 15 | opt(ins) 16 | } 17 | 18 | // if bundle isn't constructed then assign it from default 19 | if ins.bundle == nil { 20 | ins.SetBundle(defaultBundleConfig) 21 | } 22 | 23 | // if getLngHandler isn't constructed then assign it from default 24 | if ins.getLngHandler == nil { 25 | ins.getLngHandler = defaultGetLngHandler 26 | } 27 | 28 | return ins 29 | } 30 | 31 | // Localize ... 32 | func Localize(opts ...Option) gin.HandlerFunc { 33 | atI18n := newI18n(opts...) 34 | return func(context *gin.Context) { 35 | context.Set("i18n", atI18n) 36 | } 37 | } 38 | 39 | // GetMessage get the i18n message with error handling 40 | // param is one of these type: messageID, *i18n.LocalizeConfig 41 | // Example: 42 | // GetMessage(context, "hello") // messageID is hello 43 | // 44 | // GetMessage(context, &i18n.LocalizeConfig{ 45 | // MessageID: "welcomeWithName", 46 | // TemplateData: map[string]string{ 47 | // "name": context.Param("name"), 48 | // }, 49 | // }) 50 | func GetMessage(context *gin.Context, param interface{}) (string, error) { 51 | atI18n := context.Value("i18n").(GinI18n) 52 | return atI18n.GetMessage(context, param) 53 | } 54 | 55 | // MustGetMessage get the i18n message without error handling 56 | // param is one of these type: messageID, *i18n.LocalizeConfig 57 | // Example: 58 | // MustGetMessage(context, "hello") // messageID is hello 59 | // 60 | // MustGetMessage(context, &i18n.LocalizeConfig{ 61 | // MessageID: "welcomeWithName", 62 | // TemplateData: map[string]string{ 63 | // "name": context.Param("name"), 64 | // }, 65 | // }) 66 | func MustGetMessage(context *gin.Context, param interface{}) string { 67 | atI18n := context.MustGet("i18n").(GinI18n) 68 | return atI18n.MustGetMessage(context, param) 69 | } 70 | 71 | // HasLang check all i18n lang exists 72 | // Example: 73 | // HasLang(context, "ZH-cn") // return false or true 74 | func HasLang(context *gin.Context, language string) bool { 75 | atI18n := context.MustGet("i18n").(GinI18n) 76 | return atI18n.HasLang(language) 77 | } 78 | 79 | // GetDefaultLanguage get the default language 80 | // Example: 81 | // GetDefaultLanguage(context) 82 | func GetDefaultLanguage(context *gin.Context) language.Tag { 83 | atI18n := context.MustGet("i18n").(GinI18n) 84 | return atI18n.GetDefaultLanguage() 85 | } 86 | 87 | // GetCurrentLanguage get the current language 88 | // Example: 89 | // GetCurrentLanguage(context) 90 | func GetCurrentLanguage(context *gin.Context) language.Tag { 91 | atI18n := context.MustGet("i18n").(GinI18n) 92 | return atI18n.GetCurrentLanguage(context) 93 | } 94 | 95 | // I18n get GinI18n from gin.Context 96 | func I18n(context *gin.Context) GinI18n { 97 | return context.MustGet("i18n").(GinI18n) 98 | } 99 | -------------------------------------------------------------------------------- /i18n_test.go: -------------------------------------------------------------------------------- 1 | package i18n 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/nicksnyder/go-i18n/v2/i18n" 12 | "golang.org/x/text/language" 13 | ) 14 | 15 | // newServer ... 16 | func newServer() *gin.Engine { 17 | router := gin.New() 18 | router.Use(Localize()) 19 | 20 | router.GET("/", func(context *gin.Context) { 21 | context.String(http.StatusOK, MustGetMessage(context, "welcome")) 22 | }) 23 | 24 | router.GET("/messageId/:name", func(context *gin.Context) { 25 | context.String(http.StatusOK, MustGetMessage(context, &i18n.LocalizeConfig{ 26 | MessageID: "welcomeWithName", 27 | TemplateData: map[string]string{ 28 | "name": context.Param("name"), 29 | }, 30 | })) 31 | }) 32 | 33 | router.GET("/messageType/:name", func(context *gin.Context) { 34 | context.String(http.StatusOK, MustGetMessage(context, &i18n.LocalizeConfig{ 35 | DefaultMessage: &i18n.Message{ 36 | ID: "welcomeWithName", 37 | }, 38 | TemplateData: map[string]string{ 39 | "name": context.Param("name"), 40 | }, 41 | })) 42 | }) 43 | 44 | router.GET("/exist/:lang", func(context *gin.Context) { 45 | context.String(http.StatusOK, "%v", HasLang(context, context.Param("lang"))) 46 | }) 47 | router.GET("/lang/default", func(context *gin.Context) { 48 | context.String(http.StatusOK, "%s", GetDefaultLanguage(context).String()) 49 | }) 50 | router.GET("/lang/current", func(context *gin.Context) { 51 | context.String(http.StatusOK, "%s", GetCurrentLanguage(context).String()) 52 | }) 53 | router.GET("/age/:age", func(context *gin.Context) { 54 | context.String(http.StatusOK, MustGetMessage(context, i18n.LocalizeConfig{ 55 | MessageID: "welcomeWithAge", 56 | TemplateData: map[string]string{ 57 | "age": context.Param("age"), 58 | }, 59 | })) 60 | }) 61 | 62 | return router 63 | } 64 | 65 | // makeRequest ... 66 | func makeRequest( 67 | lng language.Tag, 68 | path string, 69 | ) string { 70 | req, _ := http.NewRequestWithContext(context.Background(), "GET", path, nil) 71 | req.Header.Add("Accept-Language", lng.String()) 72 | 73 | // Perform the request 74 | w := httptest.NewRecorder() 75 | r := newServer() 76 | r.ServeHTTP(w, req) 77 | 78 | return w.Body.String() 79 | } 80 | 81 | func TestI18nEN(t *testing.T) { 82 | type args struct { 83 | lng language.Tag 84 | path string 85 | } 86 | tests := []struct { 87 | name string 88 | args args 89 | want string 90 | }{ 91 | { 92 | name: "hello world", 93 | args: args{ 94 | path: "/", 95 | lng: language.English, 96 | }, 97 | want: "hello", 98 | }, 99 | { 100 | name: "hello alex - messageId", 101 | args: args{ 102 | path: "/messageId/alex", 103 | lng: language.English, 104 | }, 105 | want: "hello alex", 106 | }, 107 | { 108 | name: "hello alex - messageType", 109 | args: args{ 110 | path: "/messageType/alex", 111 | lng: language.English, 112 | }, 113 | want: "hello alex", 114 | }, 115 | { 116 | name: "18 years old", 117 | args: args{ 118 | path: "/age/18", 119 | lng: language.English, 120 | }, 121 | want: "I am 18 years old", 122 | }, 123 | // German 124 | { 125 | name: "hallo", 126 | args: args{ 127 | path: "/", 128 | lng: language.German, 129 | }, 130 | want: "hallo", 131 | }, 132 | { 133 | name: "hallo alex - messageId", 134 | args: args{ 135 | path: "/messageId/alex", 136 | lng: language.German, 137 | }, 138 | want: "hallo alex", 139 | }, 140 | { 141 | name: "hallo alex - messageType", 142 | args: args{ 143 | path: "/messageType/alex", 144 | lng: language.German, 145 | }, 146 | want: "hallo alex", 147 | }, 148 | { 149 | name: "18 jahre alt", 150 | args: args{ 151 | path: "/age/18", 152 | lng: language.German, 153 | }, 154 | want: "ich bin 18 Jahre alt", 155 | }, 156 | // French 157 | { 158 | name: "bonjour", 159 | args: args{ 160 | path: "/", 161 | lng: language.French, 162 | }, 163 | want: "bonjour", 164 | }, 165 | { 166 | name: "bonjour alex - messageId", 167 | args: args{ 168 | path: "/messageId/alex", 169 | lng: language.French, 170 | }, 171 | want: "bonjour alex", 172 | }, 173 | { 174 | name: "bonjour alex - messageType", 175 | args: args{ 176 | path: "/messageType/alex", 177 | lng: language.French, 178 | }, 179 | want: "bonjour alex", 180 | }, 181 | { 182 | name: "18 ans", 183 | args: args{ 184 | path: "/age/18", 185 | lng: language.French, 186 | }, 187 | want: "j'ai 18 ans", 188 | }, 189 | // has exist 190 | { 191 | name: "i81n lang exist", 192 | args: args{ 193 | path: fmt.Sprintf("/exist/%s", language.English.String()), 194 | lng: language.English, 195 | }, 196 | want: "true", 197 | }, 198 | { 199 | name: "i81n lang not exist", 200 | args: args{ 201 | path: fmt.Sprintf("/exist/%s", language.SimplifiedChinese.String()), 202 | lng: language.English, 203 | }, 204 | want: "false", 205 | }, 206 | // default lang 207 | { 208 | name: "i81n is default " + language.English.String(), 209 | args: args{ 210 | path: "/lang/default", 211 | lng: language.English, 212 | }, 213 | want: language.English.String(), 214 | }, 215 | { 216 | name: "i81n is not default " + language.German.String(), 217 | args: args{ 218 | path: "/lang/default", 219 | lng: language.German, 220 | }, 221 | want: language.English.String(), 222 | }, 223 | // current lang 224 | { 225 | name: "i81n is current " + language.English.String(), 226 | args: args{ 227 | path: "/lang/current", 228 | lng: language.English, 229 | }, 230 | want: language.English.String(), 231 | }, 232 | { 233 | name: "i81n is not current " + language.English.String(), 234 | args: args{ 235 | path: "/lang/current", 236 | lng: language.German, 237 | }, 238 | want: language.German.String(), 239 | }, 240 | } 241 | for _, tt := range tests { 242 | t.Run(tt.name, func(t *testing.T) { 243 | if got := makeRequest(tt.args.lng, tt.args.path); got != tt.want { 244 | t.Errorf("makeRequest() = %v, want %v", got, tt.want) 245 | } 246 | }) 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /interface.go: -------------------------------------------------------------------------------- 1 | package i18n 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "golang.org/x/text/language" 6 | ) 7 | 8 | // GinI18n is an interface that defines methods for internationalization (i18n) in a Gin web framework context. 9 | // It provides methods to get localized messages and configure the i18n bundle and language handler. 10 | type GinI18n interface { 11 | // GetMessage retrieves a localized message based on the provided context and parameter. 12 | // It returns the localized message as a string and an error if the message could not be retrieved. 13 | GetMessage(context *gin.Context, param interface{}) (string, error) 14 | 15 | // MustGetMessage retrieves a localized message based on the provided context and parameter. 16 | // It returns the localized message as a string and panics if the message could not be retrieved. 17 | MustGetMessage(context *gin.Context, param interface{}) string 18 | 19 | // SetBundle sets the i18n bundle configuration. 20 | SetBundle(cfg *BundleCfg) 21 | 22 | // SetGetLngHandler sets the handler function to determine the language from the context. 23 | SetGetLngHandler(handler GetLngHandler) 24 | 25 | // HasLang checks if the given language is supported by the i18n bundle. 26 | // It returns true if the language is supported, false otherwise. 27 | HasLang(language string) bool 28 | 29 | // GetDefaultLanguage returns the default language tag. 30 | // It returns the default language tag. 31 | GetDefaultLanguage() language.Tag 32 | 33 | // GetCurrentLanguage returns the current language tag from the context. 34 | // It returns the current language tag. 35 | GetCurrentLanguage(context *gin.Context) language.Tag 36 | } 37 | -------------------------------------------------------------------------------- /option.go: -------------------------------------------------------------------------------- 1 | package i18n 2 | 3 | import ( 4 | "github.com/nicksnyder/go-i18n/v2/i18n" 5 | "golang.org/x/text/language" 6 | ) 7 | 8 | // BundleCfg represents the configuration options for an i18n bundle. 9 | type BundleCfg struct { 10 | DefaultLanguage language.Tag // DefaultLanguage specifies the default language for the bundle. 11 | FormatBundleFile string // FormatBundleFile specifies the file format for the bundle. 12 | AcceptLanguage []language.Tag // AcceptLanguage specifies the accepted languages for the bundle. 13 | RootPath string // RootPath specifies the root path for the bundle. 14 | UnmarshalFunc i18n.UnmarshalFunc // UnmarshalFunc specifies the function used for unmarshaling bundle files. 15 | Loader Loader // Loader specifies the loader for loading bundle files. 16 | } 17 | 18 | type Loader interface { 19 | LoadMessage(path string) ([]byte, error) 20 | } 21 | 22 | type LoaderFunc func(path string) ([]byte, error) 23 | 24 | func (f LoaderFunc) LoadMessage(path string) ([]byte, error) { return f(path) } 25 | 26 | // WithBundle returns an Option that sets the bundle configuration for GinI18n. 27 | // If the loader is not provided in the BundleCfg, the defaultLoader will be used. 28 | func WithBundle(config *BundleCfg) Option { 29 | return func(g GinI18n) { 30 | if config.Loader == nil { 31 | config.Loader = defaultLoader 32 | } 33 | g.SetBundle(config) 34 | } 35 | } 36 | 37 | // WithGetLngHandle sets the handler function for retrieving the current language. 38 | // The provided handler function should accept a GinI18n instance and return the current language as a string. 39 | // This option allows you to customize how the current language is determined. 40 | func WithGetLngHandle(handler GetLngHandler) Option { 41 | return func(g GinI18n) { 42 | g.SetGetLngHandler(handler) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /testdata/localize/de.yaml: -------------------------------------------------------------------------------- 1 | welcome: hallo 2 | welcomeWithName: hallo {{ .name }} 3 | welcomeWithAge: ich bin {{ .age }} Jahre alt -------------------------------------------------------------------------------- /testdata/localize/en.yaml: -------------------------------------------------------------------------------- 1 | welcome: hello 2 | welcomeWithName: hello {{ .name }} 3 | welcomeWithAge: I am {{ .age }} years old 4 | -------------------------------------------------------------------------------- /testdata/localize/fr.yaml: -------------------------------------------------------------------------------- 1 | welcome: bonjour 2 | welcomeWithName: bonjour {{ .name }} 3 | welcomeWithAge: j'ai {{ .age }} ans -------------------------------------------------------------------------------- /testdata/localizeJSON/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome": "hallo", 3 | "welcomeWithName": "hallo {{ .name }}", 4 | "welcomeWithAge": "ich bin {{ .age }} Jahre alt" 5 | } 6 | -------------------------------------------------------------------------------- /testdata/localizeJSON/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome": "hello", 3 | "welcomeWithName": "hello {{ .name }}", 4 | "welcomeWithAge": "I am {{ .age }} years old" 5 | } 6 | -------------------------------------------------------------------------------- /testdata/localizeJSON/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome": "你好", 3 | "welcomeWithName": "你好 {{ .name }}", 4 | "welcomeWithAge": "我今年 {{ .age }} 岁了" 5 | } 6 | -------------------------------------------------------------------------------- /type.go: -------------------------------------------------------------------------------- 1 | package i18n 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | type ( 6 | // Option is a function type that takes a GinI18n instance and applies a configuration to it. 7 | // It is used to customize the behavior of the GinI18n middleware. 8 | GetLngHandler = func(context *gin.Context, defaultLng string) string 9 | 10 | // Option is a function type that takes a GinI18n instance and applies a configuration to it. 11 | Option func(GinI18n) 12 | ) 13 | --------------------------------------------------------------------------------