├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── test.yaml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── README.md ├── benchmark_test.go ├── examples └── getlocale │ └── main.go ├── go.mod ├── go.sum ├── locale_android.c ├── locale_android.go ├── locale_darwin.go ├── locale_darwin.m ├── locale_darwin_cgo.go ├── locale_darwin_nocgo.go ├── locale_ios.go ├── locale_ios.m ├── locale_js.go ├── locale_test.go ├── locale_unix.go ├── locale_unix_test.go ├── locale_windows.go ├── util.go └── util_test.go /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.137.0/containers/go/.devcontainer/base.Dockerfile 2 | ARG VARIANT="1" 3 | FROM mcr.microsoft.com/vscode/devcontainers/go:0-${VARIANT} 4 | 5 | SHELL ["/bin/bash", "-o", "pipefail", "-c"] 6 | 7 | RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b "$(go env GOPATH)/bin" v1.43.0 8 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/master/containers/go 3 | { 4 | "name": "Go", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | "args": { 8 | // Update the VARIANT arg to pick a version of Go: 1, 1.15, 1.14 9 | "VARIANT": "1" 10 | } 11 | }, 12 | "runArgs": [ 13 | "--cap-add=SYS_PTRACE", 14 | "--security-opt", 15 | "seccomp=unconfined" 16 | ], 17 | // Set *default* container specific settings.json values on container create. 18 | "settings": { 19 | "terminal.integrated.defaultProfile.linux": "bash", 20 | "go.useGoProxyToCheckForUpdates": true, 21 | "go.gopath": "/go", 22 | "go.useLanguageServer": true 23 | }, 24 | // Add the IDs of extensions you want installed when the container is created. 25 | "extensions": [ 26 | "golang.Go" 27 | ], 28 | // Connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root. 29 | "remoteUser": "vscode" 30 | } 31 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths: 7 | - "**.go" 8 | - "**.c" 9 | - "**.m" 10 | - "**.yml" 11 | pull_request: 12 | branches: 13 | - master 14 | paths: 15 | - "**.go" 16 | - "**.c" 17 | - "**.m" 18 | - "**.yml" 19 | jobs: 20 | test: 21 | strategy: 22 | matrix: 23 | os: [ubuntu-latest, macos-latest, windows-latest] 24 | go: 25 | - ~1.18 26 | - ~1.19 27 | - ~1.20 28 | - ~1.21 29 | - ~1.22 30 | - ~1.23 31 | runs-on: ${{ matrix.os }} 32 | steps: 33 | - name: Checkout the code 34 | uses: actions/checkout@v4 35 | - name: Set up Go 36 | uses: actions/setup-go@v5 37 | with: 38 | go-version: ${{ matrix.go }} 39 | - name: Cache the Go modules 40 | id: cache 41 | uses: actions/cache@v4 42 | with: 43 | path: ~/go/pkg/mod 44 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 45 | restore-keys: | 46 | ${{ runner.os }}-go- 47 | - name: Download the Go modules 48 | if: steps.cache.outputs.cache-hit != 'true' 49 | run: go mod download 50 | - name: Build 51 | run: go build -ldflags="-s -w" ./examples/getlocale 52 | - name: Build (WASM) 53 | if: startsWith(matrix.os, 'ubuntu') 54 | # Try to build the WASM executable once (on Ubuntu) 55 | run: GOOS=js GOARCH=wasm go build -ldflags="-s -w" -o getlocale.wasm ./examples/getlocale 56 | - name: Set locale (macOS) 57 | if: startsWith(matrix.os, 'macos') 58 | # Manually set the AppleLanguages on macOS 59 | # Otherwise, we get the following error when running GetLocales(): 60 | # The domain/default pair of (kCFPreferencesAnyApplication, AppleLanguages) does not exist 61 | run: defaults write NSGlobalDomain AppleLanguages "(en, fr-FR, ja-JP)" 62 | - name: Test 63 | run: go test -v -race -covermode="atomic" -coverprofile="profile.cov" 64 | - name: Test without CGO 65 | # no -race because it requires cgo 66 | run: go test -v 67 | env: 68 | CGO_ENABLED: 0 69 | - name: Send coverage 70 | uses: shogo82148/actions-goveralls@v1 71 | with: 72 | path-to-profile: profile.cov 73 | parallel: true 74 | finish: 75 | needs: test 76 | runs-on: ubuntu-latest 77 | steps: 78 | - name: Send post build webhook to Coveralls 79 | uses: shogo82148/actions-goveralls@v1 80 | with: 81 | parallel-finished: true 82 | lint: 83 | strategy: 84 | matrix: 85 | os: [ubuntu-latest, macos-latest, windows-latest] 86 | runs-on: ${{ matrix.os }} 87 | env: 88 | GOLANGCI_LINT_VERSION: 1.62.2 89 | steps: 90 | - name: Checkout the code 91 | uses: actions/checkout@v4 92 | - name: Set up Go 93 | uses: actions/setup-go@v5 94 | with: 95 | go-version: ~1.23 96 | - name: Download golangci-lint 97 | if: startsWith(matrix.os, 'windows') != true 98 | run: | 99 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b ~/bin "v${GOLANGCI_LINT_VERSION}" 100 | - name: Download golangci-lint 101 | if: startsWith(matrix.os, 'windows') 102 | run: | 103 | Invoke-WebRequest "https://github.com/golangci/golangci-lint/releases/download/v${env:GOLANGCI_LINT_VERSION}/golangci-lint-${env:GOLANGCI_LINT_VERSION}-windows-amd64.zip" -OutFile "golangci-lint.zip" 104 | Expand-Archive "golangci-lint.zip" -DestinationPath "." 105 | New-Item -ItemType Directory -Path "~\bin" 106 | Move-Item -Path "golangci-lint-${env:GOLANGCI_LINT_VERSION}-windows-amd64\golangci-lint.exe" -Destination "~\bin\" 107 | - name: Run golangci-lint 108 | run: ~/bin/golangci-lint run --out-format github-actions . 109 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | examples/getlocale/getlocale 2 | examples/getlocale/getlocale.exe 3 | 4 | ### Go ### 5 | # Binaries for programs and plugins 6 | *.exe 7 | *.exe~ 8 | *.dll 9 | *.so 10 | *.dylib 11 | 12 | # Test binary, built with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool 16 | *.out 17 | 18 | # Fyne Cross build folder 19 | fyne-cross/ 20 | 21 | ### Linux ### 22 | *~ 23 | 24 | ### Vim ### 25 | # Swap 26 | [._]*.s[a-v][a-z] 27 | [._]*.sw[a-p] 28 | [._]s[a-rt-v][a-z] 29 | [._]ss[a-gi-z] 30 | [._]sw[a-p] 31 | 32 | # Session 33 | Session.vim 34 | 35 | # Auto-generated tag files 36 | tags 37 | 38 | # Persistent undo 39 | [._]*.un~ 40 | 41 | ### VisualStudioCode ### 42 | .vscode/* 43 | !.vscode/settings.json 44 | !.vscode/tasks.json 45 | !.vscode/launch.json 46 | !.vscode/extensions.json 47 | .history 48 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 5m 3 | linters: 4 | disable-all: true 5 | enable: 6 | # Default 7 | - errcheck 8 | - gosimple 9 | - govet 10 | - ineffassign 11 | - staticcheck 12 | - typecheck 13 | - unused 14 | # Manually enabled 15 | - dupl 16 | - gocyclo 17 | - gofmt 18 | - revive 19 | - goprintffuncname 20 | - lll 21 | - misspell 22 | - nakedret 23 | - prealloc 24 | - unconvert 25 | - whitespace 26 | linter-settings: 27 | gocyclo: 28 | # Minimal code complexity to report, 30 by default 29 | min-complexity: 15 30 | govet: 31 | # Report about shadowed variables 32 | check-shadowing: true 33 | issues: 34 | exclude-use-default: false 35 | exclude: 36 | # govet: Common false positives 37 | - (possible misuse of unsafe.Pointer|should have signature) 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Alexis Jeandeau 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 | # go-locale 2 | 3 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/jeandeaual/go-locale)](https://pkg.go.dev/github.com/jeandeaual/go-locale) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/jeandeaual/go-locale)](https://goreportcard.com/report/github.com/jeandeaual/go-locale) 5 | [![Coverage Status](https://coveralls.io/repos/github/jeandeaual/go-locale/badge.svg?branch=master)](https://coveralls.io/github/jeandeaual/go-locale?branch=master) 6 | [![test](https://github.com/jeandeaual/go-locale/workflows/test/badge.svg)](https://github.com/jeandeaual/go-locale/actions?query=workflow%3Atest) 7 | 8 | Go library used to retrieve the current locale(s) of the operating system. 9 | 10 | ## OS Support 11 | 12 | * Windows\ 13 | Using [`GetUserDefaultLocaleName`](https://docs.microsoft.com/en-us/windows/win32/api/winnls/nf-winnls-getuserdefaultlocalename) and [`GetSystemDefaultLocaleName`](https://docs.microsoft.com/en-us/windows/win32/api/winnls/nf-winnls-getsystemdefaultlocalename). 14 | * macOS\ 15 | Using [`[Bundle preferredLocalizations]`](https://developer.apple.com/documentation/foundation/bundle/1417249-preferredlocalizations), falling back to 16 | calling `defaults read -g AppleLocale` and `defaults read -g AppleLanguages` (since environment variables like `LANG` are not usually set on macOS). 17 | * Unix-like systems (Linux, BSD, etc.)\ 18 | Using the `LANGUAGE`, `LC_ALL`, `LC_MESSAGES` and `LANG` environment variables. 19 | * WASM (JavaScript)\ 20 | Using [`navigator.language`](https://developer.mozilla.org/en-US/docs/Web/API/NavigatorLanguage/language) and [`navigator.languages`](https://developer.mozilla.org/en-US/docs/Web/API/NavigatorLanguage/languages). 21 | * iOS\ 22 | Using [`[NSLocale preferredLanguages]`](https://developer.apple.com/documentation/foundation/nslocale/1415614-preferredlanguages). 23 | * Android\ 24 | Using [`getResources().getConfiguration().getLocales`](https://developer.android.com/reference/android/content/res/Configuration#getLocales()) for Android N or later, or [`getResources().getConfiguration().locale`](https://developer.android.com/reference/android/content/res/Configuration#locale) otherwise. 25 | 26 | *Note*: for Android, you'll first need to call `SetRunOnJVM`, depending on which mobile framework you're using: 27 | 28 | * For [Fyne](https://fyne.io/): 29 | 30 | ```go 31 | import ( 32 | "fyne.io/fyne/v2/driver" 33 | "github.com/jeandeaual/go-locale" 34 | ) 35 | 36 | func init() { 37 | locale.SetRunOnJVM(func(fn func(vm, env, ctx uintptr) error) error { 38 | driver.RunNative(func(ctx interface{}) error { 39 | and := ctx.(*driver.AndroidContext) 40 | return fn(and.VM, and.Env, and.Ctx) 41 | }) 42 | return nil 43 | }) 44 | } 45 | ``` 46 | 47 | * For [gomobile](https://github.com/golang/go/wiki/Mobile): 48 | 49 | ```go 50 | import ( 51 | "golang.org/x/mobile/app" 52 | "github.com/jeandeaual/go-locale" 53 | ) 54 | 55 | func init() { 56 | locale.SetRunOnJVM(app.RunOnJVM) 57 | } 58 | ``` 59 | 60 | ## Usage 61 | 62 | ## GetLocales 63 | 64 | `GetLocales` returns the user's preferred locales, by order of preference, as a slice of [IETF BCP 47 language tag](https://tools.ietf.org/rfc/bcp/bcp47.txt) (e.g. `[]string{"en-US", "fr-FR", "ja-JP"}`). 65 | 66 | This works if the user set multiple languages on macOS and other Unix systems. 67 | Otherwise, it returns a slice with a single locale. 68 | 69 | ```go 70 | userLocales, err := locale.GetLocales() 71 | if err == nil { 72 | fmt.Println("Locales:", userLocales) 73 | } 74 | ``` 75 | 76 | This can be used with [golang.org/x/text](https://godoc.org/golang.org/x/text) or [go-i18n](https://github.com/nicksnyder/go-i18n) to set the localizer's language preferences: 77 | 78 | ```go 79 | import ( 80 | "github.com/jeandeaual/go-locale" 81 | "golang.org/x/text/message" 82 | ) 83 | 84 | func main() { 85 | userLocales, _ := locale.GetLocales() 86 | p := message.NewPrinter(message.MatchLanguage(userLocales...)) 87 | ... 88 | } 89 | ``` 90 | 91 | ```go 92 | import ( 93 | "github.com/jeandeaual/go-locale" 94 | "github.com/nicksnyder/go-i18n/v2/i18n" 95 | "golang.org/x/text/language" 96 | ) 97 | 98 | func main() { 99 | userLocales, _ := locale.GetLocales() 100 | bundle := i18n.NewBundle(language.English) 101 | localizer := i18n.NewLocalizer(bundle, userLocales...) 102 | ... 103 | } 104 | ``` 105 | 106 | ## GetLocale 107 | 108 | `GetLocale` returns the current locale as defined in [IETF BCP 47](https://tools.ietf.org/rfc/bcp/bcp47.txt) (e.g. `"en-US"`). 109 | 110 | ```go 111 | userLocale, err := locale.GetLocale() 112 | if err == nil { 113 | fmt.Println("Locale:", userLocale) 114 | } 115 | ``` 116 | 117 | ## GetLanguage 118 | 119 | `GetLanguage` returns the current language as an [ISO 639](http://en.wikipedia.org/wiki/ISO_639) language code (e.g. `"en"`). 120 | 121 | ```go 122 | userLanguage, err := locale.GetLanguage() 123 | if err == nil { 124 | fmt.Println("Language:", userLocale) 125 | } 126 | ``` 127 | 128 | ## GetRegion 129 | 130 | `GetRegion` returns the current language as an [ISO 3166](http://en.wikipedia.org/wiki/ISO_3166-1) country code (e.g. `"US"`). 131 | 132 | ```go 133 | userRegion, err := locale.GetRegion() 134 | if err == nil { 135 | fmt.Println("Region:", userRegion) 136 | } 137 | ``` 138 | 139 | ## Aknowledgements 140 | 141 | Inspired by [jibber_jabber](https://github.com/cloudfoundry-attic/jibber_jabber). 142 | -------------------------------------------------------------------------------- /benchmark_test.go: -------------------------------------------------------------------------------- 1 | // Package locale implements functions to retrieve the current locale(s) 2 | // of the operating system. 3 | package locale 4 | 5 | import ( 6 | "testing" 7 | ) 8 | 9 | var ( 10 | result string 11 | results []string 12 | e error 13 | ) 14 | 15 | func BenchmarkGetLocale(b *testing.B) { 16 | var ( 17 | locale string 18 | err error 19 | ) 20 | 21 | for n := 0; n < b.N; n++ { 22 | locale, err = GetLocale() 23 | } 24 | 25 | result = locale 26 | e = err 27 | } 28 | 29 | func BenchmarkGetLocales(b *testing.B) { 30 | var ( 31 | locales []string 32 | err error 33 | ) 34 | 35 | for n := 0; n < b.N; n++ { 36 | locales, err = GetLocales() 37 | } 38 | 39 | results = locales 40 | e = err 41 | } 42 | 43 | func BenchmarkGetLanguage(b *testing.B) { 44 | var ( 45 | language string 46 | err error 47 | ) 48 | 49 | for n := 0; n < b.N; n++ { 50 | language, err = GetLanguage() 51 | } 52 | 53 | result = language 54 | e = err 55 | } 56 | 57 | func BenchmarkGetRegion(b *testing.B) { 58 | var ( 59 | region string 60 | err error 61 | ) 62 | 63 | for n := 0; n < b.N; n++ { 64 | region, err = GetRegion() 65 | } 66 | 67 | result = region 68 | e = err 69 | } 70 | -------------------------------------------------------------------------------- /examples/getlocale/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/jeandeaual/go-locale" 8 | ) 9 | 10 | func main() { 11 | locales, err := locale.GetLocales() 12 | if err != nil { 13 | fmt.Fprintf(os.Stderr, "Couldn't retrieve the locales: %v\n", err) 14 | } else { 15 | fmt.Println("Locales:", locales) 16 | } 17 | 18 | userLocale, err := locale.GetLocale() 19 | if err != nil { 20 | fmt.Fprintf(os.Stderr, "Couldn't retrieve the locale: %v\n", err) 21 | } else { 22 | fmt.Println("Locale:", userLocale) 23 | } 24 | 25 | language, err := locale.GetLanguage() 26 | if err != nil { 27 | fmt.Fprintf(os.Stderr, "Couldn't retrieve the language: %v\n", err) 28 | } else { 29 | fmt.Println("Language:", language) 30 | } 31 | 32 | region, err := locale.GetRegion() 33 | if err != nil { 34 | fmt.Fprintf(os.Stderr, "Couldn't retrieve the region: %v\n", err) 35 | } else { 36 | fmt.Println("Region:", region) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jeandeaual/go-locale 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/stretchr/testify v1.10.0 7 | golang.org/x/sys v0.27.0 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/pmezard/go-difflib v1.0.0 // indirect 13 | gopkg.in/yaml.v3 v3.0.1 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 6 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 7 | golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= 8 | golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 11 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 12 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 13 | -------------------------------------------------------------------------------- /locale_android.c: -------------------------------------------------------------------------------- 1 | //go:build android 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define LOG_FATAL(...) __android_log_print(ANDROID_LOG_FATAL, "GoLog", __VA_ARGS__) 9 | 10 | static const char *jstringToCharCopy(JNIEnv *env, const jstring string) 11 | { 12 | const char *chars = (*env)->GetStringUTFChars(env, string, NULL); 13 | const char *copy = strdup(chars); 14 | (*env)->ReleaseStringUTFChars(env, string, chars); 15 | 16 | return copy; 17 | } 18 | 19 | static jclass findClass(JNIEnv *env, const char *class_name) 20 | { 21 | jclass clazz = (*env)->FindClass(env, class_name); 22 | 23 | if (clazz == NULL) 24 | { 25 | (*env)->ExceptionClear(env); 26 | LOG_FATAL("cannot find %s", class_name); 27 | return NULL; 28 | } 29 | 30 | return clazz; 31 | } 32 | 33 | static jmethodID findMethod(JNIEnv *env, jclass clazz, const char *name, const char *sig) 34 | { 35 | jmethodID m = (*env)->GetMethodID(env, clazz, name, sig); 36 | 37 | if (m == 0) 38 | { 39 | (*env)->ExceptionClear(env); 40 | LOG_FATAL("cannot find method %s %s", name, sig); 41 | return 0; 42 | } 43 | 44 | return m; 45 | } 46 | 47 | static jfieldID findField(JNIEnv *env, jclass clazz, const char *name, const char *sig) 48 | { 49 | jfieldID f = (*env)->GetFieldID(env, clazz, name, sig); 50 | 51 | if (f == 0) 52 | { 53 | (*env)->ExceptionClear(env); 54 | LOG_FATAL("cannot find method %s %s", name, sig); 55 | return 0; 56 | } 57 | 58 | return f; 59 | } 60 | 61 | static jfieldID getStaticFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig) 62 | { 63 | jfieldID f = (*env)->GetStaticFieldID(env, clazz, name, sig); 64 | 65 | if (f == 0) 66 | { 67 | (*env)->ExceptionClear(env); 68 | LOG_FATAL("cannot find static field %s %s", name, sig); 69 | return 0; 70 | } 71 | 72 | return f; 73 | } 74 | 75 | static const char *toLanguageTag(JNIEnv *env, jobject locale) 76 | { 77 | const jclass java_util_Locale = findClass(env, "java/util/Locale"); 78 | 79 | const jstring localeStr = 80 | (*env)->CallObjectMethod( 81 | env, 82 | locale, 83 | (*env)->GetMethodID(env, java_util_Locale, "toLanguageTag", "()Ljava/lang/String;")); 84 | 85 | return jstringToCharCopy(env, localeStr); 86 | } 87 | 88 | static const char *toLanguageTags(JNIEnv *env, jobject locales, jclass android_os_LocaleList) 89 | { 90 | const jstring localeStr = 91 | (*env)->CallObjectMethod( 92 | env, 93 | locales, 94 | (*env)->GetMethodID(env, android_os_LocaleList, "toLanguageTags", "()Ljava/lang/String;")); 95 | 96 | return jstringToCharCopy(env, localeStr); 97 | } 98 | 99 | static int getAPIVersion(JNIEnv *env) 100 | { 101 | // VERSION is a nested class within android.os.Build (hence "$" rather than "/") 102 | const jclass versionClass = findClass(env, "android/os/Build$VERSION"); 103 | const jfieldID sdkIntFieldID = getStaticFieldID(env, versionClass, "SDK_INT", "I"); 104 | 105 | int sdkInt = (*env)->GetStaticIntField(env, versionClass, sdkIntFieldID); 106 | 107 | return sdkInt; 108 | } 109 | 110 | static const jobject getConfiguration(JNIEnv *env, jobject context) 111 | { 112 | const jclass android_content_ContextWrapper = findClass(env, "android/content/ContextWrapper"); 113 | const jclass android_content_res_Resources = findClass(env, "android/content/res/Resources"); 114 | 115 | const jobject resources = 116 | (*env)->CallObjectMethod( 117 | env, 118 | context, 119 | findMethod(env, android_content_ContextWrapper, "getResources", "()Landroid/content/res/Resources;")); 120 | const jobject configuration = 121 | (*env)->CallObjectMethod( 122 | env, 123 | resources, 124 | findMethod(env, android_content_res_Resources, "getConfiguration", "()Landroid/content/res/Configuration;")); 125 | 126 | return configuration; 127 | } 128 | 129 | static const jobject getLocaleObject(JNIEnv *env, jobject context) 130 | { 131 | const jobject configuration = getConfiguration(env, context); 132 | const jclass android_content_res_Configuration = findClass(env, "android/content/res/Configuration"); 133 | 134 | int version = getAPIVersion(env); 135 | 136 | // Android N or later 137 | // See https://developer.android.com/reference/android/content/res/Configuration#locale 138 | if (version >= 24) { 139 | const jclass android_os_LocaleList = findClass(env, "android/os/LocaleList"); 140 | 141 | const jobject locales = 142 | (*env)->CallObjectMethod( 143 | env, 144 | configuration, 145 | findMethod(env, android_content_res_Configuration, "getLocales", "()Landroid/os/LocaleList;")); 146 | 147 | return (*env)->CallObjectMethod( 148 | env, 149 | locales, 150 | findMethod(env, android_os_LocaleList, "get", "(I)Ljava/util/Locale;"), 151 | 0); 152 | } else { 153 | return (*env)->GetObjectField( 154 | env, 155 | configuration, 156 | findField(env, android_content_res_Configuration, "locale", "Ljava/util/Locale;")); 157 | } 158 | } 159 | 160 | // Basically the same as `getResources().getConfiguration().getLocales()` for Android N and later, 161 | // or `getResources().getConfiguration().locale` for earlier Android version. 162 | const char *getLocales(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx) 163 | { 164 | JavaVM *vm = (JavaVM *)java_vm; 165 | JNIEnv *env = (JNIEnv *)jni_env; 166 | jobject context = (jobject)ctx; 167 | 168 | const jobject configuration = getConfiguration(env, context); 169 | const jclass android_content_res_Configuration = findClass(env, "android/content/res/Configuration"); 170 | 171 | int version = getAPIVersion(env); 172 | 173 | // Android N or later 174 | // See https://developer.android.com/reference/android/content/res/Configuration#locale 175 | if (version >= 24) { 176 | const jclass android_os_LocaleList = findClass(env, "android/os/LocaleList"); 177 | 178 | const jobject locales = 179 | (*env)->CallObjectMethod( 180 | env, 181 | configuration, 182 | findMethod(env, android_content_res_Configuration, "getLocales", "()Landroid/os/LocaleList;")); 183 | 184 | return toLanguageTags(env, locales, android_os_LocaleList); 185 | } else { 186 | const jobject locale = 187 | (*env)->GetObjectField( 188 | env, 189 | configuration, 190 | findField(env, android_content_res_Configuration, "locale", "Ljava/util/Locale;")); 191 | 192 | return toLanguageTag(env, locale); 193 | } 194 | } 195 | 196 | // Basically the same as `getResources().getConfiguration().getLocales().get(0).toString()` for Android N and later, 197 | // or `getResources().getConfiguration().locale` for earlier Android version. 198 | const char *getLocale(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx) 199 | { 200 | JavaVM *vm = (JavaVM *)java_vm; 201 | JNIEnv *env = (JNIEnv *)jni_env; 202 | jobject context = (jobject)ctx; 203 | 204 | const jobject locale = getLocaleObject(env, context); 205 | 206 | return toLanguageTag(env, locale); 207 | } 208 | 209 | const char *getLanguage(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx) 210 | { 211 | JavaVM *vm = (JavaVM *)java_vm; 212 | JNIEnv *env = (JNIEnv *)jni_env; 213 | jobject context = (jobject)ctx; 214 | 215 | const jobject locale = getLocaleObject(env, context); 216 | const jclass java_util_Locale = findClass(env, "java/util/Locale"); 217 | 218 | const jstring language = 219 | (*env)->CallObjectMethod( 220 | env, 221 | locale, 222 | (*env)->GetMethodID(env, java_util_Locale, "getLanguage", "()Ljava/lang/String;")); 223 | 224 | return jstringToCharCopy(env, language); 225 | } 226 | 227 | const char *getRegion(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx) 228 | { 229 | JavaVM *vm = (JavaVM *)java_vm; 230 | JNIEnv *env = (JNIEnv *)jni_env; 231 | jobject context = (jobject)ctx; 232 | 233 | const jobject locale = getLocaleObject(env, context); 234 | const jclass java_util_Locale = findClass(env, "java/util/Locale"); 235 | 236 | const jstring country = 237 | (*env)->CallObjectMethod( 238 | env, 239 | locale, 240 | (*env)->GetMethodID(env, java_util_Locale, "getCountry", "()Ljava/lang/String;")); 241 | 242 | return jstringToCharCopy(env, country); 243 | } 244 | -------------------------------------------------------------------------------- /locale_android.go: -------------------------------------------------------------------------------- 1 | //go:build android 2 | 3 | package locale 4 | 5 | /* 6 | #cgo LDFLAGS: -landroid -llog 7 | 8 | #include 9 | 10 | const char *getLocales(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx); 11 | const char *getLocale(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx); 12 | const char *getLanguage(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx); 13 | const char *getRegion(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx); 14 | */ 15 | import "C" 16 | import ( 17 | "errors" 18 | "strings" 19 | "unsafe" 20 | ) 21 | 22 | var ( 23 | errRunOnJVMNotSet error = errors.New("you first need to call SetRunOnJVM") 24 | runOnJVM func(fn func(vm, env, ctx uintptr) error) error 25 | ) 26 | 27 | // SetRunOnJVM sets the RunOnJVM function that will be called by this library. 28 | // This can either be "golang.org/x/mobile/app".RunOnJVM or "github.com/fyne-io/mobile/app".RunOnJVM, 29 | // depending on the mobile framework you're using (both can't be imported at the same time). 30 | // 31 | // RunOnJVM runs fn on a new goroutine locked to an OS thread with a JNIEnv. 32 | // 33 | // RunOnJVM blocks until the call to fn is complete. Any Java 34 | // exception or failure to attach to the JVM is returned as an error. 35 | // 36 | // The function fn takes vm, the current JavaVM*, 37 | // env, the current JNIEnv*, and 38 | // ctx, a jobject representing the global android.context.Context. 39 | func SetRunOnJVM(fn func(fn func(vm, env, ctx uintptr) error) error) { 40 | runOnJVM = fn 41 | } 42 | 43 | // GetLocale retrieves the IETF BCP 47 language tag set on the system. 44 | func GetLocale() (string, error) { 45 | if runOnJVM == nil { 46 | return "", errRunOnJVMNotSet 47 | } 48 | 49 | locale := "" 50 | 51 | err := runOnJVM(func(vm, env, ctx uintptr) error { 52 | chars := C.getLocale(C.uintptr_t(vm), C.uintptr_t(env), C.uintptr_t(ctx)) 53 | locale = C.GoString(chars) 54 | C.free(unsafe.Pointer(chars)) 55 | return nil 56 | }) 57 | 58 | return locale, err 59 | } 60 | 61 | // GetLocales retrieves the IETF BCP 47 language tags set on the system. 62 | func GetLocales() ([]string, error) { 63 | if runOnJVM == nil { 64 | return nil, errRunOnJVMNotSet 65 | } 66 | 67 | locales := "" 68 | 69 | err := runOnJVM(func(vm, env, ctx uintptr) error { 70 | chars := C.getLocales(C.uintptr_t(vm), C.uintptr_t(env), C.uintptr_t(ctx)) 71 | locales = C.GoString(chars) 72 | C.free(unsafe.Pointer(chars)) 73 | return nil 74 | }) 75 | 76 | return strings.Split(locales, ","), err 77 | } 78 | 79 | // GetLanguage retrieves the IETF BCP 47 language tag set on the system and 80 | // returns the language part of the tag. 81 | func GetLanguage() (string, error) { 82 | if runOnJVM == nil { 83 | return "", errRunOnJVMNotSet 84 | } 85 | 86 | language := "" 87 | 88 | err := runOnJVM(func(vm, env, ctx uintptr) error { 89 | chars := C.getLanguage(C.uintptr_t(vm), C.uintptr_t(env), C.uintptr_t(ctx)) 90 | language = C.GoString(chars) 91 | C.free(unsafe.Pointer(chars)) 92 | return nil 93 | }) 94 | 95 | return language, err 96 | } 97 | 98 | // GetRegion retrieves the IETF BCP 47 language tag set on the system and 99 | // returns the region part of the tag. 100 | func GetRegion() (string, error) { 101 | if runOnJVM == nil { 102 | return "", errRunOnJVMNotSet 103 | } 104 | 105 | region := "" 106 | 107 | err := runOnJVM(func(vm, env, ctx uintptr) error { 108 | chars := C.getRegion(C.uintptr_t(vm), C.uintptr_t(env), C.uintptr_t(ctx)) 109 | region = C.GoString(chars) 110 | C.free(unsafe.Pointer(chars)) 111 | return nil 112 | }) 113 | 114 | return region, err 115 | } 116 | -------------------------------------------------------------------------------- /locale_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin && !ios 2 | 3 | package locale 4 | 5 | import ( 6 | "fmt" 7 | "os/exec" 8 | "regexp" 9 | "strings" 10 | "syscall" 11 | ) 12 | 13 | // Common code between locale_darwin_cgo.go and locale_darwin_nocgo.go 14 | // GetLocale exported functions are in those files 15 | 16 | func execCommand(cmd string, args ...string) (status int, out string, err error) { 17 | var bytesOut []byte 18 | status = -1 19 | command := exec.Command(cmd, args...) 20 | 21 | // Execute the command and get the standard and error outputs 22 | bytesOut, err = command.CombinedOutput() 23 | out = string(bytesOut) 24 | if err != nil { 25 | return 26 | } 27 | 28 | // Check the status code 29 | if w, ok := command.ProcessState.Sys().(syscall.WaitStatus); ok { 30 | status = w.ExitStatus() 31 | } 32 | 33 | return 34 | } 35 | 36 | // getLocaleCli retrieves the IETF BCP 47 language tag set on the system without using CGO to call OS APIs. 37 | func getLocaleCli() (string, error) { 38 | _, output, err := execCommand("defaults", "read", "-g", "AppleLocale") 39 | if err != nil { 40 | return "", fmt.Errorf("cannot determine locale: %v (output: %s)", err, output) 41 | } 42 | 43 | // defaults read -g AppleLocale can return a string containing additional 44 | // information after the locale, e.g. "en_US@currency=USD" 45 | if idx := strings.Index(output, "@"); idx != -1 { 46 | output = output[:idx] 47 | } 48 | 49 | return strings.TrimRight(strings.Replace(output, "_", "-", 1), "\n"), nil 50 | } 51 | 52 | // appleLanguagesRegex is used to parse the output of "defaults read -g AppleLanguages" 53 | // e.g.: 54 | // (en, "fr-FR", "ja-JP") 55 | var appleLanguagesRegex = regexp.MustCompile(`([a-z]{2}(?:-[A-Z]{2})?)`) 56 | 57 | // getLocalesCli retrieves the IETF BCP 47 language tags set on the system without using CGO to call OS APIs. 58 | func getLocalesCli() ([]string, error) { 59 | _, output, err := execCommand("defaults", "read", "-g", "AppleLanguages") 60 | if err != nil { 61 | return nil, fmt.Errorf("cannot determine locale: %v (output: %s)", err, output) 62 | } 63 | 64 | matches := appleLanguagesRegex.FindAllStringSubmatch(output, -1) 65 | if len(matches) == 0 { 66 | return nil, fmt.Errorf("invalid output from \"defaults read -g AppleLanguages\": %s", output) 67 | } 68 | 69 | locales := make([]string, 0, len(matches)) 70 | 71 | for _, match := range matches { 72 | locales = append(locales, match[1]) 73 | } 74 | 75 | return locales, nil 76 | } 77 | 78 | // GetLanguage retrieves the IETF BCP 47 language tag set on the system and 79 | // returns the language part of the tag. 80 | func GetLanguage() (string, error) { 81 | language := "" 82 | 83 | locale, err := GetLocale() 84 | if err == nil { 85 | language, _ = splitLocale(locale) 86 | } 87 | 88 | return language, err 89 | } 90 | 91 | // GetRegion retrieves the IETF BCP 47 language tag set on the system and 92 | // returns the region part of the tag. 93 | func GetRegion() (string, error) { 94 | region := "" 95 | 96 | locale, err := GetLocale() 97 | if err == nil { 98 | _, region = splitLocale(locale) 99 | } 100 | 101 | return region, err 102 | } 103 | -------------------------------------------------------------------------------- /locale_darwin.m: -------------------------------------------------------------------------------- 1 | //go:build darwin && !ios 2 | 3 | #import 4 | 5 | const char *preferredLocalization() 6 | { 7 | NSString *locale = [[[NSBundle mainBundle] preferredLocalizations] firstObject]; 8 | 9 | return [locale UTF8String]; 10 | } 11 | 12 | const char *preferredLocalizations() 13 | { 14 | NSString *locales = [[[NSBundle mainBundle] preferredLocalizations] componentsJoinedByString:@","]; 15 | 16 | return [locales UTF8String]; 17 | } 18 | -------------------------------------------------------------------------------- /locale_darwin_cgo.go: -------------------------------------------------------------------------------- 1 | //go:build darwin && !ios 2 | 3 | package locale 4 | 5 | // Non-CGO implementation is in locale_darwin_nocgo.go 6 | 7 | /* 8 | #cgo CFLAGS: -x objective-c 9 | #cgo LDFLAGS: -framework Foundation 10 | 11 | #include 12 | 13 | const char * preferredLocalization(); 14 | const char * preferredLocalizations(); 15 | */ 16 | import "C" 17 | import ( 18 | "strings" 19 | ) 20 | 21 | // GetLocale retrieves the IETF BCP 47 language tag set on the system. 22 | func GetLocale() (string, error) { 23 | str := C.preferredLocalization() 24 | if output := C.GoString(str); output != "" { 25 | return strings.Replace(output, "_", "-", 1), nil 26 | } 27 | 28 | return getLocaleCli() 29 | } 30 | 31 | // GetLocales retrieves the IETF BCP 47 language tags set on the system. 32 | func GetLocales() ([]string, error) { 33 | str := C.preferredLocalizations() 34 | if output := C.GoString(str); output != "" { 35 | r := []string{} 36 | for _, s := range strings.Split(output, ",") { 37 | r = append(r, strings.Replace(s, "_", "-", 1)) 38 | } 39 | return r, nil 40 | } 41 | 42 | return getLocalesCli() 43 | } 44 | -------------------------------------------------------------------------------- /locale_darwin_nocgo.go: -------------------------------------------------------------------------------- 1 | //go:build darwin && !ios && !cgo 2 | 3 | package locale 4 | 5 | // GetLocale retrieves the IETF BCP 47 language tag set on the system. 6 | func GetLocale() (string, error) { 7 | return getLocaleCli() 8 | } 9 | 10 | // GetLocales retrieves the IETF BCP 47 language tags set on the system. 11 | func GetLocales() ([]string, error) { 12 | return getLocalesCli() 13 | } 14 | -------------------------------------------------------------------------------- /locale_ios.go: -------------------------------------------------------------------------------- 1 | //go:build ios 2 | 3 | package locale 4 | 5 | /* 6 | #cgo CFLAGS: -x objective-c 7 | #cgo LDFLAGS: -framework Foundation -framework UIKit 8 | 9 | const char *getLocale(); 10 | const char *getLocales(); 11 | */ 12 | import "C" 13 | import ( 14 | "strings" 15 | ) 16 | 17 | // GetLocale retrieves the IETF BCP 47 language tag set on the system. 18 | func GetLocale() (string, error) { 19 | return C.GoString(C.getLocale()), nil 20 | } 21 | 22 | // GetLocales retrieves the IETF BCP 47 language tags set on the system. 23 | func GetLocales() ([]string, error) { 24 | return strings.Split(C.GoString(C.getLocales()), ","), nil 25 | } 26 | 27 | // GetLanguage retrieves the IETF BCP 47 language tag set on the system and 28 | // returns the language part of the tag. 29 | func GetLanguage() (string, error) { 30 | language := "" 31 | 32 | locale, err := GetLocale() 33 | if err == nil { 34 | language, _ = splitLocale(locale) 35 | } 36 | 37 | return language, err 38 | } 39 | 40 | // GetRegion retrieves the IETF BCP 47 language tag set on the system and 41 | // returns the region part of the tag. 42 | func GetRegion() (string, error) { 43 | region := "" 44 | 45 | locale, err := GetLocale() 46 | if err == nil { 47 | _, region = splitLocale(locale) 48 | } 49 | 50 | return region, err 51 | } 52 | -------------------------------------------------------------------------------- /locale_ios.m: -------------------------------------------------------------------------------- 1 | //go:build ios 2 | 3 | #import 4 | 5 | const char *getLocale() 6 | { 7 | NSString *locale = [[NSLocale preferredLanguages] firstObject]; 8 | 9 | return [locale UTF8String]; 10 | } 11 | 12 | const char *getLocales() 13 | { 14 | NSString *locales = [[NSLocale preferredLanguages] componentsJoinedByString:@","]; 15 | 16 | return [locales UTF8String]; 17 | } 18 | -------------------------------------------------------------------------------- /locale_js.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | 3 | package locale 4 | 5 | import ( 6 | "errors" 7 | "syscall/js" 8 | ) 9 | 10 | func getNavigatorObject() js.Value { 11 | return js.Global().Get("navigator") 12 | } 13 | 14 | // GetLocale retrieves the IETF BCP 47 language tag set on the system. 15 | func GetLocale() (string, error) { 16 | navigator := getNavigatorObject() 17 | if navigator.IsUndefined() { 18 | return "", errors.New("couldn't get window.navigator") 19 | } 20 | 21 | language := navigator.Get("language") 22 | if language.IsUndefined() { 23 | return "", errors.New("couldn't get window.navigator.language") 24 | } 25 | 26 | return language.String(), nil 27 | } 28 | 29 | // GetLocales retrieves the IETF BCP 47 language tags set on the system. 30 | func GetLocales() ([]string, error) { 31 | navigator := getNavigatorObject() 32 | if navigator.IsUndefined() { 33 | return nil, errors.New("couldn't get window.navigator") 34 | } 35 | 36 | languages := navigator.Get("languages") 37 | if languages.IsUndefined() { 38 | return nil, errors.New("couldn't get window.navigator.languages") 39 | } 40 | 41 | locales := make([]string, 0, languages.Length()) 42 | 43 | // Convert the Javascript object to a string slice 44 | for i := 0; i < languages.Length(); i++ { 45 | locales = append(locales, languages.Index(i).String()) 46 | } 47 | 48 | return locales, nil 49 | } 50 | 51 | // GetLanguage retrieves the IETF BCP 47 language tag set on the system and 52 | // returns the language part of the tag. 53 | func GetLanguage() (string, error) { 54 | language := "" 55 | 56 | locale, err := GetLocale() 57 | if err == nil { 58 | language, _ = splitLocale(locale) 59 | } 60 | 61 | return language, err 62 | } 63 | 64 | // GetRegion retrieves the IETF BCP 47 language tag set on the system and 65 | // returns the region part of the tag. 66 | func GetRegion() (string, error) { 67 | region := "" 68 | 69 | locale, err := GetLocale() 70 | if err == nil { 71 | _, region = splitLocale(locale) 72 | } 73 | 74 | return region, err 75 | } 76 | -------------------------------------------------------------------------------- /locale_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows || (darwin && !ios) 2 | 3 | package locale 4 | 5 | import ( 6 | "regexp" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | var ( 13 | localeRegex = regexp.MustCompile(`^[a-z]{2}(?:-[A-Z]{2})?$`) 14 | languageRegex = regexp.MustCompile(`^[a-z]{2}$`) 15 | regionRegex = regexp.MustCompile(`^$|^[A-Z]{2}$`) 16 | ) 17 | 18 | func TestGetLocale(t *testing.T) { 19 | locale, err := GetLocale() 20 | assert.Equal(t, nil, err, "err should be nil") 21 | assert.True(t, localeRegex.MatchString(locale), "\"%s\" should match %v", locale, localeRegex) 22 | } 23 | 24 | func TestGetLanguage(t *testing.T) { 25 | language, err := GetLanguage() 26 | assert.Equal(t, nil, err, "err should be nil") 27 | assert.True(t, languageRegex.MatchString(language), "\"%s\" should match %v", language, languageRegex) 28 | } 29 | 30 | func TestGetRegion(t *testing.T) { 31 | region, err := GetRegion() 32 | assert.Equal(t, nil, err, "err should be nil") 33 | assert.True(t, regionRegex.MatchString(region), "\"%s\" should match %v", region, regionRegex) 34 | } 35 | 36 | func TestGetLocales(t *testing.T) { 37 | locales, err := GetLocales() 38 | assert.Equal(t, nil, err, "err should be nil") 39 | assert.NotZero(t, locales) 40 | 41 | for _, locale := range locales { 42 | assert.True(t, localeRegex.MatchString(locale), "\"%s\" should match %v", locale, localeRegex) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /locale_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows && !darwin && !js && !android 2 | 3 | package locale 4 | 5 | import ( 6 | "os" 7 | "strings" 8 | ) 9 | 10 | func splitLocales(locales string) []string { 11 | // If the user set different locales, they might be set in $LANGUAGE, 12 | // separated by a colon 13 | return strings.Split(locales, ":") 14 | } 15 | 16 | func getLangFromEnv() string { 17 | locale := "" 18 | 19 | // Check the following environment variables for the language information 20 | // See https://www.gnu.org/software/gettext/manual/html_node/Locale-Environment-Variables.html 21 | for _, env := range [...]string{"LC_ALL", "LC_MESSAGES", "LANG"} { 22 | locale = os.Getenv(env) 23 | if len(locale) > 0 { 24 | break 25 | } 26 | } 27 | 28 | if locale == "C" || locale == "POSIX" { 29 | return locale 30 | } 31 | 32 | // Check LANGUAGE if localization is enabled (not set to "C") 33 | // See https://www.gnu.org/software/gettext/manual/html_node/The-LANGUAGE-variable.html#The-LANGUAGE-variable 34 | languages := os.Getenv("LANGUAGE") 35 | if len(languages) > 0 { 36 | return languages 37 | } 38 | 39 | return locale 40 | } 41 | 42 | func getUnixLocales() []string { 43 | locale := getLangFromEnv() 44 | if locale == "C" || locale == "POSIX" || len(locale) == 0 { 45 | return nil 46 | } 47 | 48 | return splitLocales(locale) 49 | } 50 | 51 | // GetLocale retrieves the IETF BCP 47 language tag set on the system. 52 | func GetLocale() (string, error) { 53 | unixLocales := getUnixLocales() 54 | if unixLocales == nil { 55 | return "", nil 56 | } 57 | 58 | language, region := splitLocale(unixLocales[0]) 59 | locale := language 60 | if len(region) > 0 { 61 | locale = strings.Join([]string{language, region}, "-") 62 | } 63 | 64 | return locale, nil 65 | } 66 | 67 | // GetLocales retrieves the IETF BCP 47 language tags set on the system. 68 | func GetLocales() ([]string, error) { 69 | unixLocales := getUnixLocales() 70 | if unixLocales == nil { 71 | return nil, nil 72 | } 73 | 74 | locales := make([]string, 0, len(unixLocales)) 75 | 76 | for _, unixLocale := range unixLocales { 77 | language, region := splitLocale(unixLocale) 78 | locale := language 79 | if len(region) > 0 { 80 | locale = strings.Join([]string{language, region}, "-") 81 | } 82 | locales = append(locales, locale) 83 | } 84 | 85 | return locales, nil 86 | } 87 | 88 | // GetLanguage retrieves the IETF BCP 47 language tag set on the system and 89 | // returns the language part of the tag. 90 | func GetLanguage() (string, error) { 91 | language := "" 92 | 93 | unixLocales := getUnixLocales() 94 | if unixLocales == nil { 95 | return "", nil 96 | } 97 | 98 | language, _ = splitLocale(unixLocales[0]) 99 | 100 | return language, nil 101 | } 102 | 103 | // GetRegion retrieves the IETF BCP 47 language tag set on the system and 104 | // returns the region part of the tag. 105 | func GetRegion() (string, error) { 106 | region := "" 107 | 108 | unixLocales := getUnixLocales() 109 | if unixLocales == nil { 110 | return "", nil 111 | } 112 | 113 | _, region = splitLocale(unixLocales[0]) 114 | 115 | return region, nil 116 | } 117 | -------------------------------------------------------------------------------- /locale_unix_test.go: -------------------------------------------------------------------------------- 1 | //go:build !windows && !darwin && !js && !android 2 | 3 | package locale 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestMultipleLocales(t *testing.T) { 12 | t.Setenv("LANGUAGE", "en_US:fr:ja") 13 | t.Setenv("LC_ALL", "") 14 | t.Setenv("LC_MESSAGES", "") 15 | t.Setenv("LANG", "en_US.UTF-8") 16 | 17 | locales, err := GetLocales() 18 | assert.Equal(t, nil, err, "err should be nil") 19 | assert.Equal(t, []string{"en-US", "fr", "ja"}, locales) 20 | 21 | locale, err := GetLocale() 22 | assert.Equal(t, nil, err, "err should be nil") 23 | assert.Equal(t, "en-US", locale) 24 | 25 | lang, err := GetLanguage() 26 | assert.Equal(t, nil, err, "err should be nil") 27 | assert.Equal(t, "en", lang) 28 | 29 | region, err := GetRegion() 30 | assert.Equal(t, nil, err, "err should be nil") 31 | assert.Equal(t, "US", region) 32 | 33 | // If "C" is set, we should ignore LANGUAGE 34 | t.Setenv("LC_ALL", "C") 35 | 36 | var nilStringSlice []string 37 | 38 | locales, err = GetLocales() 39 | assert.Equal(t, nil, err, "err should be nil") 40 | assert.Equal(t, nilStringSlice, locales) 41 | 42 | locale, err = GetLocale() 43 | assert.Equal(t, nil, err, "err should be nil") 44 | assert.Equal(t, "", locale) 45 | 46 | lang, err = GetLanguage() 47 | assert.Equal(t, nil, err, "err should be nil") 48 | assert.Equal(t, "", lang) 49 | 50 | region, err = GetRegion() 51 | assert.Equal(t, nil, err, "err should be nil") 52 | assert.Equal(t, "", region) 53 | } 54 | 55 | func TestSingleLocale(t *testing.T) { 56 | t.Setenv("LANGUAGE", "ja_JP") 57 | t.Setenv("LC_ALL", "en_US") 58 | t.Setenv("LC_MESSAGES", "ko_KR") 59 | t.Setenv("LANG", "fr_FR") 60 | 61 | locales, err := GetLocales() 62 | assert.Equal(t, nil, err, "err should be nil") 63 | assert.Equal(t, []string{"ja-JP"}, locales) 64 | 65 | locale, err := GetLocale() 66 | assert.Equal(t, nil, err, "err should be nil") 67 | assert.Equal(t, "ja-JP", locale) 68 | 69 | lang, err := GetLanguage() 70 | assert.Equal(t, nil, err, "err should be nil") 71 | assert.Equal(t, "ja", lang) 72 | 73 | region, err := GetRegion() 74 | assert.Equal(t, nil, err, "err should be nil") 75 | assert.Equal(t, "JP", region) 76 | 77 | t.Setenv("LANGUAGE", "") 78 | 79 | locale, err = GetLocale() 80 | assert.Equal(t, nil, err, "err should be nil") 81 | assert.Equal(t, "en-US", locale) 82 | 83 | lang, err = GetLanguage() 84 | assert.Equal(t, nil, err, "err should be nil") 85 | assert.Equal(t, "en", lang) 86 | 87 | region, err = GetRegion() 88 | assert.Equal(t, nil, err, "err should be nil") 89 | assert.Equal(t, "US", region) 90 | 91 | t.Setenv("LC_ALL", "") 92 | 93 | locale, err = GetLocale() 94 | assert.Equal(t, nil, err, "err should be nil") 95 | assert.Equal(t, "ko-KR", locale) 96 | 97 | lang, err = GetLanguage() 98 | assert.Equal(t, nil, err, "err should be nil") 99 | assert.Equal(t, "ko", lang) 100 | 101 | region, err = GetRegion() 102 | assert.Equal(t, nil, err, "err should be nil") 103 | assert.Equal(t, "KR", region) 104 | 105 | t.Setenv("LC_MESSAGES", "") 106 | 107 | locale, err = GetLocale() 108 | assert.Equal(t, nil, err, "err should be nil") 109 | assert.Equal(t, "fr-FR", locale) 110 | 111 | lang, err = GetLanguage() 112 | assert.Equal(t, nil, err, "err should be nil") 113 | assert.Equal(t, "fr", lang) 114 | 115 | region, err = GetRegion() 116 | assert.Equal(t, nil, err, "err should be nil") 117 | assert.Equal(t, "FR", region) 118 | } 119 | 120 | func TestLocaleNoRegion(t *testing.T) { 121 | t.Setenv("LANG", "fr") 122 | 123 | locales, err := GetLocales() 124 | assert.Equal(t, nil, err, "err should be nil") 125 | assert.Equal(t, []string{"fr"}, locales) 126 | 127 | locale, err := GetLocale() 128 | assert.Equal(t, nil, err, "err should be nil") 129 | assert.Equal(t, "fr", locale) 130 | 131 | lang, err := GetLanguage() 132 | assert.Equal(t, nil, err, "err should be nil") 133 | assert.Equal(t, "fr", lang) 134 | 135 | region, err := GetRegion() 136 | assert.Equal(t, nil, err, "err should be nil") 137 | assert.Equal(t, "", region) 138 | } 139 | 140 | func TestNoLocale(t *testing.T) { 141 | var nilStringSlice []string 142 | 143 | for _, env := range [...]string{"LANGUAGE", "LC_ALL", "LC_MESSAGES", "LANG"} { 144 | t.Setenv(env, "") 145 | } 146 | 147 | locales, err := GetLocales() 148 | assert.Equal(t, nil, err, "err should be nil") 149 | assert.Equal(t, nilStringSlice, locales) 150 | 151 | locale, err := GetLocale() 152 | assert.Equal(t, nil, err, "err should be nil") 153 | assert.Equal(t, "", locale) 154 | 155 | lang, err := GetLanguage() 156 | assert.Equal(t, nil, err, "err should be nil") 157 | assert.Equal(t, "", lang) 158 | 159 | region, err := GetRegion() 160 | assert.Equal(t, nil, err, "err should be nil") 161 | assert.Equal(t, "", region) 162 | } 163 | -------------------------------------------------------------------------------- /locale_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package locale 4 | 5 | import ( 6 | "fmt" 7 | "unsafe" 8 | 9 | "golang.org/x/sys/windows" 10 | ) 11 | 12 | // LocaleNameMaxLength is the maximum length of a locale name on Windows. 13 | // See https://docs.microsoft.com/en-us/windows/win32/intl/locale-name-constants. 14 | const LocaleNameMaxLength uint32 = 85 15 | 16 | func getWindowsLocaleFromProc(syscall string) (string, error) { 17 | dll, err := windows.LoadDLL("kernel32") 18 | if err != nil { 19 | return "", fmt.Errorf("could not find the kernel32 DLL: %v", err) 20 | } 21 | 22 | proc, err := dll.FindProc(syscall) 23 | if err != nil { 24 | return "", fmt.Errorf("could not find the %s proc in kernel32: %v", syscall, err) 25 | } 26 | 27 | buffer := make([]uint16, LocaleNameMaxLength) 28 | 29 | // See https://docs.microsoft.com/en-us/windows/win32/api/winnls/nf-winnls-getuserdefaultlocalename 30 | // and https://docs.microsoft.com/en-us/windows/win32/api/winnls/nf-winnls-getsystemdefaultlocalename 31 | // GetUserDefaultLocaleName and GetSystemDefaultLocaleName both take a buffer and a buffer size, 32 | // and return the length of the locale name (0 if not found). 33 | ret, _, err := proc.Call(uintptr(unsafe.Pointer(&buffer[0])), uintptr(LocaleNameMaxLength)) 34 | if ret == 0 { 35 | return "", fmt.Errorf("locale not found when calling %s: %v", syscall, err) 36 | } 37 | 38 | return windows.UTF16ToString(buffer), nil 39 | } 40 | 41 | func getWindowsLocale() (string, error) { 42 | var ( 43 | locale string 44 | err error 45 | ) 46 | 47 | for _, proc := range [...]string{"GetUserDefaultLocaleName", "GetSystemDefaultLocaleName"} { 48 | locale, err = getWindowsLocaleFromProc(proc) 49 | if err == nil { 50 | return locale, err 51 | } 52 | } 53 | 54 | return locale, err 55 | } 56 | 57 | // GetLocale retrieves the IETF BCP 47 language tag set on the system. 58 | func GetLocale() (string, error) { 59 | locale, err := getWindowsLocale() 60 | if err != nil { 61 | return "", fmt.Errorf("cannot determine locale: %v", err) 62 | } 63 | 64 | return locale, err 65 | } 66 | 67 | // GetLocales retrieves the IETF BCP 47 language tags set on the system. 68 | func GetLocales() ([]string, error) { 69 | locale, err := GetLocale() 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | return []string{locale}, nil 75 | } 76 | 77 | // GetLanguage retrieves the IETF BCP 47 language tag set on the system and 78 | // returns the language part of the tag. 79 | func GetLanguage() (string, error) { 80 | language := "" 81 | 82 | locale, err := GetLocale() 83 | if err == nil { 84 | language, _ = splitLocale(locale) 85 | } 86 | 87 | return language, err 88 | } 89 | 90 | // GetRegion retrieves the IETF BCP 47 language tag set on the system and 91 | // returns the region part of the tag. 92 | func GetRegion() (string, error) { 93 | region := "" 94 | 95 | locale, err := GetLocale() 96 | if err == nil { 97 | _, region = splitLocale(locale) 98 | } 99 | 100 | return region, err 101 | } 102 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | //go:build !android 2 | 3 | package locale 4 | 5 | import ( 6 | "strings" 7 | ) 8 | 9 | // SetRunOnJVM is a noop, this function is only valid on Android 10 | func SetRunOnJVM(_ func(fn func(vm, env, ctx uintptr) error) error) {} 11 | 12 | func splitLocale(locale string) (string, string) { 13 | // Remove the encoding, if present. 14 | formattedLocale, _, _ := strings.Cut(locale, ".") 15 | 16 | // Normalize by replacing the hyphens with underscores 17 | formattedLocale = strings.ReplaceAll(formattedLocale, "-", "_") 18 | 19 | // Split at the underscore. 20 | language, territory, _ := strings.Cut(formattedLocale, "_") 21 | return language, territory 22 | } 23 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | //go:build !android 2 | 3 | package locale 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | var ( 12 | globalLanguage string 13 | globalRegion string 14 | ) 15 | 16 | func BenchmarkSplitLocale(b *testing.B) { 17 | language := "" 18 | region := "" 19 | b.ReportAllocs() 20 | 21 | for i := 0; i < b.N; i++ { 22 | language, region = splitLocale("en_US.UTF-8") 23 | } 24 | 25 | globalLanguage = language 26 | globalRegion = region 27 | } 28 | 29 | func TestSplitLocale(t *testing.T) { 30 | language, region := splitLocale("en_US.UTF-8") 31 | assert.Equal(t, "en", language) 32 | assert.Equal(t, "US", region) 33 | 34 | language, region = splitLocale("fr-FR") 35 | assert.Equal(t, "fr", language) 36 | assert.Equal(t, "FR", region) 37 | 38 | language, region = splitLocale("ja") 39 | assert.Equal(t, "ja", language) 40 | assert.Equal(t, "", region) 41 | 42 | language, region = splitLocale("test") 43 | assert.Equal(t, "test", language) 44 | assert.Equal(t, "", region) 45 | } 46 | --------------------------------------------------------------------------------