pax_global_header 0000666 0000000 0000000 00000000064 13562341400 0014510 g ustar 00root root 0000000 0000000 52 comment=f87b1403479a348651dbf5f07f5cc6e5fcf07008
universal-translator-0.17.0/ 0000775 0000000 0000000 00000000000 13562341400 0015774 5 ustar 00root root 0000000 0000000 universal-translator-0.17.0/.gitignore 0000664 0000000 0000000 00000000430 13562341400 0017761 0 ustar 00root root 0000000 0000000 # Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
*.coverprofile universal-translator-0.17.0/.travis.yml 0000664 0000000 0000000 00000001027 13562341400 0020105 0 ustar 00root root 0000000 0000000 language: go
go:
- 1.13.4
- tip
matrix:
allow_failures:
- go: tip
notifications:
email:
recipients: dean.karn@gmail.com
on_success: change
on_failure: always
before_install:
- go install github.com/mattn/goveralls
# Only clone the most recent commit.
git:
depth: 1
script:
- go test -v -race -covermode=atomic -coverprofile=coverage.coverprofile ./...
after_success: |
[ $TRAVIS_GO_VERSION = 1.13.4 ] &&
goveralls -coverprofile=coverage.coverprofile -service travis-ci -repotoken $COVERALLS_TOKEN universal-translator-0.17.0/LICENSE 0000664 0000000 0000000 00000002070 13562341400 0017000 0 ustar 00root root 0000000 0000000 The MIT License (MIT)
Copyright (c) 2016 Go Playground
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
universal-translator-0.17.0/README.md 0000664 0000000 0000000 00000011077 13562341400 0017261 0 ustar 00root root 0000000 0000000 ## universal-translator

