├── testdata ├── en │ ├── invalid.txt │ └── default.ini ├── zh-CN │ ├── default.ini │ └── other.ini ├── zh-CN.ini └── en.ini ├── .github ├── dependabot.yml ├── ISSUE_TEMPLATE │ └── bug_report.md ├── changelog.yml └── workflows │ ├── release.yml │ ├── go.yml │ └── codeql.yml ├── .gitignore ├── go.mod ├── LICENSE ├── go.sum ├── std_test.go ├── std.go ├── README.zh-CN.md ├── README.md ├── i18n_test.go └── i18n.go /testdata/en/invalid.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/en/default.ini: -------------------------------------------------------------------------------- 1 | name = inhere 2 | -------------------------------------------------------------------------------- /testdata/zh-CN/default.ini: -------------------------------------------------------------------------------- 1 | use-for = 语言管理 2 | -------------------------------------------------------------------------------- /testdata/zh-CN/other.ini: -------------------------------------------------------------------------------- 1 | [other] 2 | key = val0 3 | -------------------------------------------------------------------------------- /testdata/zh-CN.ini: -------------------------------------------------------------------------------- 1 | name = 博客 2 | argMsg = 你好 %s, 欢迎 3 | 4 | [menu] 5 | home = 首页 6 | about = 关于 7 | -------------------------------------------------------------------------------- /testdata/en.ini: -------------------------------------------------------------------------------- 1 | name = Blog 2 | argMsg = hello %s, welcome 3 | onlyInEn = val0 4 | 5 | [menu] 6 | home = Home 7 | about = About 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: gomod 5 | directory: "/" 6 | schedule: 7 | interval: daily 8 | open-pull-requests-limit: 10 9 | 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | # Check for updates to GitHub Actions every weekday 14 | interval: "daily" 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.swp 3 | .idea 4 | *.patch 5 | ### Go template 6 | # Binaries for programs and plugins 7 | *.exe 8 | *.exe~ 9 | *.dll 10 | *.so 11 | *.dylib 12 | 13 | # Test binary, build with `go test -c` 14 | *.test 15 | 16 | # Output of the go coverage tool, specifically when used with LiteIDE 17 | *.out 18 | .DS_Store 19 | #go.sum 20 | vendor 21 | oryxBuildBinary 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gookit/i18n 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/gookit/goutil v0.7.2 7 | github.com/gookit/ini/v2 v2.3.2 8 | ) 9 | 10 | require ( 11 | github.com/go-viper/mapstructure/v2 v2.4.0 // indirect 12 | golang.org/x/sync v0.11.0 // indirect 13 | golang.org/x/sys v0.30.0 // indirect 14 | golang.org/x/term v0.29.0 // indirect 15 | golang.org/x/text v0.22.0 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: inhere 7 | 8 | --- 9 | 10 | **System (please complete the following information):** 11 | 12 | - OS: `linux` [e.g. linux, macOS] 13 | - GO Version: `1.13` [e.g. `1.13`] 14 | - Pkg Version: `1.1.1` [e.g. `1.1.1`] 15 | 16 | **Describe the bug** 17 | 18 | A clear and concise description of what the bug is. 19 | 20 | **To Reproduce** 21 | 22 | ```go 23 | // go code 24 | ``` 25 | 26 | **Expected behavior** 27 | 28 | A clear and concise description of what you expected to happen. 29 | 30 | **Screenshots** 31 | 32 | If applicable, add screenshots to help explain your problem. 33 | 34 | **Additional context** 35 | 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /.github/changelog.yml: -------------------------------------------------------------------------------- 1 | title: '## Change Log' 2 | # style allow: simple, markdown(mkdown), ghr(gh-release) 3 | style: gh-release 4 | # group names 5 | names: [Refactor, Fixed, Feature, Update, Other] 6 | # if empty will auto fetch by git remote 7 | #repo_url: https://github.com/gookit/goutil 8 | 9 | filters: 10 | # message length should >= 12 11 | - name: msg_len 12 | min_len: 12 13 | # message words should >= 3 14 | - name: words_len 15 | min_len: 3 16 | - name: keyword 17 | keyword: format code 18 | exclude: true 19 | - name: keywords 20 | keywords: format code, action test 21 | exclude: true 22 | 23 | # group match rules 24 | # not matched will use 'Other' group. 25 | rules: 26 | - name: Refactor 27 | start_withs: [refactor, break] 28 | contains: ['refactor:'] 29 | - name: Fixed 30 | start_withs: [fix] 31 | contains: ['fix:'] 32 | - name: Feature 33 | start_withs: [feat, new] 34 | contains: [feature, 'feat:'] 35 | - name: Update 36 | start_withs: [up] 37 | contains: ['update:', 'up:'] 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 inhere 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= 2 | github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 3 | github.com/gookit/goutil v0.7.2 h1:NSiqWWY+BT0MwIlKDeSVPfQmr9xTkkAqwDjhplobdgo= 4 | github.com/gookit/goutil v0.7.2/go.mod h1:vJS9HXctYTCLtCsZot5L5xF+O1oR17cDYO9R0HxBmnU= 5 | github.com/gookit/ini/v2 v2.3.2 h1:W6tzOGE6zOLQelH2xhcH8BIBZPtnEpJgQ+J6SsAKBSw= 6 | github.com/gookit/ini/v2 v2.3.2/go.mod h1:StKSqY5niArRwYBS8Z71+iWUt5ow47qt359sS9YQLYY= 7 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 8 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 9 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 10 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 11 | golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= 12 | golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= 13 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 14 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Tag-release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | release: 10 | name: Release new version 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 10 13 | strategy: 14 | fail-fast: true 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v6 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Setup ENV 23 | # https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-an-environment-variable 24 | run: | 25 | echo "RELEASE_TAG=${GITHUB_REF:10}" >> $GITHUB_ENV 26 | echo "RELEASE_NAME=$GITHUB_WORKFLOW" >> $GITHUB_ENV 27 | 28 | - name: Generate changelog 29 | run: | 30 | curl https://github.com/gookit/gitw/releases/latest/download/chlog-linux-amd64 -L -o /usr/local/bin/chlog 31 | chmod a+x /usr/local/bin/chlog 32 | chlog -c .github/changelog.yml -o changelog.md prev last 33 | 34 | # https://github.com/softprops/action-gh-release 35 | - name: Create release and upload assets 36 | uses: softprops/action-gh-release@v2 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | with: 40 | name: ${{ env.RELEASE_TAG }} 41 | tag_name: ${{ env.RELEASE_TAG }} 42 | body_path: changelog.md 43 | token: ${{ secrets.GITHUB_TOKEN }} 44 | # files: macos-chlog.exe 45 | -------------------------------------------------------------------------------- /std_test.go: -------------------------------------------------------------------------------- 1 | package i18n_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gookit/goutil/testutil/assert" 7 | "github.com/gookit/i18n" 8 | ) 9 | 10 | func TestDefault(t *testing.T) { 11 | is := assert.New(t) 12 | languages := map[string]string{ 13 | "en": "English", 14 | "zh-CN": "简体中文", 15 | // "zh-TW": "繁体中文", 16 | } 17 | 18 | defer i18n.Reset() 19 | i18n.Init("testdata", "en", languages) 20 | 21 | m := i18n.Default() 22 | is.IsType(new(i18n.I18n), m) 23 | 24 | is.Eq("Blog", i18n.T("en", "name")) 25 | 26 | is.Eq("Blog", i18n.Tr("en", "name")) 27 | is.Eq("Blog", i18n.Dt("name")) 28 | is.Eq("Blog", i18n.Dtr("name")) 29 | is.Eq("Blog", i18n.DefTr("name")) 30 | is.Eq("博客", i18n.T("zh-CN", "name")) 31 | is.Eq("博客", i18n.Tr("zh-CN", "name")) 32 | } 33 | 34 | func TestAddLang(t *testing.T) { 35 | defer i18n.Reset() 36 | 37 | lang := "custom" 38 | i18n.AddLang(lang, "") 39 | i18n.Config(func(l *i18n.I18n) { 40 | l.DefaultLang = "en" 41 | }) 42 | 43 | assert.True(t, i18n.Default().HasLang(lang)) 44 | assert.NotEmpty(t, i18n.Default().Languages()) 45 | 46 | err := i18n.Std().SetValues(lang, "", map[string]string{ 47 | "name": "inhere", 48 | "age": "234", 49 | }) 50 | assert.NoErr(t, err) 51 | 52 | assert.NotNil(t, i18n.LangData(lang)) 53 | assert.Eq(t, "inhere", i18n.LangData(lang).String("name")) 54 | assert.Eq(t, 234, i18n.LangData(lang).Int("age")) 55 | 56 | assert.Eq(t, "inhere", i18n.Tr(lang, "name")) 57 | assert.Eq(t, "234", i18n.T(lang, "age")) 58 | } 59 | -------------------------------------------------------------------------------- /std.go: -------------------------------------------------------------------------------- 1 | package i18n 2 | 3 | import "github.com/gookit/ini/v2" 4 | 5 | /************************************************************ 6 | * default instance 7 | ************************************************************/ 8 | 9 | // default instance 10 | var std = NewEmpty() 11 | 12 | // Std get default i18n instance 13 | func Std() *I18n { return std } 14 | 15 | // Default get default i18n instance 16 | func Default() *I18n { return std } 17 | 18 | // Reset std instance 19 | func Reset() { std = NewEmpty() } 20 | 21 | // Init the default language instance 22 | func Init(langDir, defLang string, languages map[string]string) *I18n { 23 | std.langDir = langDir 24 | std.languages = languages 25 | std.DefaultLang = defLang 26 | 27 | return std.Init() 28 | } 29 | 30 | // Config the default instance 31 | func Config(fn func(l *I18n)) { 32 | fn(std) 33 | } 34 | 35 | // T translate language key to value string 36 | func T(lang, key string, args ...interface{}) string { 37 | return std.T(lang, key, args...) 38 | } 39 | 40 | // Tr translate language key to value string 41 | func Tr(lang, key string, args ...interface{}) string { 42 | return std.Tr(lang, key, args...) 43 | } 44 | 45 | // Dt translate language key from default language 46 | func Dt(key string, args ...interface{}) string { 47 | return std.DefTr(key, args...) 48 | } 49 | 50 | // Dtr translate language key from default language 51 | func Dtr(key string, args ...interface{}) string { 52 | return std.DefTr(key, args...) 53 | } 54 | 55 | // DefTr translate language key from default language 56 | func DefTr(key string, args ...interface{}) string { 57 | return std.DefTr(key, args...) 58 | } 59 | 60 | // AddLang register and init new language. alias of NewLang() 61 | func AddLang(lang string, name string) { 62 | std.NewLang(lang, name) 63 | } 64 | 65 | // LangData get language data instance 66 | func LangData(lang string) *ini.Ini { 67 | return std.Lang(lang) 68 | } 69 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Unit-Tests 2 | on: 3 | pull_request: 4 | paths: 5 | - 'go.mod' 6 | - '**.go' 7 | - '**.yml' 8 | push: 9 | paths: 10 | - '**.go' 11 | - 'go.mod' 12 | - '**.yml' 13 | 14 | jobs: 15 | 16 | test: 17 | name: Test on go ${{ matrix.go_version }} 18 | runs-on: ubuntu-latest 19 | strategy: 20 | matrix: 21 | go_version: [1.21, 1.22, 1.23, 1.24] 22 | 23 | steps: 24 | - name: Check out code 25 | uses: actions/checkout@v6 26 | 27 | - name: Setup Go Faster 28 | uses: WillAbides/setup-go-faster@v1.14.0 29 | timeout-minutes: 3 30 | with: 31 | go-version: ${{ matrix.go_version }} 32 | 33 | - name: Revive check 34 | uses: morphy2k/revive-action@v2.7.8 35 | with: 36 | # Exclude patterns, separated by semicolons (optional) 37 | exclude: "./_examples/..." 38 | 39 | - name: Run static check 40 | uses: reviewdog/action-staticcheck@v1 41 | if: ${{ github.event_name == 'pull_request'}} 42 | with: 43 | github_token: ${{ secrets.github_token }} 44 | # Change reviewdog reporter if you need [github-pr-check,github-check,github-pr-review]. 45 | reporter: github-pr-check 46 | # Report all results. [added,diff_context,file,nofilter]. 47 | filter_mode: added 48 | # Exit with 1 when it find at least one finding. 49 | fail_on_error: true 50 | 51 | - name: Run unit tests 52 | # run: go test -v -cover ./... 53 | # must add " for profile.cov on windows OS 54 | run: go test -v -coverprofile="profile.cov" ./... 55 | 56 | - name: Send coverage 57 | uses: shogo82148/actions-goveralls@v1 58 | with: 59 | path-to-profile: profile.cov 60 | flag-name: Go-${{ matrix.go_version }} 61 | parallel: true 62 | 63 | # notifies that all test jobs are finished. 64 | # https://github.com/shogo82148/actions-goveralls 65 | finish: 66 | needs: test 67 | runs-on: ubuntu-latest 68 | steps: 69 | - uses: shogo82148/actions-goveralls@v1 70 | with: 71 | parallel-finished: true 72 | -------------------------------------------------------------------------------- /.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: '40 14 * * 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://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v6 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v4 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 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v4 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v4 73 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # I18n 2 | 3 | [![Actions Status](https://github.com/gookit/i18n/workflows/Unit-Tests/badge.svg)](https://github.com/gookit/i18n/actions) 4 | [![Coverage Status](https://coveralls.io/repos/github/gookit/i18n/badge.svg?branch=master)](https://coveralls.io/github/gookit/i18n?branch=master) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/gookit/i18n)](https://goreportcard.com/report/github.com/gookit/i18n) 6 | [![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/gookit/i18n)](https://github.com/gookit/i18n) 7 | ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/gookit/i18n?style=flat-square) 8 | [![Go Reference](https://pkg.go.dev/badge/github.com/gookit/i18n.svg)](https://pkg.go.dev/github.com/gookit/i18n) 9 | 10 | 使用 `INI` 文件实现的多语言数据的管理和使用。 11 | 12 | > **[EN README](README.md)** 13 | 14 | ## 功能简介 15 | 16 | - 使用简单,支持加载多个语言,多个配置文件 17 | - 两种数据加载模式:单文件 `FileMode`(默认) 、文件夹 `DirMode` 18 | - 支持设置默认语言,备用语言;当在默认语言数据没找到时,自动尝试到备用语言查找 19 | - 支持参数替换,有两种模式 20 | - `SprintfMode` 通过 `fmt.Sprintf` 替换参数 21 | - `ReplaceMode` 则使用 `strings.Replacer` 替换 22 | 23 | ## 安装 24 | 25 | ```bash 26 | go get github.com/gookit/i18n 27 | ``` 28 | 29 | ## 文档 30 | 31 | - [Go doc](https://pkg.go.dev/github.com/gookit/i18n) 32 | 33 | ## 开始使用 34 | 35 | **使用 `FileMode` 模式的语言文件结构**: 36 | 37 | ```text 38 | lang/ 39 | en.ini 40 | ru.ini 41 | zh-CN.ini 42 | zh-TW.ini 43 | ... ... 44 | ``` 45 | 46 | **使用 `DirMode` 模式的语言文件结构**: 47 | 48 | ```text 49 | lang/ 50 | en/ 51 | default.ini 52 | ... ... 53 | zh-CN/ 54 | default.ini 55 | ... ... 56 | zh-TW/ 57 | default.ini 58 | ... ... 59 | ``` 60 | 61 | ### 初始化 62 | 63 | ```go 64 | import "github/gookit/i18n" 65 | 66 | defaultLang := "en" 67 | languages := map[string]string{ 68 | "en": "English", 69 | "zh-CN": "简体中文", 70 | // "zh-TW": "繁体中文", 71 | } 72 | 73 | // 这里直接初始化的默认实例 74 | i18n.Init("conf/lang", defaultLang, languages) 75 | ``` 76 | 77 | **或者创建自定义的新实例** 78 | 79 | ```go 80 | myI18n := i18n.New(langDir string, defLang string, languages) 81 | 82 | myI18n := i18n.NewEmpty() 83 | ``` 84 | 85 | ## 翻译数据 86 | 87 | ```go 88 | // 从默认语言翻译 89 | msg = i18n.Dtr("key") 90 | // with arguments. 91 | msg = i18n.DefTr("key1", "arg1", "arg2") 92 | 93 | // 从指定的语言翻译 94 | msg := i18n.Tr("en", "key") 95 | ``` 96 | 97 | **方法列表**: 98 | 99 | ```go 100 | // 从默认语言翻译 101 | func Dt(key string, args ...interface{}) string 102 | func Dtr(key string, args ...interface{}) string 103 | func DefTr(key string, args ...interface{}) string 104 | 105 | // 从指定的语言翻译 106 | func T(lang, key string, args ...interface{}) string 107 | func Tr(lang, key string, args ...interface{}) string 108 | ``` 109 | 110 | ## 参数替换模式 111 | 112 | ### 使用 `SprintfMode` 模式 113 | 114 | 默认就是 `SprintfMode` 模式, 内部使用 `fmt.Spritf()` 进行参数的替换处理 115 | 116 | ```ini 117 | # en.ini 118 | desc = I am %s, age is %d 119 | ``` 120 | 121 | **按顺序传入参数使用**: 122 | 123 | ```go 124 | msg := i18n.Tr("en", "desc", "tom", 22) 125 | // Output: "I am tom, age is 22" 126 | ``` 127 | 128 | ### 使用 `ReplaceMode` 模式 129 | 130 | 使用 `ReplaceMode` 模式, 内部使用 `strings.Replacer` 进行参数的替换处理 131 | 132 | **启用 `ReplaceMode` 替换模式**: 133 | 134 | ```go 135 | // set mode 136 | i18n.Config(func(l *i18n.I18n) { 137 | l.TransMode = i18n.ReplaceMode 138 | }) 139 | 140 | // OR 141 | i18n.Std().TransMode = i18n.ReplaceMode 142 | ``` 143 | 144 | 语言配置示例: 145 | 146 | ```ini 147 | # en.ini 148 | desc = I am {name}, age is {age} 149 | ``` 150 | 151 | **按kv顺序传入参数使用**: 152 | 153 | ```go 154 | msg := i18n.Tr("en", "desc", "name", "tom", "age", 22) 155 | // Output: "I am tom, age is 22" 156 | ``` 157 | 158 | **传入 kv-map 参数使用**: 159 | 160 | ```go 161 | msg := i18n.Tr("en", "desc", map[string]interface{}{ 162 | "name": "tom", 163 | "age": 22, 164 | }) 165 | // Output: "I am tom, age is 22" 166 | ``` 167 | 168 | ## 测试 169 | 170 | ```bash 171 | go test -cover 172 | ``` 173 | 174 | ## 依赖包 175 | 176 | - [gookit/ini](https://github.com/gookit/ini) 功能强大的 INI 解析管理 177 | 178 | ## License 179 | 180 | **[MIT](LICENSE)** 181 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # I18n 2 | 3 | [![Actions Status](https://github.com/gookit/i18n/workflows/Unit-Tests/badge.svg)](https://github.com/gookit/i18n/actions) 4 | [![Coverage Status](https://coveralls.io/repos/github/gookit/i18n/badge.svg?branch=master)](https://coveralls.io/github/gookit/i18n?branch=master) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/gookit/i18n)](https://goreportcard.com/report/github.com/gookit/i18n) 6 | [![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/gookit/i18n)](https://github.com/gookit/i18n) 7 | ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/gookit/i18n?style=flat-square) 8 | [![Go Reference](https://pkg.go.dev/badge/github.com/gookit/i18n.svg)](https://pkg.go.dev/github.com/gookit/i18n) 9 | 10 | Management and use of multilingual data using `INI` files. 11 | 12 | > **[中文说明](README.zh-CN.md)** 13 | 14 | ## Features 15 | 16 | - Easy to use, supports loading multiple languages, multiple files 17 | - Two data loading modes: single file `FileMode`(default), folder `DirMode` 18 | - Support to set the default language and fallback language 19 | - when the default language data is not found, it will automatically try to find the fallback language 20 | - Support parameter replacement, there are two modes 21 | - `SprintfMode` replaces parameters via `fmt.Sprintf` 22 | - `ReplaceMode` uses func `strings.Replacer` 23 | 24 | ## Install 25 | 26 | ```shell 27 | go get github.com/gookit/i18n 28 | ``` 29 | 30 | ## Godoc 31 | 32 | - [go doc](https://pkg.go.dev/github.com/gookit/i18n) 33 | 34 | ## Usage 35 | 36 | **Structs on use single `FileMode` mode**: 37 | 38 | ```text 39 | lang/ 40 | en.ini 41 | ru.ini 42 | zh-CN.ini 43 | zh-TW.ini 44 | ... ... 45 | ``` 46 | 47 | **Structs on use folder `DirMode` mode**: 48 | 49 | ```text 50 | lang/ 51 | en/ 52 | default.ini 53 | ... ... 54 | zh-CN/ 55 | default.ini 56 | ... ... 57 | zh-TW/ 58 | default.ini 59 | ... ... 60 | ``` 61 | 62 | ### Init i18n 63 | 64 | ```go 65 | import "github/gookit/i18n" 66 | 67 | languages := map[string]string{ 68 | "en": "English", 69 | "zh-CN": "简体中文", 70 | // "zh-TW": "繁体中文", 71 | } 72 | 73 | // The default instance initialized directly here 74 | i18n.Init("conf/lang", "en", languages) 75 | 76 | // Create a custom new instance 77 | // i18n.New(langDir string, defLang string, languages) 78 | // i18n.NewEmpty() 79 | ``` 80 | 81 | ### Translate message 82 | 83 | ```go 84 | // Translate from default language 85 | msg = i18n.Dt("key") 86 | // with arguments. 87 | msg = i18n.DefTr("key1", "arg1", "arg2") 88 | 89 | // Translate from the specified language 90 | msg := i18n.Tr("en", "key") 91 | ``` 92 | 93 | **Function list**: 94 | 95 | ```go 96 | // Translate from default language 97 | func Dt(key string, args ...interface{}) string 98 | func Dtr(key string, args ...interface{}) string 99 | func DefTr(key string, args ...interface{}) string 100 | 101 | // Translate from the specified language 102 | func T(lang, key string, args ...interface{}) string 103 | func Tr(lang, key string, args ...interface{}) string 104 | ``` 105 | 106 | ## Parameters replacement mode 107 | 108 | ### Use sprintf mode 109 | 110 | > TIP: default mode is `SprintfMode` 111 | 112 | ```ini 113 | # en.ini 114 | desc = I am %s, age is %d 115 | ``` 116 | 117 | Usage with parameters like sprintf: 118 | 119 | ```go 120 | msg := i18n.Tr("en", "desc", "tom", 22) 121 | // Output: "I am tom, age is 22" 122 | ``` 123 | 124 | ### Use replace mode 125 | 126 | Enable replace mode: 127 | 128 | ```go 129 | // set mode 130 | i18n.Std().TransMode = i18n.ReplaceMode 131 | 132 | // OR 133 | i18n.Config(func(l *i18n.I18n) { 134 | l.TransMode = i18n.ReplaceMode 135 | }) 136 | ``` 137 | 138 | Examples for language data: 139 | 140 | ```ini 141 | # en.ini 142 | desc = I am {name}, age is {age} 143 | ``` 144 | 145 | **Usage with parameters**: 146 | 147 | ```go 148 | // args is {"name": "tom", "age": 22} 149 | msg := i18n.Tr("en", "desc", "name", "tom", "age", 22) 150 | // Output: "I am tom, age is 22" 151 | ``` 152 | 153 | **Usage with kv-map parameters**: 154 | 155 | ```go 156 | msg := i18n.Tr("en", "desc", map[string]interface{}{ 157 | "name": "tom", 158 | "age": 22, 159 | }) 160 | // Output: "I am tom, age is 22" 161 | ``` 162 | 163 | ## Tests 164 | 165 | ```bash 166 | go test -cover 167 | ``` 168 | 169 | ## Dep packages 170 | 171 | - [gookit/ini](https://github.com/gookit/ini) is an INI config file/data manage implement 172 | 173 | ## License 174 | 175 | **[MIT](LICENSE)** 176 | -------------------------------------------------------------------------------- /i18n_test.go: -------------------------------------------------------------------------------- 1 | package i18n 2 | 3 | // test cover details: https://gocover.io/github.com/gookit/i18n 4 | import ( 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/gookit/goutil/testutil/assert" 9 | "github.com/gookit/ini/v2" 10 | ) 11 | 12 | func Example() { 13 | languages := map[string]string{ 14 | "en": "English", 15 | "zh-CN": "简体中文", 16 | // "zh-TW": "繁体中文", 17 | } 18 | 19 | l := New("testdata", "en", languages) 20 | l.Init() 21 | 22 | fmt.Printf("name: %s\n", l.T("en", "name")) 23 | fmt.Printf("name: %s\n", l.Dt("name")) 24 | fmt.Printf("name: %s\n", l.Tr("zh-CN", "name")) 25 | fmt.Printf("use args: %s\n", l.DefTr("argMsg", "inhere")) 26 | 27 | // Output: 28 | // name: Blog 29 | // name: Blog 30 | // name: 博客 31 | // use args: hello inhere, welcome 32 | } 33 | 34 | func TestNewWithInit(t *testing.T) { 35 | is := assert.New(t) 36 | 37 | languages := map[string]string{ 38 | "en": "English", 39 | "zh-CN": "简体中文", 40 | // "zh-TW": "繁体中文", 41 | } 42 | 43 | m := NewWithInit("testdata", "en", languages) 44 | is.True(m.HasLang("zh-CN")) 45 | is.False(m.HasLang("zh-TW")) 46 | is.Eq(FileMode, m.LoadMode) 47 | 48 | str := m.Tr("zh-TW", "key") 49 | is.Eq("key", str) 50 | 51 | is.True(m.HasKey("en", "onlyInEn")) 52 | is.False(m.HasKey("zh-CN", "onlyInEn")) 53 | is.False(m.HasKey("no-lang", "key")) 54 | 55 | ls := m.Languages() 56 | is.Eq("English", ls["en"]) 57 | 58 | // use args 59 | str = m.DefTr("argMsg", "inhere") 60 | is.Contains(str, "inhere") 61 | 62 | // fallback lang 63 | m.FallbackLang = "en" 64 | str = m.Tr("zh-CN", "onlyInEn") 65 | is.Eq("val0", str) 66 | 67 | str = m.Tr("zh-CN", "noKey") 68 | is.Eq("noKey", str) 69 | 70 | str = m.Tr("no-lang", "argMsg", "inhere") 71 | is.Contains(str, "inhere") 72 | 73 | str = m.Tr("no-lang", "no-key") 74 | is.Eq("no-key", str) 75 | 76 | // get lang 77 | l := m.Lang("no-lang") 78 | is.Nil(l) 79 | 80 | l = m.Lang("en") 81 | is.NotNil(l) 82 | is.Eq("Blog", l.String("name")) 83 | 84 | ok := m.DelLang("zh-CN") 85 | is.True(ok) 86 | is.False(m.HasLang("zh-CN")) 87 | 88 | is.Panics(func() { 89 | languages["not-exist"] = "not-Exist" 90 | NewWithInit("testdata", "en", languages) 91 | }) 92 | } 93 | 94 | func TestDirMode(t *testing.T) { 95 | is := assert.New(t) 96 | languages := map[string]string{ 97 | "en": "English", 98 | "zh-CN": "简体中文", 99 | // "zh-TW": "繁体中文", 100 | } 101 | 102 | m := New("testdata", "en", languages) 103 | // setting 104 | m.LoadMode = DirMode 105 | m.Init() 106 | 107 | is.True(m.HasLang("zh-CN")) 108 | is.False(m.HasLang("zh-TW")) 109 | is.Eq(DirMode, m.LoadMode) 110 | 111 | is.Eq("inhere", m.Dt("name")) 112 | is.Eq("inhere", m.DefTr("name")) 113 | is.Eq("语言管理", m.Tr("zh-CN", "use-for")) 114 | 115 | fmt.Println(m.Lang("zh-CN").Data()) 116 | 117 | is.Panics(func() { 118 | m := New("testdata", "en", languages) 119 | // setting invalid mode 120 | m.LoadMode = 3 121 | m.Init() 122 | }) 123 | 124 | is.Panics(func() { 125 | // invalid lang 126 | languages["not-exist"] = "not-Exist" 127 | 128 | m := New("testdata", "en", languages) 129 | m.LoadMode = DirMode 130 | m.Init() 131 | }) 132 | } 133 | 134 | func TestI18n_TransMode(t *testing.T) { 135 | is := assert.New(t) 136 | 137 | m := NewEmpty() 138 | m.TransMode = ReplaceMode 139 | m.Add("en", "English") 140 | 141 | err := m.LoadString("en", "desc = i am {name}, age is {age}") 142 | is.NoErr(err) 143 | 144 | is.Eq("i am tom, age is 22", m.Tr("en", "desc", "name", "tom", "age", 22)) 145 | is.Eq("i am tom, age is 22", m.Tr("en", "desc", map[string]interface{}{ 146 | "name": "tom", 147 | "age": 22, 148 | })) 149 | is.Eq( 150 | "i am tom, age is CANNOT-TO-STRING", 151 | m.Tr("en", "desc", map[string]interface{}{"name": "tom", "age": []int{2}}), 152 | ) 153 | } 154 | 155 | func TestI18n_NewLang(t *testing.T) { 156 | is := assert.New(t) 157 | 158 | l := NewEmpty() 159 | l.AddLang("en", "English") 160 | 161 | err := l.LoadFile("en", "testdata/en.ini") 162 | is.Nil(err) 163 | 164 | // invalid file path 165 | err = l.LoadFile("en", "not-exiis.ini") 166 | is.Err(err) 167 | 168 | // invalid data string 169 | err = l.LoadString("en", "invalid string") 170 | is.Err(err) 171 | 172 | is.Eq("Blog", l.Tr("en", "name")) 173 | is.Eq("name", l.DefTr("name")) 174 | 175 | // not exist lang 176 | err = l.LoadFile("zh-CN", "testdata/zh-CN.ini") 177 | is.Err(err) 178 | err = l.LoadString("zh-CN", "name = 博客") 179 | is.Err(err) 180 | l.NewLang("zh-CN", "简体中文") 181 | err = l.LoadString("zh-CN", "name = 博客") 182 | is.Nil(err) 183 | 184 | // set default lang 185 | l.Config(func(l *I18n) { 186 | l.DefaultLang = "en" 187 | }) 188 | is.Eq("Blog", l.Dtr("name")) 189 | } 190 | 191 | func TestI18n_Export(t *testing.T) { 192 | is := assert.New(t) 193 | 194 | m := NewEmpty() 195 | m.Add("en", "English") 196 | // repeat 197 | m.NewLang("en", "English") 198 | 199 | err := m.LoadString("en", "name = Blog") 200 | is.Nil(err) 201 | 202 | is.Contains(m.Export("en"), "name = Blog") 203 | is.Eq("", m.Export("no-lang")) 204 | } 205 | 206 | func TestI18n_SetValues(t *testing.T) { 207 | is := assert.New(t) 208 | l := NewEmpty().WithLang("en", "English") 209 | 210 | err := l.SetValues("notExist", "", nil) 211 | is.Err(err) 212 | 213 | err = l.SetValues("en", "", nil) 214 | is.NoErr(err) 215 | 216 | err = l.SetValues("en", "site", ini.Section{ 217 | "name": "my site", 218 | }) 219 | is.NoErr(err) 220 | is.Eq("my site", l.Tr("en", "site.name")) 221 | } 222 | 223 | func TestI18n_LoadString(t *testing.T) { 224 | is := assert.New(t) 225 | 226 | m := NewEmpty() 227 | m.NewLang("en", "English") 228 | is.True(m.HasLang("en")) 229 | 230 | err := m.LoadString("en", "name = Blog") 231 | is.Nil(err) 232 | is.Eq("Blog", m.Tr("en", "name")) 233 | 234 | err = m.LoadString("en", "name = Blog") 235 | is.Nil(err) 236 | is.Eq("Blog", m.Tr("en", "name")) 237 | } 238 | -------------------------------------------------------------------------------- /i18n.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package i18n is a simple language manager, use INI format file. 3 | 4 | Source code and other details for the project are available at GitHub: 5 | 6 | https://github.com/gookit/i18n 7 | 8 | Language files: 9 | 10 | // structs on mode is FileMode(default) 11 | lang/ 12 | en.ini 13 | zh-CN.ini 14 | 15 | // structs on mode is DirMode 16 | lang/ 17 | en/ 18 | default.ini 19 | other.ini 20 | zh-CN/ 21 | default.ini 22 | other.ini 23 | 24 | Load: 25 | 26 | defaultLang = "en" 27 | languages := map[string]string{ 28 | "en": "English", 29 | "zh-CN": "简体中文", 30 | "zh-TW": "繁体中文", 31 | } 32 | 33 | i18n.Init("conf/lang", defaultLang, languages) 34 | 35 | Usage: 36 | 37 | // translate from default language 38 | val := i18n.Dtr("key") 39 | 40 | // translate from special language 41 | val := i18n.Tr("en", "key") 42 | */ 43 | package i18n 44 | 45 | import ( 46 | "bytes" 47 | "errors" 48 | "fmt" 49 | "io/ioutil" 50 | "os" 51 | "strings" 52 | 53 | "github.com/gookit/goutil/errorx" 54 | "github.com/gookit/goutil/strutil" 55 | "github.com/gookit/ini/v2" 56 | ) 57 | 58 | const ( 59 | // ---- I18n.LoadMode language file load mode 60 | 61 | // FileMode language name is file name. "en" -> "lang/en.ini" 62 | FileMode uint8 = 0 63 | // DirMode language name is dir name, will load all file in the dir. "en" -> "lang/en/*.ini" 64 | DirMode uint8 = 1 65 | 66 | // ---- I18n.TransMode translate message mode. 67 | 68 | // SprintfMode render message arguments by fmt.Sprintf 69 | SprintfMode uint8 = 0 70 | // ReplaceMode render message arguments by string replace 71 | ReplaceMode uint8 = 1 72 | ) 73 | 74 | // I18n language manager 75 | type I18n struct { 76 | // languages data 77 | data map[string]*ini.Ini 78 | 79 | // language files directory 80 | langDir string 81 | // language list {en:English, zh-CN:简体中文} 82 | languages map[string]string 83 | // loaded lang files 84 | // loadedFiles []string 85 | 86 | // ------------ config for i18n ------------ 87 | 88 | // LoadMode mode for the load language files. 89 | // 0 single language file 90 | // 1 multi-language directory 91 | LoadMode uint8 92 | // TransMode translate mode. 93 | // 0 sprintf 94 | // 1 replace 95 | TransMode uint8 96 | // DefaultLang default language name. eg. "en" 97 | DefaultLang string 98 | // FallbackLang spare(fallback) language name. eg. "en" 99 | FallbackLang string 100 | } 101 | 102 | /************************************************************ 103 | * create instance 104 | ************************************************************/ 105 | 106 | // New an i18n instance 107 | func New(langDir, defLang string, languages map[string]string) *I18n { 108 | return &I18n{ 109 | data: make(map[string]*ini.Ini, 0), 110 | // language data config 111 | langDir: langDir, 112 | languages: languages, 113 | // set default lang 114 | DefaultLang: defLang, 115 | } 116 | } 117 | 118 | // NewEmpty new an empty i18n instance 119 | func NewEmpty() *I18n { 120 | return &I18n{ 121 | data: make(map[string]*ini.Ini, 0), 122 | // init languages 123 | languages: make(map[string]string, 0), 124 | } 125 | } 126 | 127 | // NewWithInit an i18n instance and call init 128 | func NewWithInit(langDir, defLang string, languages map[string]string) *I18n { 129 | return New(langDir, defLang, languages).Init() 130 | } 131 | 132 | // Config the manager instance 133 | func (l *I18n) Config(fn func(l *I18n)) { 134 | fn(l) 135 | } 136 | 137 | // Init load add language files 138 | func (l *I18n) Init() *I18n { 139 | if l.LoadMode == FileMode { 140 | l.loadSingleFiles() 141 | } else if l.LoadMode == DirMode { 142 | l.loadDirFiles() 143 | } else { 144 | panic("invalid load mode setting. only allow 0, 1") 145 | } 146 | 147 | return l 148 | } 149 | 150 | /************************************************************ 151 | * translate message 152 | ************************************************************/ 153 | 154 | // Dt translate from default lang 155 | func (l *I18n) Dt(key string, args ...interface{}) string { 156 | return l.Tr(l.DefaultLang, key, args...) 157 | } 158 | 159 | // Dtr translate from default lang 160 | func (l *I18n) Dtr(key string, args ...interface{}) string { 161 | return l.Tr(l.DefaultLang, key, args...) 162 | } 163 | 164 | // DefTr translate from default lang 165 | func (l *I18n) DefTr(key string, args ...interface{}) string { 166 | return l.Tr(l.DefaultLang, key, args...) 167 | } 168 | 169 | // T translate from a lang by key 170 | func (l *I18n) T(lang, key string, args ...interface{}) string { 171 | return l.Tr(lang, key, args...) 172 | } 173 | 174 | // Tr translate from a lang by key 175 | // 176 | // Config: 177 | // 178 | // [site] 179 | // name = my blog 180 | // 181 | // Read: 182 | // 183 | // site.name => "my blog" 184 | func (l *I18n) Tr(lang, key string, args ...interface{}) string { 185 | if !l.HasLang(lang) { 186 | // find from fallback lang 187 | msg := l.transFromFallback(key) 188 | if msg == "" { 189 | return key 190 | } 191 | 192 | // has args for the message 193 | if len(args) > 0 { 194 | msg = l.renderMessage(msg, args...) 195 | } 196 | return msg 197 | } 198 | 199 | // find message by key 200 | msg, ok := l.data[lang].GetValue(key) 201 | if !ok { 202 | // key not exists, find from fallback lang 203 | msg = l.transFromFallback(key) 204 | } 205 | 206 | // message is empty 207 | if msg == "" { 208 | return key 209 | } 210 | 211 | // has args for the message 212 | if len(args) > 0 { 213 | msg = l.renderMessage(msg, args...) 214 | } 215 | return msg 216 | } 217 | 218 | // HasKey in the language data 219 | func (l *I18n) HasKey(lang, key string) (ok bool) { 220 | if !l.HasLang(lang) { 221 | return 222 | } 223 | 224 | _, ok = l.data[lang].GetValue(key) 225 | return 226 | } 227 | 228 | // translate from fallback language 229 | func (l *I18n) transFromFallback(key string) string { 230 | fl := l.FallbackLang 231 | if !l.HasLang(fl) { 232 | return "" 233 | } 234 | 235 | return l.data[fl].String(key) 236 | } 237 | 238 | const errMsg = "CANNOT-TO-STRING" 239 | 240 | func (l *I18n) renderMessage(msg string, args ...interface{}) string { 241 | if l.TransMode == SprintfMode { 242 | return fmt.Sprintf(msg, args...) 243 | } 244 | 245 | // if args[0] is []string 246 | if ss, ok := args[0].([]string); ok { 247 | return strings.NewReplacer(ss...).Replace(msg) 248 | } 249 | 250 | var ss []string 251 | 252 | // if args is map[string]interface{} 253 | if mp, ok := args[0].(map[string]interface{}); ok { 254 | for k, v := range mp { 255 | str, err := strutil.ToString(v) 256 | if err != nil { 257 | str = errMsg 258 | } 259 | 260 | ss = append(ss, "{"+k+"}") 261 | ss = append(ss, str) 262 | } 263 | } else { 264 | // if args is: {field1, value1, field2, value2, ...}, try convert all element to string. 265 | for i, val := range args { 266 | str, err := strutil.ToString(val) 267 | if err != nil { 268 | str = errMsg 269 | } 270 | 271 | if i%2 == 0 { 272 | str = "{" + str + "}" 273 | } 274 | ss = append(ss, str) 275 | } 276 | } 277 | 278 | return strings.NewReplacer(ss...).Replace(msg) 279 | } 280 | 281 | /************************************************************ 282 | * data manage 283 | ************************************************************/ 284 | 285 | // load language files when LoadMode is 0 286 | func (l *I18n) loadSingleFiles() { 287 | pathSep := string(os.PathSeparator) 288 | 289 | for lang := range l.languages { 290 | lData := ini.New() 291 | err := lData.LoadFiles(l.langDir + pathSep + lang + ".ini") 292 | if err != nil { 293 | panic("fail to load language: " + lang + ", error " + err.Error()) 294 | } 295 | 296 | l.data[lang] = lData 297 | } 298 | } 299 | 300 | // load language files when LoadMode is 1 301 | func (l *I18n) loadDirFiles() { 302 | pathSep := string(os.PathSeparator) 303 | 304 | for lang := range l.languages { 305 | dirPath := l.langDir + pathSep + lang 306 | files, err := ioutil.ReadDir(dirPath) 307 | if err != nil { 308 | panic("read dir fail: " + dirPath + ", error " + err.Error()) 309 | } 310 | 311 | sl := l.data[lang] 312 | 313 | for _, fi := range files { 314 | // skip dir and filter the specified format 315 | if fi.IsDir() || !strings.HasSuffix(fi.Name(), ".ini") { 316 | continue 317 | } 318 | 319 | var err error 320 | if sl != nil { 321 | err = sl.LoadFiles(dirPath + pathSep + fi.Name()) 322 | } else { // create new language instance 323 | sl = ini.New() 324 | err = sl.LoadFiles(dirPath + pathSep + fi.Name()) 325 | l.data[lang] = sl 326 | } 327 | 328 | if err != nil { 329 | panic("fail to load language file: " + lang + ", error " + err.Error()) 330 | } 331 | } 332 | } 333 | } 334 | 335 | // Add register and init new language. alias of NewLang() 336 | func (l *I18n) Add(lang string, name string) { 337 | l.NewLang(lang, name) 338 | } 339 | 340 | // AddLang register and init new language. alias of NewLang() 341 | func (l *I18n) AddLang(lang string, name string) { 342 | l.NewLang(lang, name) 343 | } 344 | 345 | // WithLang register and init new language. alias of NewLang() 346 | func (l *I18n) WithLang(lang string, name string) *I18n { 347 | l.NewLang(lang, name) 348 | return l 349 | } 350 | 351 | // NewLang create/add a new language 352 | // 353 | // Usage: 354 | // 355 | // i18n.NewLang("zh-CN", "简体中文") 356 | func (l *I18n) NewLang(lang string, name string) { 357 | // language exist 358 | if _, ok := l.languages[lang]; ok { 359 | return 360 | } 361 | 362 | if name == "" { 363 | name = strutil.UpperFirst(lang) 364 | } 365 | l.data[lang] = ini.New() 366 | l.languages[lang] = name 367 | } 368 | 369 | // LoadFile append data to a exist language 370 | // 371 | // Usage: 372 | // 373 | // i18n.LoadFile("zh-CN", "path/to/zh-CN.ini") 374 | func (l *I18n) LoadFile(lang string, file string) (err error) { 375 | // append data 376 | if ld, ok := l.data[lang]; ok { 377 | err = ld.LoadFiles(file) 378 | if err != nil { 379 | return 380 | } 381 | } else { 382 | err = errors.New("language" + lang + " not exist, please create it before load data") 383 | } 384 | 385 | return 386 | } 387 | 388 | // LoadString load language data form a string 389 | // 390 | // Usage: 391 | // 392 | // i18n.LoadString("zh-CN", ` 393 | // name = blog 394 | // age = 233 395 | // `) 396 | func (l *I18n) LoadString(lang string, data string) (err error) { 397 | ld, ok := l.data[lang] 398 | if !ok { 399 | return errorx.Rawf("language '%s' is not registered", lang) 400 | } 401 | 402 | // append data 403 | return ld.LoadStrings(data) 404 | } 405 | 406 | // SetValues to the special language data instance 407 | func (l *I18n) SetValues(lang, group string, values map[string]string) error { 408 | ld, ok := l.data[lang] 409 | if !ok { 410 | return errorx.Rawf("language '%s' is not registered", lang) 411 | } 412 | 413 | if len(values) == 0 { 414 | return nil 415 | } 416 | 417 | // group name is empty, set to default section 418 | if group == "" { 419 | group = ld.DefSection() 420 | } 421 | 422 | return ld.LoadData(map[string]ini.Section{ 423 | group: values, 424 | }) 425 | } 426 | 427 | // Lang get language data instance 428 | func (l *I18n) Lang(lang string) *ini.Ini { 429 | if _, ok := l.languages[lang]; ok { 430 | return l.data[lang] 431 | } 432 | return nil 433 | } 434 | 435 | // Export a language data as INI string 436 | func (l *I18n) Export(lang string) string { 437 | if _, ok := l.languages[lang]; !ok { 438 | return "" 439 | } 440 | 441 | var buf bytes.Buffer 442 | 443 | _, err := l.data[lang].WriteTo(&buf) 444 | if err != nil { 445 | panic(err) 446 | } 447 | 448 | return buf.String() 449 | } 450 | 451 | // HasLang in the manager 452 | func (l *I18n) HasLang(lang string) bool { 453 | _, ok := l.languages[lang] 454 | return ok 455 | } 456 | 457 | // DelLang from the i18n manager 458 | func (l *I18n) DelLang(lang string) bool { 459 | _, ok := l.languages[lang] 460 | if ok { 461 | delete(l.data, lang) 462 | delete(l.languages, lang) 463 | } 464 | 465 | return ok 466 | } 467 | 468 | // Languages get all languages 469 | func (l *I18n) Languages() map[string]string { 470 | return l.languages 471 | } 472 | --------------------------------------------------------------------------------