[](https://travis-ci.org/go-playground/universal-translator)
[](https://coveralls.io/github/go-playground/universal-translator)
[](https://goreportcard.com/report/github.com/go-playground/universal-translator)
[](https://godoc.org/github.com/go-playground/universal-translator)

[](https://gitter.im/go-playground/universal-translator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
Universal Translator is an i18n Translator for Go/Golang using CLDR data + pluralization rules
Why another i18n library?
--------------------------
Because none of the plural rules seem to be correct out there, including the previous implementation of this package,
so I took it upon myself to create [locales](https://github.com/go-playground/locales) for everyone to use; this package
is a thin wrapper around [locales](https://github.com/go-playground/locales) in order to store and translate text for
use in your applications.
Features
--------
- [x] Rules generated from the [CLDR](http://cldr.unicode.org/index/downloads) data, v30.0.3
- [x] Contains Cardinal, Ordinal and Range Plural Rules
- [x] Contains Month, Weekday and Timezone translations built in
- [x] Contains Date & Time formatting functions
- [x] Contains Number, Currency, Accounting and Percent formatting functions
- [x] Supports the "Gregorian" calendar only ( my time isn't unlimited, had to draw the line somewhere )
- [x] Support loading translations from files
- [x] Exporting translations to file(s), mainly for getting them professionally translated
- [ ] Code Generation for translation files -> Go code.. i.e. after it has been professionally translated
- [ ] Tests for all languages, I need help with this, please see [here](https://github.com/go-playground/locales/issues/1)
Installation
-----------
Use go get
```shell
go get github.com/go-playground/universal-translator
```
Usage & Documentation
-------
Please see https://godoc.org/github.com/go-playground/universal-translator for usage docs
##### Examples:
- [Basic](https://github.com/go-playground/universal-translator/tree/master/_examples/basic)
- [Full - no files](https://github.com/go-playground/universal-translator/tree/master/_examples/full-no-files)
- [Full - with files](https://github.com/go-playground/universal-translator/tree/master/_examples/full-with-files)
File formatting
--------------
All types, Plain substitution, Cardinal, Ordinal and Range translations can all be contained withing the same file(s);
they are only separated for easy viewing.
##### Examples:
- [Formats](https://github.com/go-playground/universal-translator/tree/master/_examples/file-formats)
##### Basic Makeup
NOTE: not all fields are needed for all translation types, see [examples](https://github.com/go-playground/universal-translator/tree/master/_examples/file-formats)
```json
{
"locale": "en",
"key": "days-left",
"trans": "You have {0} day left.",
"type": "Cardinal",
"rule": "One",
"override": false
}
```
|Field|Description|
|---|---|
|locale|The locale for which the translation is for.|
|key|The translation key that will be used to store and lookup each translation; normally it is a string or integer.|
|trans|The actual translation text.|
|type|The type of translation Cardinal, Ordinal, Range or "" for a plain substitution(not required to be defined if plain used)|
|rule|The plural rule for which the translation is for eg. One, Two, Few, Many or Other.(not required to be defined if plain used)|
|override|If you wish to override an existing translation that has already been registered, set this to 'true'. 99% of the time there is no need to define it.|
Help With Tests
---------------
To anyone interesting in helping or contributing, I sure could use some help creating tests for each language.
Please see issue [here](https://github.com/go-playground/locales/issues/1) for details.
License
------
Distributed under MIT License, please see license file in code for more details.
universal-translator-0.17.0/_examples/ 0000775 0000000 0000000 00000000000 13562341400 0017751 5 ustar 00root root 0000000 0000000 universal-translator-0.17.0/_examples/basic/ 0000775 0000000 0000000 00000000000 13562341400 0021032 5 ustar 00root root 0000000 0000000 universal-translator-0.17.0/_examples/basic/main.go 0000664 0000000 0000000 00000005563 13562341400 0022316 0 ustar 00root root 0000000 0000000 package main
import (
"fmt"
"github.com/go-playground/locales"
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/en_CA"
"github.com/go-playground/locales/fr"
"github.com/go-playground/locales/nl"
"github.com/go-playground/universal-translator"
)
// only one instance as translators within are shared + goroutine safe
var universalTraslator *ut.UniversalTranslator
func main() {
// NOTE: this example is omitting a lot of error checking for brevity
e := en.New()
universalTraslator = ut.New(e, e, en_CA.New(), nl.New(), fr.New())
en, _ := universalTraslator.GetTranslator("en")
// generally used after parsing an http 'Accept-Language' header
// and this will try to find a matching locale you support or
// fallback locale.
// en, _ := ut.FindTranslator([]string{"en", "en_CA", "nl"})
// this will help
fmt.Println("Cardinal Plural Rules:", en.PluralsCardinal())
fmt.Println("Ordinal Plural Rules:", en.PluralsOrdinal())
fmt.Println("Range Plural Rules:", en.PluralsRange())
// add basic language only translations
// last param indicates if it's ok to override the translation if one already exists
en.Add("welcome", "Welcome {0} to our test", false)
// add language translations dependant on cardinal plural rules
en.AddCardinal("days", "You have {0} day left to register", locales.PluralRuleOne, false)
en.AddCardinal("days", "You have {0} days left to register", locales.PluralRuleOther, false)
// add language translations dependant on ordinal plural rules
en.AddOrdinal("day-of-month", "{0}st", locales.PluralRuleOne, false)
en.AddOrdinal("day-of-month", "{0}nd", locales.PluralRuleTwo, false)
en.AddOrdinal("day-of-month", "{0}rd", locales.PluralRuleFew, false)
en.AddOrdinal("day-of-month", "{0}th", locales.PluralRuleOther, false)
// add language translations dependant on range plural rules
// NOTE: only one plural rule for range in 'en' locale
en.AddRange("between", "It's {0}-{1} days away", locales.PluralRuleOther, false)
// now lets use the translations we just added, in the same order we added them
fmt.Println(en.T("welcome", "Joeybloggs"))
fmt.Println(en.C("days", 1, 0, en.FmtNumber(1, 0))) // you'd normally have variables defined for 1 and 0
fmt.Println(en.C("days", 2, 0, en.FmtNumber(2, 0)))
fmt.Println(en.C("days", 10456.25, 2, en.FmtNumber(10456.25, 2)))
fmt.Println(en.O("day-of-month", 1, 0, en.FmtNumber(1, 0)))
fmt.Println(en.O("day-of-month", 2, 0, en.FmtNumber(2, 0)))
fmt.Println(en.O("day-of-month", 3, 0, en.FmtNumber(3, 0)))
fmt.Println(en.O("day-of-month", 4, 0, en.FmtNumber(4, 0)))
fmt.Println(en.O("day-of-month", 10456.25, 0, en.FmtNumber(10456.25, 0)))
fmt.Println(en.R("between", 0, 0, 1, 0, en.FmtNumber(0, 0), en.FmtNumber(1, 0)))
fmt.Println(en.R("between", 1, 0, 2, 0, en.FmtNumber(1, 0), en.FmtNumber(2, 0)))
fmt.Println(en.R("between", 1, 0, 100, 0, en.FmtNumber(1, 0), en.FmtNumber(100, 0)))
}
universal-translator-0.17.0/_examples/file-formats/ 0000775 0000000 0000000 00000000000 13562341400 0022341 5 ustar 00root root 0000000 0000000 universal-translator-0.17.0/_examples/file-formats/cardinal.json 0000664 0000000 0000000 00000000511 13562341400 0025006 0 ustar 00root root 0000000 0000000 [
{
"locale": "en",
"key": "cardinal_test",
"trans": "You have {0} day left.",
"type": "Cardinal",
"rule": "One"
},
{
"locale": "en",
"key": "cardinal_test",
"trans": "You have {0} days left.",
"type": "Cardinal",
"rule": "Other"
}
] universal-translator-0.17.0/_examples/file-formats/ordinal.json 0000664 0000000 0000000 00000001034 13562341400 0024662 0 ustar 00root root 0000000 0000000 [
{
"locale": "en",
"key": "day",
"trans": "{0}st",
"type": "Ordinal",
"rule": "One"
},
{
"locale": "en",
"key": "day",
"trans": "{0}nd",
"type": "Ordinal",
"rule": "Two"
},
{
"locale": "en",
"key": "day",
"trans": "{0}rd",
"type": "Ordinal",
"rule": "Few"
},
{
"locale": "en",
"key": "day",
"trans": "{0}th",
"type": "Ordinal",
"rule": "Other"
}
] universal-translator-0.17.0/_examples/file-formats/plain-substitution.json 0000664 0000000 0000000 00000000766 13562341400 0027122 0 ustar 00root root 0000000 0000000 [
{
"locale": "en",
"key": "test_trans4",
"trans": "{0}{1}"
},
{
"locale": "en",
"key": "test_trans",
"trans": "Welcome {0} to the {1}."
},
{
"locale": "en",
"key": -1,
"trans": "Welcome {0}"
},
{
"locale": "en",
"key": "test_trans2",
"trans": "{0} to the {1}."
},
{
"locale": "en",
"key": "test_trans3",
"trans": "Welcome {0} to the {1}"
}
] universal-translator-0.17.0/_examples/file-formats/range.json 0000664 0000000 0000000 00000000465 13562341400 0024335 0 ustar 00root root 0000000 0000000 [
{
"locale": "nl",
"key": "day",
"trans": "er {0}-{1} dag vertrokken",
"type": "Range",
"rule": "One"
},
{
"locale": "nl",
"key": "day",
"trans": "er zijn {0}-{1} dagen over",
"type": "Range",
"rule": "Other"
}
] universal-translator-0.17.0/_examples/full-no-files/ 0000775 0000000 0000000 00000000000 13562341400 0022425 5 ustar 00root root 0000000 0000000 universal-translator-0.17.0/_examples/full-no-files/home.tmpl 0000664 0000000 0000000 00000003331 13562341400 0024253 0 ustar 00root root 0000000 0000000 {{ define "home" }}
Locale: {{ .Trans.Locale }}
Trans1: {{ .Trans.C "days-left" 1 0 "1" }}
Trans2: {{ .Trans.C "days-left" 2 0 "2" }}
FmtNumber Positive: {{ .Trans.FmtNumber .PositiveNum 2 }}
FmtNumber Negative: {{ .Trans.FmtNumber .NegativeNum 2 }}
FmtPercent Negative: {{ .Trans.FmtPercent .Percent 2 }}
FmtCurrency Negative: {{ .Trans.FmtCurrency .PositiveNum 2 .Trans.Currency }}
FmtCurrency Negative: {{ .Trans.FmtCurrency .NegativeNum 2 .Trans.Currency }}
FmtAccounting Negative: {{ .Trans.FmtAccounting .PositiveNum 2 .Trans.Currency }}
FmtAccounting Negative: {{ .Trans.FmtAccounting .NegativeNum 2 .Trans.Currency }}
FmtDateShort: {{ .Trans.FmtDateShort .Now }}
FmtDateMedium: {{ .Trans.FmtDateMedium .Now }}
FmtDateLong: {{ .Trans.FmtDateLong .Now }}
FmtDateFull: {{ .Trans.FmtDateFull .Now }}
FmtTimeShort: {{ .Trans.FmtTimeShort .Now }}
FmtTimeMedium: {{ .Trans.FmtTimeMedium .Now }}
FmtTimeLong: {{ .Trans.FmtTimeLong .Now }}
FmtTimeFull: {{ .Trans.FmtTimeFull .Now }}
MonthsAbbreviated: {{ .Trans.MonthsAbbreviated }}
MonthsNarrow: {{ .Trans.MonthsNarrow }}
MonthsWide: {{ .Trans.MonthsWide }}
WeekdaysAbbreviated: {{ .Trans.WeekdaysAbbreviated }}
WeekdaysNarrow: {{ .Trans.WeekdaysNarrow }}
WeekdaysShort: {{ .Trans.WeekdaysShort }}
WeekdaysWide: {{ .Trans.WeekdaysWide }}
{{ end }} universal-translator-0.17.0/_examples/full-no-files/main.go 0000664 0000000 0000000 00000013341 13562341400 0023702 0 ustar 00root root 0000000 0000000 package main import ( "context" "html/template" "log" "net/http" "time" "github.com/go-playground/locales" "github.com/go-playground/locales/currency" "github.com/go-playground/locales/en" "github.com/go-playground/locales/fr" "github.com/go-playground/pure/v5" "github.com/go-playground/pure/v5/_examples/middleware/logging-recovery" "github.com/go-playground/universal-translator" ) var ( tmpls *template.Template utrans *ut.UniversalTranslator transKey = struct { name string }{ name: "transKey", } ) // Translator wraps ut.Translator in order to handle errors transparently // it is totally optional but recommended as it can now be used directly in // templates and nobody can add translations where they're not supposed to. type Translator interface { locales.Translator // creates the translation for the locale given the 'key' and params passed in. // wraps ut.Translator.T to handle errors T(key interface{}, params ...string) string // creates the cardinal translation for the locale given the 'key', 'num' and 'digit' arguments // and param passed in. // wraps ut.Translator.C to handle errors C(key interface{}, num float64, digits uint64, param string) string // creates the ordinal translation for the locale given the 'key', 'num' and 'digit' arguments // and param passed in. // wraps ut.Translator.O to handle errors O(key interface{}, num float64, digits uint64, param string) string // creates the range translation for the locale given the 'key', 'num1', 'digit1', 'num2' and // 'digit2' arguments and 'param1' and 'param2' passed in // wraps ut.Translator.R to handle errors R(key interface{}, num1 float64, digits1 uint64, num2 float64, digits2 uint64, param1, param2 string) string // Currency returns the type used by the given locale. Currency() currency.Type } // implements Translator interface definition above. type translator struct { locales.Translator trans ut.Translator } var _ Translator = (*translator)(nil) func (t *translator) T(key interface{}, params ...string) string { s, err := t.trans.T(key, params...) if err != nil { log.Printf("issue translating key: '%v' error: '%s'", key, err) } return s } func (t *translator) C(key interface{}, num float64, digits uint64, param string) string { s, err := t.trans.C(key, num, digits, param) if err != nil { log.Printf("issue translating cardinal key: '%v' error: '%s'", key, err) } return s } func (t *translator) O(key interface{}, num float64, digits uint64, param string) string { s, err := t.trans.C(key, num, digits, param) if err != nil { log.Printf("issue translating ordinal key: '%v' error: '%s'", key, err) } return s } func (t *translator) R(key interface{}, num1 float64, digits1 uint64, num2 float64, digits2 uint64, param1, param2 string) string { s, err := t.trans.R(key, num1, digits1, num2, digits2, param1, param2) if err != nil { log.Printf("issue translating range key: '%v' error: '%s'", key, err) } return s } func (t *translator) Currency() currency.Type { // choose your own locale. The reason it isn't mapped for you is because many // countries have multiple currencies; it's up to you and you're application how // and which currencies to use. I recommend adding a function it to to your custon translator // interface like defined above. switch t.Locale() { case "en": return currency.USD case "fr": return currency.EUR default: return currency.USD } } func main() { en := en.New() utrans = ut.New(en, en, fr.New()) setup() tmpls, _ = template.ParseFiles("home.tmpl") r := pure.New() r.Use(middleware.LoggingAndRecovery(true), translatorMiddleware) r.Get("/", home) log.Println("Running on Port :8080") log.Println("Try me with URL http://localhost:8080/?locale=en") log.Println("and then http://localhost:8080/?locale=fr") http.ListenAndServe(":8080", r.Serve()) } func home(w http.ResponseWriter, r *http.Request) { // get locale translator ( could be wrapped into a helper function ) t := r.Context().Value(transKey).(Translator) s := struct { Trans Translator Now time.Time PositiveNum float64 NegativeNum float64 Percent float64 }{ Trans: t, Now: time.Now(), PositiveNum: 1234576.45, NegativeNum: -35900394.34, Percent: 96.76, } if err := tmpls.ExecuteTemplate(w, "home", s); err != nil { log.Fatal(err) } } func translatorMiddleware(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // there are many ways to check, this is just checking for query param & // Accept-Language header but can be expanded to Cookie's etc.... params := r.URL.Query() locale := params.Get("locale") var t ut.Translator if len(locale) > 0 { var found bool if t, found = utrans.GetTranslator(locale); found { goto END } } // get and parse the "Accept-Language" http header and return an array t, _ = utrans.FindTranslator(pure.AcceptedLanguages(r)...) END: // I would normally wrap ut.Translator with one with my own functions in order // to handle errors and be able to use all functions from translator within the templates. r = r.WithContext(context.WithValue(r.Context(), transKey, &translator{trans: t, Translator: t.(locales.Translator)})) next(w, r) } } func setup() { en, _ := utrans.FindTranslator("en") en.AddCardinal("days-left", "There is {0} day left", locales.PluralRuleOne, false) en.AddCardinal("days-left", "There are {0} days left", locales.PluralRuleOther, false) fr, _ := utrans.FindTranslator("fr") fr.AddCardinal("days-left", "Il reste {0} jour", locales.PluralRuleOne, false) fr.AddCardinal("days-left", "Il reste {0} jours", locales.PluralRuleOther, false) err := utrans.VerifyTranslations() if err != nil { log.Fatal(err) } } universal-translator-0.17.0/_examples/full-with-files/ 0000775 0000000 0000000 00000000000 13562341400 0022764 5 ustar 00root root 0000000 0000000 universal-translator-0.17.0/_examples/full-with-files/home.tmpl 0000664 0000000 0000000 00000003331 13562341400 0024612 0 ustar 00root root 0000000 0000000 {{ define "home" }}Locale: {{ .Trans.Locale }}
Trans1: {{ .Trans.C "days-left" 1 0 "1" }}
Trans2: {{ .Trans.C "days-left" 2 0 "2" }}
FmtNumber Positive: {{ .Trans.FmtNumber .PositiveNum 2 }}
FmtNumber Negative: {{ .Trans.FmtNumber .NegativeNum 2 }}
FmtPercent Negative: {{ .Trans.FmtPercent .Percent 2 }}
FmtCurrency Negative: {{ .Trans.FmtCurrency .PositiveNum 2 .Trans.Currency }}
FmtCurrency Negative: {{ .Trans.FmtCurrency .NegativeNum 2 .Trans.Currency }}
FmtAccounting Negative: {{ .Trans.FmtAccounting .PositiveNum 2 .Trans.Currency }}
FmtAccounting Negative: {{ .Trans.FmtAccounting .NegativeNum 2 .Trans.Currency }}
FmtDateShort: {{ .Trans.FmtDateShort .Now }}
FmtDateMedium: {{ .Trans.FmtDateMedium .Now }}
FmtDateLong: {{ .Trans.FmtDateLong .Now }}
FmtDateFull: {{ .Trans.FmtDateFull .Now }}
FmtTimeShort: {{ .Trans.FmtTimeShort .Now }}
FmtTimeMedium: {{ .Trans.FmtTimeMedium .Now }}
FmtTimeLong: {{ .Trans.FmtTimeLong .Now }}
FmtTimeFull: {{ .Trans.FmtTimeFull .Now }}
MonthsAbbreviated: {{ .Trans.MonthsAbbreviated }}
MonthsNarrow: {{ .Trans.MonthsNarrow }}
MonthsWide: {{ .Trans.MonthsWide }}
WeekdaysAbbreviated: {{ .Trans.WeekdaysAbbreviated }}
WeekdaysNarrow: {{ .Trans.WeekdaysNarrow }}
WeekdaysShort: {{ .Trans.WeekdaysShort }}
WeekdaysWide: {{ .Trans.WeekdaysWide }}
{{ end }} universal-translator-0.17.0/_examples/full-with-files/main.go 0000664 0000000 0000000 00000012636 13562341400 0024247 0 ustar 00root root 0000000 0000000 package main import ( "context" "html/template" "log" "net/http" "time" "github.com/go-playground/locales" "github.com/go-playground/locales/currency" "github.com/go-playground/locales/en" "github.com/go-playground/locales/fr" "github.com/go-playground/pure/v5" "github.com/go-playground/pure/v5/_examples/middleware/logging-recovery" "github.com/go-playground/universal-translator" ) var ( tmpls *template.Template utrans *ut.UniversalTranslator transKey = struct { name string }{ name: "transKey", } ) // Translator wraps ut.Translator in order to handle errors transparently // it is totally optional but recommended as it can now be used directly in // templates and nobody can add translations where they're not supposed to. type Translator interface { locales.Translator // creates the translation for the locale given the 'key' and params passed in. // wraps ut.Translator.T to handle errors T(key interface{}, params ...string) string // creates the cardinal translation for the locale given the 'key', 'num' and 'digit' arguments // and param passed in. // wraps ut.Translator.C to handle errors C(key interface{}, num float64, digits uint64, param string) string // creates the ordinal translation for the locale given the 'key', 'num' and 'digit' arguments // and param passed in. // wraps ut.Translator.O to handle errors O(key interface{}, num float64, digits uint64, param string) string // creates the range translation for the locale given the 'key', 'num1', 'digit1', 'num2' and // 'digit2' arguments and 'param1' and 'param2' passed in // wraps ut.Translator.R to handle errors R(key interface{}, num1 float64, digits1 uint64, num2 float64, digits2 uint64, param1, param2 string) string // Currency returns the type used by the given locale. Currency() currency.Type } // implements Translator interface definition above. type translator struct { locales.Translator trans ut.Translator } var _ Translator = (*translator)(nil) func (t *translator) T(key interface{}, params ...string) string { s, err := t.trans.T(key, params...) if err != nil { log.Printf("issue translating key: '%v' error: '%s'", key, err) } return s } func (t *translator) C(key interface{}, num float64, digits uint64, param string) string { s, err := t.trans.C(key, num, digits, param) if err != nil { log.Printf("issue translating cardinal key: '%v' error: '%s'", key, err) } return s } func (t *translator) O(key interface{}, num float64, digits uint64, param string) string { s, err := t.trans.C(key, num, digits, param) if err != nil { log.Printf("issue translating ordinal key: '%v' error: '%s'", key, err) } return s } func (t *translator) R(key interface{}, num1 float64, digits1 uint64, num2 float64, digits2 uint64, param1, param2 string) string { s, err := t.trans.R(key, num1, digits1, num2, digits2, param1, param2) if err != nil { log.Printf("issue translating range key: '%v' error: '%s'", key, err) } return s } func (t *translator) Currency() currency.Type { // choose your own locale. The reason it isn't mapped for you is because many // countries have multiple currencies; it's up to you and you're application how // and which currencies to use. I recommend adding a function it to to your custon translator // interface like defined above. switch t.Locale() { case "en": return currency.USD case "fr": return currency.EUR default: return currency.USD } } func main() { en := en.New() utrans = ut.New(en, en, fr.New()) setup() tmpls, _ = template.ParseFiles("home.tmpl") r := pure.New() r.Use(middleware.LoggingAndRecovery(true), translatorMiddleware) r.Get("/", home) log.Println("Running on Port :8080") log.Println("Try me with URL http://localhost:8080/?locale=en") log.Println("and then http://localhost:8080/?locale=fr") http.ListenAndServe(":8080", r.Serve()) } func home(w http.ResponseWriter, r *http.Request) { // get locale translator ( could be wrapped into a helper function ) t := r.Context().Value(transKey).(Translator) s := struct { Trans Translator Now time.Time PositiveNum float64 NegativeNum float64 Percent float64 }{ Trans: t, Now: time.Now(), PositiveNum: 1234576.45, NegativeNum: -35900394.34, Percent: 96.76, } if err := tmpls.ExecuteTemplate(w, "home", s); err != nil { log.Fatal(err) } } func translatorMiddleware(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // there are many ways to check, this is just checking for query param & // Accept-Language header but can be expanded to Cookie's etc.... params := r.URL.Query() locale := params.Get("locale") var t ut.Translator if len(locale) > 0 { var found bool if t, found = utrans.GetTranslator(locale); found { goto END } } // get and parse the "Accept-Language" http header and return an array t, _ = utrans.FindTranslator(pure.AcceptedLanguages(r)...) END: // I would normally wrap ut.Translator with one with my own functions in order // to handle errors and be able to use all functions from translator within the templates. r = r.WithContext(context.WithValue(r.Context(), transKey, &translator{trans: t, Translator: t.(locales.Translator)})) next(w, r) } } func setup() { err := utrans.Import(ut.FormatJSON, "translations") if err != nil { log.Fatal(err) } err = utrans.VerifyTranslations() if err != nil { log.Fatal(err) } } universal-translator-0.17.0/_examples/full-with-files/translations/ 0000775 0000000 0000000 00000000000 13562341400 0025505 5 ustar 00root root 0000000 0000000 universal-translator-0.17.0/_examples/full-with-files/translations/en/ 0000775 0000000 0000000 00000000000 13562341400 0026107 5 ustar 00root root 0000000 0000000 universal-translator-0.17.0/_examples/full-with-files/translations/en/home.json 0000664 0000000 0000000 00000000500 13562341400 0027725 0 ustar 00root root 0000000 0000000 [ { "locale": "en", "key": "days-left", "trans": "There is {0} day left", "type": "Cardinal", "rule": "One" }, { "locale": "en", "key": "days-left", "trans": "There are {0} days left", "type": "Cardinal", "rule": "Other" } ] universal-translator-0.17.0/_examples/full-with-files/translations/fr/ 0000775 0000000 0000000 00000000000 13562341400 0026114 5 ustar 00root root 0000000 0000000 universal-translator-0.17.0/_examples/full-with-files/translations/fr/home.json 0000664 0000000 0000000 00000000467 13562341400 0027746 0 ustar 00root root 0000000 0000000 [ { "locale": "fr", "key": "days-left", "trans": "Il reste {0} jour", "type": "Cardinal", "rule": "One" }, { "locale": "fr", "key": "days-left", "trans": "Il reste {0} jours", "type": "Cardinal", "rule": "Other" } ] universal-translator-0.17.0/benchmarks_test.go 0000664 0000000 0000000 00000003734 13562341400 0021506 0 ustar 00root root 0000000 0000000 package ut import ( "testing" "github.com/go-playground/locales/en" ) func BenchmarkBasicTranslation(b *testing.B) { en := en.New() ut := New(en, en) loc, found := ut.FindTranslator("en") if !found { b.Fatalf("Expected '%t' Got '%t'", true, found) } translations := []struct { key interface{} trans string expected error override bool }{ { key: "welcome", trans: "Welcome to the site", expected: nil, }, { key: "welcome-user", trans: "Welcome to the site {0}", expected: nil, }, { key: "welcome-user2", trans: "Welcome to the site {0}, your location is {1}", expected: nil, }, } for _, tt := range translations { if err := loc.Add(tt.key, tt.trans, tt.override); err != nil { b.Fatalf("adding translation '%s' failed with key '%s'", tt.trans, tt.key) } } var err error b.ResetTimer() b.Run("", func(b *testing.B) { for i := 0; i < b.N; i++ { if _, err = loc.T("welcome"); err != nil { b.Error(err) } } }) b.Run("Parallel", func(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { if _, err = loc.T("welcome"); err != nil { b.Error(err) } } }) }) b.Run("With1Param", func(b *testing.B) { for i := 0; i < b.N; i++ { if _, err = loc.T("welcome-user", "Joeybloggs"); err != nil { b.Error(err) } } }) b.Run("ParallelWith1Param", func(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { if _, err = loc.T("welcome-user", "Joeybloggs"); err != nil { b.Error(err) } } }) }) b.Run("With2Param", func(b *testing.B) { for i := 0; i < b.N; i++ { if _, err = loc.T("welcome-user2", "Joeybloggs", "/dev/tty0"); err != nil { b.Error(err) } } }) b.Run("ParallelWith2Param", func(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { if _, err = loc.T("welcome-user2", "Joeybloggs", "/dev/tty0"); err != nil { b.Error(err) } } }) }) } universal-translator-0.17.0/errors.go 0000664 0000000 0000000 00000011102 13562341400 0017632 0 ustar 00root root 0000000 0000000 package ut import ( "errors" "fmt" "github.com/go-playground/locales" ) var ( // ErrUnknowTranslation indicates the translation could not be found ErrUnknowTranslation = errors.New("Unknown Translation") ) var _ error = new(ErrConflictingTranslation) var _ error = new(ErrRangeTranslation) var _ error = new(ErrOrdinalTranslation) var _ error = new(ErrCardinalTranslation) var _ error = new(ErrMissingPluralTranslation) var _ error = new(ErrExistingTranslator) // ErrExistingTranslator is the error representing a conflicting translator type ErrExistingTranslator struct { locale string } // Error returns ErrExistingTranslator's internal error text func (e *ErrExistingTranslator) Error() string { return fmt.Sprintf("error: conflicting translator for locale '%s'", e.locale) } // ErrConflictingTranslation is the error representing a conflicting translation type ErrConflictingTranslation struct { locale string key interface{} rule locales.PluralRule text string } // Error returns ErrConflictingTranslation's internal error text func (e *ErrConflictingTranslation) Error() string { if _, ok := e.key.(string); !ok { return fmt.Sprintf("error: conflicting key '%#v' rule '%s' with text '%s' for locale '%s', value being ignored", e.key, e.rule, e.text, e.locale) } return fmt.Sprintf("error: conflicting key '%s' rule '%s' with text '%s' for locale '%s', value being ignored", e.key, e.rule, e.text, e.locale) } // ErrRangeTranslation is the error representing a range translation error type ErrRangeTranslation struct { text string } // Error returns ErrRangeTranslation's internal error text func (e *ErrRangeTranslation) Error() string { return e.text } // ErrOrdinalTranslation is the error representing an ordinal translation error type ErrOrdinalTranslation struct { text string } // Error returns ErrOrdinalTranslation's internal error text func (e *ErrOrdinalTranslation) Error() string { return e.text } // ErrCardinalTranslation is the error representing a cardinal translation error type ErrCardinalTranslation struct { text string } // Error returns ErrCardinalTranslation's internal error text func (e *ErrCardinalTranslation) Error() string { return e.text } // ErrMissingPluralTranslation is the error signifying a missing translation given // the locales plural rules. type ErrMissingPluralTranslation struct { locale string key interface{} rule locales.PluralRule translationType string } // Error returns ErrMissingPluralTranslation's internal error text func (e *ErrMissingPluralTranslation) Error() string { if _, ok := e.key.(string); !ok { return fmt.Sprintf("error: missing '%s' plural rule '%s' for translation with key '%#v' and locale '%s'", e.translationType, e.rule, e.key, e.locale) } return fmt.Sprintf("error: missing '%s' plural rule '%s' for translation with key '%s' and locale '%s'", e.translationType, e.rule, e.key, e.locale) } // ErrMissingBracket is the error representing a missing bracket in a translation // eg. This is a {0 <-- missing ending '}' type ErrMissingBracket struct { locale string key interface{} text string } // Error returns ErrMissingBracket error message func (e *ErrMissingBracket) Error() string { return fmt.Sprintf("error: missing bracket '{}', in translation. locale: '%s' key: '%v' text: '%s'", e.locale, e.key, e.text) } // ErrBadParamSyntax is the error representing a bad parameter definition in a translation // eg. This is a {must-be-int} type ErrBadParamSyntax struct { locale string param string key interface{} text string } // Error returns ErrBadParamSyntax error message func (e *ErrBadParamSyntax) Error() string { return fmt.Sprintf("error: bad parameter syntax, missing parameter '%s' in translation. locale: '%s' key: '%v' text: '%s'", e.param, e.locale, e.key, e.text) } // import/export errors // ErrMissingLocale is the error representing an expected locale that could // not be found aka locale not registered with the UniversalTranslator Instance type ErrMissingLocale struct { locale string } // Error returns ErrMissingLocale's internal error text func (e *ErrMissingLocale) Error() string { return fmt.Sprintf("error: locale '%s' not registered.", e.locale) } // ErrBadPluralDefinition is the error representing an incorrect plural definition // usually found within translations defined within files during the import process. type ErrBadPluralDefinition struct { tl translation } // Error returns ErrBadPluralDefinition's internal error text func (e *ErrBadPluralDefinition) Error() string { return fmt.Sprintf("error: bad plural definition '%#v'", e.tl) } universal-translator-0.17.0/go.mod 0000664 0000000 0000000 00000000160 13562341400 0017077 0 ustar 00root root 0000000 0000000 module github.com/go-playground/universal-translator go 1.13 require github.com/go-playground/locales v0.13.0 universal-translator-0.17.0/go.sum 0000664 0000000 0000000 00000000566 13562341400 0017136 0 ustar 00root root 0000000 0000000 github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= universal-translator-0.17.0/import_export.go 0000664 0000000 0000000 00000013172 13562341400 0021242 0 ustar 00root root 0000000 0000000 package ut import ( "encoding/json" "fmt" "io/ioutil" "os" "path/filepath" "io" "github.com/go-playground/locales" ) type translation struct { Locale string `json:"locale"` Key interface{} `json:"key"` // either string or integer Translation string `json:"trans"` PluralType string `json:"type,omitempty"` PluralRule string `json:"rule,omitempty"` OverrideExisting bool `json:"override,omitempty"` } const ( cardinalType = "Cardinal" ordinalType = "Ordinal" rangeType = "Range" ) // ImportExportFormat is the format of the file import or export type ImportExportFormat uint8 // supported Export Formats const ( FormatJSON ImportExportFormat = iota ) // Export writes the translations out to a file on disk. // // NOTE: this currently only works with string or int translations keys. func (t *UniversalTranslator) Export(format ImportExportFormat, dirname string) error { _, err := os.Stat(dirname) fmt.Println(dirname, err, os.IsNotExist(err)) if err != nil { if !os.IsNotExist(err) { return err } if err = os.MkdirAll(dirname, 0744); err != nil { return err } } // build up translations var trans []translation var b []byte var ext string for _, locale := range t.translators { for k, v := range locale.(*translator).translations { trans = append(trans, translation{ Locale: locale.Locale(), Key: k, Translation: v.text, }) } for k, pluralTrans := range locale.(*translator).cardinalTanslations { for i, plural := range pluralTrans { // leave enough for all plural rules // but not all are set for all languages. if plural == nil { continue } trans = append(trans, translation{ Locale: locale.Locale(), Key: k.(string), Translation: plural.text, PluralType: cardinalType, PluralRule: locales.PluralRule(i).String(), }) } } for k, pluralTrans := range locale.(*translator).ordinalTanslations { for i, plural := range pluralTrans { // leave enough for all plural rules // but not all are set for all languages. if plural == nil { continue } trans = append(trans, translation{ Locale: locale.Locale(), Key: k.(string), Translation: plural.text, PluralType: ordinalType, PluralRule: locales.PluralRule(i).String(), }) } } for k, pluralTrans := range locale.(*translator).rangeTanslations { for i, plural := range pluralTrans { // leave enough for all plural rules // but not all are set for all languages. if plural == nil { continue } trans = append(trans, translation{ Locale: locale.Locale(), Key: k.(string), Translation: plural.text, PluralType: rangeType, PluralRule: locales.PluralRule(i).String(), }) } } switch format { case FormatJSON: b, err = json.MarshalIndent(trans, "", " ") ext = ".json" } if err != nil { return err } err = ioutil.WriteFile(filepath.Join(dirname, fmt.Sprintf("%s%s", locale.Locale(), ext)), b, 0644) if err != nil { return err } trans = trans[0:0] } return nil } // Import reads the translations out of a file or directory on disk. // // NOTE: this currently only works with string or int translations keys. func (t *UniversalTranslator) Import(format ImportExportFormat, dirnameOrFilename string) error { fi, err := os.Stat(dirnameOrFilename) if err != nil { return err } processFn := func(filename string) error { f, err := os.Open(filename) if err != nil { return err } defer f.Close() return t.ImportByReader(format, f) } if !fi.IsDir() { return processFn(dirnameOrFilename) } // recursively go through directory walker := func(path string, info os.FileInfo, err error) error { if info.IsDir() { return nil } switch format { case FormatJSON: // skip non JSON files if filepath.Ext(info.Name()) != ".json" { return nil } } return processFn(path) } return filepath.Walk(dirnameOrFilename, walker) } // ImportByReader imports the the translations found within the contents read from the supplied reader. // // NOTE: generally used when assets have been embedded into the binary and are already in memory. func (t *UniversalTranslator) ImportByReader(format ImportExportFormat, reader io.Reader) error { b, err := ioutil.ReadAll(reader) if err != nil { return err } var trans []translation switch format { case FormatJSON: err = json.Unmarshal(b, &trans) } if err != nil { return err } for _, tl := range trans { locale, found := t.FindTranslator(tl.Locale) if !found { return &ErrMissingLocale{locale: tl.Locale} } pr := stringToPR(tl.PluralRule) if pr == locales.PluralRuleUnknown { err = locale.Add(tl.Key, tl.Translation, tl.OverrideExisting) if err != nil { return err } continue } switch tl.PluralType { case cardinalType: err = locale.AddCardinal(tl.Key, tl.Translation, pr, tl.OverrideExisting) case ordinalType: err = locale.AddOrdinal(tl.Key, tl.Translation, pr, tl.OverrideExisting) case rangeType: err = locale.AddRange(tl.Key, tl.Translation, pr, tl.OverrideExisting) default: return &ErrBadPluralDefinition{tl: tl} } if err != nil { return err } } return nil } func stringToPR(s string) locales.PluralRule { switch s { case "One": return locales.PluralRuleOne case "Two": return locales.PluralRuleTwo case "Few": return locales.PluralRuleFew case "Many": return locales.PluralRuleMany case "Other": return locales.PluralRuleOther default: return locales.PluralRuleUnknown } } universal-translator-0.17.0/import_export_test.go 0000664 0000000 0000000 00000046036 13562341400 0022306 0 ustar 00root root 0000000 0000000 package ut import ( "fmt" "path/filepath" "testing" "os" "github.com/go-playground/locales" "github.com/go-playground/locales/en" "github.com/go-playground/locales/nl" ) // NOTES: // - Run "go test" to run tests // - Run "gocov test | gocov report" to report on test converage by file // - Run "gocov test | gocov annotate -" to report on all code and functions, those ,marked with "MISS" were never called // // or // // -- may be a good idea to change to output path to somewherelike /tmp // go test -coverprofile cover.out && go tool cover -html=cover.out -o cover.html // func TestExportImportBasic(t *testing.T) { e := en.New() uni := New(e, e) en, found := uni.GetTranslator("en") // or fallback if fails to find 'en' if !found { t.Fatalf("Expected '%t' Got '%t'", true, found) } translations := []struct { key interface{} trans string expected error expectedError bool override bool }{ { key: "test_trans", trans: "Welcome {0}", expected: nil, }, { key: -1, trans: "Welcome {0}", expected: nil, }, { key: "test_trans2", trans: "{0} to the {1}.", expected: nil, }, { key: "test_trans3", trans: "Welcome {0} to the {1}", expected: nil, }, { key: "test_trans4", trans: "{0}{1}", expected: nil, }, { key: "test_trans", trans: "{0}{1}", expected: &ErrConflictingTranslation{locale: en.Locale(), key: "test_trans", text: "{0}{1}"}, expectedError: true, }, { key: -1, trans: "{0}{1}", expected: &ErrConflictingTranslation{locale: en.Locale(), key: -1, text: "{0}{1}"}, expectedError: true, }, { key: "test_trans", trans: "Welcome {0} to the {1}.", expected: nil, override: true, }, } for _, tt := range translations { err := en.Add(tt.key, tt.trans, tt.override) if err != tt.expected { if !tt.expectedError { t.Errorf("Expected '%s' Got '%s'", tt.expected, err) } else { if err.Error() != tt.expected.Error() { t.Errorf("Expected '%s' Got '%s'", tt.expected.Error(), err.Error()) } } } } dirname := "testdata/translations" defer os.RemoveAll(dirname) err := uni.Export(FormatJSON, dirname) if err != nil { t.Fatalf("Expected '%v' Got '%s'", nil, err) } uni = New(e, e) err = uni.Import(FormatJSON, dirname) if err != nil { t.Fatalf("Expected '%v' Got '%s'", nil, err) } en, found = uni.GetTranslator("en") // or fallback if fails to find 'en' if !found { t.Fatalf("Expected '%t' Got '%t'", true, found) } tests := []struct { key interface{} params []string expected string expectedError bool }{ { key: "test_trans", params: []string{"Joeybloggs", "The Test"}, expected: "Welcome Joeybloggs to the The Test.", }, { key: "test_trans2", params: []string{"Joeybloggs", "The Test"}, expected: "Joeybloggs to the The Test.", }, { key: "test_trans3", params: []string{"Joeybloggs", "The Test"}, expected: "Welcome Joeybloggs to the The Test", }, { key: "test_trans4", params: []string{"Joeybloggs", "The Test"}, expected: "JoeybloggsThe Test", }, // bad translation { key: "non-existant-key", params: []string{"Joeybloggs", "The Test"}, expected: "", expectedError: true, }, } for _, tt := range tests { s, err := en.T(tt.key, tt.params...) if s != tt.expected { if !tt.expectedError || (tt.expectedError && err != ErrUnknowTranslation) { t.Errorf("Expected '%s' Got '%s'", tt.expected, s) } } } } func TestExportImportCardinal(t *testing.T) { e := en.New() uni := New(e, e) en, found := uni.GetTranslator("en") if !found { t.Fatalf("Expected '%t' Got '%t'", true, found) } translations := []struct { key interface{} trans string rule locales.PluralRule expected error expectedError bool override bool }{ // bad translation { key: "cardinal_test", trans: "You have a day left.", rule: locales.PluralRuleOne, expected: &ErrCardinalTranslation{text: fmt.Sprintf("error: parameter '%s' not found, may want to use 'Add' instead of 'AddCardinal'. locale: '%s' key: '%v' text: '%s'", paramZero, en.Locale(), "cardinal_test", "You have a day left.")}, expectedError: true, }, { key: "cardinal_test", trans: "You have {0} day", rule: locales.PluralRuleOne, expected: nil, }, { key: "cardinal_test", trans: "You have {0} days left.", rule: locales.PluralRuleOther, expected: nil, }, { key: "cardinal_test", trans: "You have {0} days left.", rule: locales.PluralRuleOther, expected: &ErrConflictingTranslation{locale: en.Locale(), key: "cardinal_test", rule: locales.PluralRuleOther, text: "You have {0} days left."}, expectedError: true, }, { key: "cardinal_test", trans: "You have {0} day left.", rule: locales.PluralRuleOne, expected: nil, override: true, }, } for _, tt := range translations { err := en.AddCardinal(tt.key, tt.trans, tt.rule, tt.override) if err != tt.expected { if !tt.expectedError || err.Error() != tt.expected.Error() { t.Errorf("Expected '%s' Got '%s'", tt.expected, err) } } } dirname := "testdata/translations" defer os.RemoveAll(dirname) err := uni.Export(FormatJSON, dirname) if err != nil { t.Fatalf("Expected '%v' Got '%s'", nil, err) } uni = New(e, e) err = uni.Import(FormatJSON, dirname) if err != nil { t.Fatalf("Expected '%v' Got '%s'", nil, err) } en, found = uni.GetTranslator("en") // or fallback if fails to find 'en' if !found { t.Fatalf("Expected '%t' Got '%t'", true, found) } tests := []struct { key interface{} num float64 digits uint64 param string expected string expectedError bool }{ { key: "cardinal_test", num: 1, digits: 0, param: string(en.FmtNumber(1, 0)), expected: "You have 1 day left.", }, // bad translation key { key: "non-existant", num: 1, digits: 0, param: string(en.FmtNumber(1, 0)), expected: "", expectedError: true, }, } for _, tt := range tests { s, err := en.C(tt.key, tt.num, tt.digits, tt.param) if err != nil { if !tt.expectedError && err != ErrUnknowTranslation { t.Errorf("Expected '