Browse Source

first commit

master
andrey1s 3 years ago
commit
9129f25c31
  1. 13
      .drone.yml
  2. 17
      .gitignore
  3. 46
      .golangci.yml
  4. 19
      LICENSE
  5. 5
      README.md
  6. 19
      arg/arg.go
  7. 45
      arg/currency.go
  8. 49
      arg/number.go
  9. 27
      context.go
  10. 5
      go.mod
  11. 3
      go.sum
  12. 22
      icu/trans.go
  13. 41
      icu/trans_example_test.go
  14. 108
      provider/gotext/provider.go
  15. 80
      translate.go
  16. 56
      translator.go
  17. 91
      translator_example_test.go
  18. 32
      translator_test.go

13
.drone.yml

@ -0,0 +1,13 @@
kind: pipeline
name: default
steps:
- name: test
image: golang
commands:
- go test -parallel 10 ./...
- name: golangci-lint
image: golangci/golangci-lint:v1.39
commands:
- golangci-lint run

17
.gitignore

@ -0,0 +1,17 @@
# ---> Go
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/

46
.golangci.yml

@ -0,0 +1,46 @@
run:
timeout: 5m
linters-settings:
dupl:
threshold: 100
funlen:
lines: 100
statements: 50
goconst:
min-len: 2
min-occurrences: 2
gocyclo:
min-complexity: 15
golint:
min-confidence: 0
govet:
check-shadowing: true
lll:
line-length: 140
maligned:
suggest-new: true
misspell:
locale: US
linters:
enable-all: true
disable:
- exhaustivestruct
- maligned
- interfacer
- scopelint
issues:
# Excluding configuration per-path, per-linter, per-text and per-source
exclude-rules:
- path: _test\.go
linters:
- gomnd
- exhaustivestruct
- wrapcheck
- path: test/*
linters:
- gomnd
- exhaustivestruct
- wrapcheck

19
LICENSE

@ -0,0 +1,19 @@
MIT License Copyright (c) 2020 4devs
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 (including the next
paragraph) 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.

5
README.md

@ -0,0 +1,5 @@
# translation
[![Build Status](https://drone.gitoa.ru/api/badges/go-4devs/translation/status.svg)](https://drone.gitoa.ru/go-4devs/translation)
[![Go Report Card](https://goreportcard.com/badge/gitoa.ru/go-4devs/translation)](https://goreportcard.com/report/gitoa.ru/go-4devs/translation)
[![GoDoc](https://godoc.org/gitoa.ru/go-4devs/translation?status.svg)](http://godoc.org/gitoa.ru/go-4devs/translation)

19
arg/arg.go

@ -0,0 +1,19 @@
package arg
import "fmt"
// Arg defaults argument.
type Arg struct {
Key string
Value interface{}
}
// String arg to string.
func (a Arg) String() string {
return fmt.Sprintf("key:%s, value:%v", a.Key, a.Value)
}
// Val gets valuet argument.
func (a Arg) Val() interface{} {
return a.Value
}

45
arg/currency.go

@ -0,0 +1,45 @@
package arg
import "fmt"
// FormatCurrency types format.
type FormatCurrency int
// Currency format.
const (
CurrencyFormatSymbol FormatCurrency = iota + 1
CurrencyFormatISO
CurrencyFormatNarrowSymbol
)
// CurrencyOption configures option.
type CurrencyOption func(*Currency)
// WithCurrencyFormat sets format currency.
func WithCurrencyFormat(format FormatCurrency) CurrencyOption {
return func(c *Currency) { c.Format = format }
}
// WithCurrencyISO sets ISO 4217 code currecy.
func WithCurrencyISO(iso string) CurrencyOption {
return func(c *Currency) { c.ISO = iso }
}
// Currency argument.
type Currency struct {
Key string
Value interface{}
Format FormatCurrency
// ISO 3-letter ISO 4217
ISO string
}
// String gets string from currency.
func (a Currency) String() string {
return fmt.Sprintf("currency key:%s, value:%v", a.Key, a.Value)
}
// Val gets value currency.
func (a Currency) Val() interface{} {
return a.Value
}

49
arg/number.go

@ -0,0 +1,49 @@
package arg
import "fmt"
// FormatNumber format number.
type FormatNumber int
// format argument.
const (
NumberFormatDecimal FormatNumber = iota + 1
NumberFormatPercent
NumberFormatPerMille
NumberFormatEngineering
NumberFormatScientific
)
// NumberOption configure number argument.
type NumberOption func(*Number)
// WithNumberFormat sets format number.
func WithNumberFormat(format FormatNumber) NumberOption {
return func(n *Number) { n.Format = format }
}
// Number argument.
type Number struct {
Key string
Value interface{}
Format FormatNumber
}
// Configure number.
func (n Number) Configure(opts ...NumberOption) Number {
for _, o := range opts {
o(&n)
}
return n
}
// Val gets number value.
func (n Number) Val() interface{} {
return n.Value
}
// String number to string.
func (n Number) String() string {
return fmt.Sprintf("number key: %s, value: %v", n.Key, n.Value)
}

27
context.go

@ -0,0 +1,27 @@
package translation
import (
"context"
"golang.org/x/text/language"
)
type ctxkey uint8
const (
localeKey ctxkey = iota
)
// WithLanguage sets language to context.
func WithLanguage(ctx context.Context, lang language.Tag) context.Context {
return context.WithValue(ctx, localeKey, lang)
}
// FromContext get language from context or use default.
func FromContext(ctx context.Context, def language.Tag) language.Tag {
if t, ok := ctx.Value(localeKey).(language.Tag); ok {
return t
}
return def
}

5
go.mod

@ -0,0 +1,5 @@
module gitoa.ru/go-4devs/translation
go 1.16
require golang.org/x/text v0.3.6

3
go.sum

@ -0,0 +1,3 @@
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

22
icu/trans.go

@ -0,0 +1,22 @@
package icu
import (
"context"
"gitoa.ru/go-4devs/translation"
"gitoa.ru/go-4devs/translation/provider/gotext"
"golang.org/x/text/language"
)
// nolint: gochecknoglobals
var trans = translation.New(language.English, gotext.NewProvider())
// Trans translate message.
func Trans(ctx context.Context, key string, opts ...translation.Option) string {
return trans.Trans(ctx, key, opts...)
}
// SetTranslator sets translator.
func SetTranslator(tr *translation.Translator) {
trans = tr
}

41
icu/trans_example_test.go

@ -0,0 +1,41 @@
package icu_test
import (
"context"
"fmt"
"log"
"gitoa.ru/go-4devs/translation"
"gitoa.ru/go-4devs/translation/arg"
"gitoa.ru/go-4devs/translation/icu"
"golang.org/x/text/language"
"golang.org/x/text/message"
"golang.org/x/text/message/catalog"
)
func ExampleTrans_withLanguage() {
err := message.Set(language.Russian, "Hello {city}", catalog.String("Привет %s"))
if err != nil {
log.Fatal(err)
}
err = message.Set(language.Russian, "It costs {cost}", catalog.String("Это стоит %.2f."))
if err != nil {
log.Fatal(err)
}
lang, err := language.Parse("ru")
if err != nil {
log.Fatal(err)
}
ctx := translation.WithLanguage(context.Background(), lang)
tr := icu.Trans(ctx, "Hello {city}", translation.WithArgs("Москва"))
fmt.Println(tr)
tr = icu.Trans(ctx, "It costs {cost}", translation.WithNumber("cost", 1000.00, arg.WithNumberFormat(arg.NumberFormatDecimal)))
fmt.Println(tr)
// Output:
// Привет Москва
// Это стоит 1 000,00.
}

108
provider/gotext/provider.go

@ -0,0 +1,108 @@
package gotext
import (
"context"
"gitoa.ru/go-4devs/translation"
"gitoa.ru/go-4devs/translation/arg"
"golang.org/x/text/currency"
"golang.org/x/text/language"
"golang.org/x/text/message"
"golang.org/x/text/message/catalog"
"golang.org/x/text/number"
)
var _ translation.Provider = (*Provider)(nil)
// Option confires message provider.
type Option func(*Provider)
// WithCatalog set coatalog bu domain name.
func WithCatalog(domain string, cat catalog.Catalog) Option {
return func(mp *Provider) {
mp.catalog[domain] = cat
}
}
// NewProvider creates new messgae provider.
func NewProvider(opts ...Option) *Provider {
mp := &Provider{
catalog: map[string]catalog.Catalog{
translation.DefaultDomain: message.DefaultCatalog,
},
}
for _, o := range opts {
o(mp)
}
return mp
}
// Provider provider messages.
type Provider struct {
catalog map[string]catalog.Catalog
}
// Translate by key and args.
func (mp *Provider) Translate(ctx context.Context, key string, opt translation.Translate) string {
return message.NewPrinter(opt.Locale, message.Catalog(mp.catalog[opt.Domain])).
Sprintf(key, messageArgs(opt.Locale, opt.Args)...)
}
func currencyMessage(lang language.Tag) func(v arg.Currency) interface{} {
return func(v arg.Currency) interface{} {
var u currency.Unit
if v.ISO != "" {
u, _ = currency.ParseISO(v.ISO)
} else {
u, _ = currency.FromTag(lang)
}
switch v.Format {
case arg.CurrencyFormatISO:
return currency.ISO(u.Amount(v.Val()))
case arg.CurrencyFormatSymbol:
return currency.Symbol(u.Amount(v.Val()))
case arg.CurrencyFormatNarrowSymbol:
return currency.NarrowSymbol(u.Amount(v.Val()))
}
return u.Amount(v.Val())
}
}
func numberMessage(v arg.Number) interface{} {
switch v.Format {
case arg.NumberFormatPercent:
return number.Percent(v.Val())
case arg.NumberFormatEngineering:
return number.Engineering(v.Val())
case arg.NumberFormatPerMille:
return number.PerMille(v.Val())
case arg.NumberFormatDecimal:
return number.Decimal(v.Val())
case arg.NumberFormatScientific:
return number.Scientific(v.Val())
}
return v.Val()
}
func messageArgs(lang language.Tag, in []translation.Arg) []interface{} {
out := make([]interface{}, 0, len(in))
for _, a := range in {
switch v := a.(type) {
case arg.Currency:
out = append(out, currencyMessage(lang)(v))
case arg.Number:
out = append(out, numberMessage(v))
default:
out = append(out, v.Val())
}
}
return out
}

80
translate.go

@ -0,0 +1,80 @@
package translation
import (
"fmt"
"gitoa.ru/go-4devs/translation/arg"
"golang.org/x/text/language"
)
// Option configures translate.
type Option func(*Translate)
// Translate configure translate.
type Translate struct {
Domain string
Locale language.Tag
Args []Arg
}
// ArgValues gets value arguments.
func (t *Translate) ArgValues() []interface{} {
a := make([]interface{}, len(t.Args))
for i, v := range t.Args {
a[i] = v.Val()
}
return a
}
// Arg arg translate.
type Arg interface {
Val() interface{}
fmt.Stringer
}
// WithNumber sets number with options.
func WithNumber(key string, val interface{}, opts ...arg.NumberOption) Option {
return func(t *Translate) {
t.Args = append(t.Args, arg.Number{Key: key, Value: val}.Configure(opts...))
}
}
// WithCurrency sets date argument.
func WithCurrency(key string, val interface{}, opts ...arg.CurrencyOption) Option {
return func(t *Translate) {
c := arg.Currency{Value: val, Key: key}
for _, o := range opts {
o(&c)
}
t.Args = append(t.Args, c)
}
}
// WithDomain sets domain translate.
func WithDomain(domain string) Option {
return func(o *Translate) {
o.Domain = domain
}
}
// WithLocale sets locale translate.
func WithLocale(locale string) Option {
return func(o *Translate) {
o.Locale = language.Make(locale)
}
}
// WithArgs sets arguments value.
func WithArgs(vals ...interface{}) Option {
return func(t *Translate) {
args := make([]Arg, len(vals))
for i, val := range vals {
args[i] = arg.Arg{Value: val}
}
t.Args = append(t.Args, args...)
}
}

56
translator.go

@ -0,0 +1,56 @@
package translation
import (
"context"
"golang.org/x/text/language"
)
// Default values.
const (
DefaultDomain = "messages"
)
// Provider for translate key.
type Provider interface {
Translate(ctx context.Context, key string, opt Translate) string
}
// TranslatorOption options translator.
type TranslatorOption func(*Translator)
// New creates new translator.
func New(locale language.Tag, provider Provider, opts ...TranslatorOption) *Translator {
tr := Translator{
locale: locale,
provider: provider,
domain: DefaultDomain,
}
for _, o := range opts {
o(&tr)
}
return &tr
}
// Translator struct.
type Translator struct {
provider Provider
domain string
locale language.Tag
}
// Trans translates key by options.
func (t *Translator) Trans(ctx context.Context, key string, opts ...Option) string {
opt := Translate{
Locale: FromContext(ctx, t.locale),
Domain: t.domain,
}
for _, o := range opts {
o(&opt)
}
return t.provider.Translate(ctx, key, opt)
}

91
translator_example_test.go

@ -0,0 +1,91 @@
package translation_test
import (
"context"
"fmt"
"gitoa.ru/go-4devs/translation"
"gitoa.ru/go-4devs/translation/arg"
"gitoa.ru/go-4devs/translation/provider/gotext"
"golang.org/x/text/feature/plural"
"golang.org/x/text/language"
"golang.org/x/text/message/catalog"
)
func ExampleTranslator_Trans() {
cat := catalog.NewBuilder()
_ = cat.Set(language.Russian, "Hello World!", catalog.String("Привет Мир!"))
_ = cat.Set(language.Slovak, "Hello World!", catalog.String("Ahoj Svet!"))
_ = cat.Set(language.Hebrew, "Hello World!", catalog.String("שלום עולם"))
_ = cat.Set(language.Russian, "Hello {name}!", catalog.String("Привет %[1]s!"))
_ = cat.Set(language.English, "Hello {name}!", catalog.String("Hello %[1]s!"))
_ = cat.Set(language.English, "There are {bikes} bikes per household.", catalog.String("There are %v bikes per household."))
_ = cat.Set(language.Russian, "There are {bikes} bikes per household.", catalog.String("В каждом доме есть %v велосипеда."))
_ = cat.Set(language.English, "You are {minute} minute(s) late.",
plural.Selectf(1, "",
"=0", "You're on time.",
plural.One, "You are %v minute late.",
plural.Other, "You are %v minutes late."))
_ = cat.Set(language.Russian, "You are {minute} minute(s) late.",
plural.Selectf(1, "",
"=1", "Вы опоздали на одну минуту.",
"=0", "Вы вовремя.",
plural.One, "Вы опоздали на %v минуту.",
plural.Few, "Вы опоздали на %v минуты.",
plural.Other, "Вы опоздали на %v минут.",
),
)
_ = cat.Set(language.Russian, "It costs {cost}.",
catalog.String("Это стоит %.2f."))
_ = cat.Set(language.English, "It costs {cost}.",
catalog.String("It costs %.2f."))
provider := gotext.NewProvider(gotext.WithCatalog(translation.DefaultDomain, cat))
trans := translation.New(language.Russian, provider)
ctx := context.Background()
// context with language
heCtx := translation.WithLanguage(ctx, language.Make("he"))
fmt.Println(trans.Trans(ctx, "Hello World!"))
fmt.Println(trans.Trans(ctx, "Hello World!", translation.WithLocale("en")))
fmt.Println(trans.Trans(ctx, "Hello World!", translation.WithLocale("sk")))
fmt.Println(trans.Trans(heCtx, "Hello World!"))
fmt.Println(trans.Trans(ctx, "Hello {name}!", translation.WithArgs("Andrey")))
fmt.Println(trans.Trans(ctx, "You are {minute} minute(s) late.", translation.WithNumber("minute", 1)))
fmt.Println(trans.Trans(ctx, "You are {minute} minute(s) late.", translation.WithNumber("minute", 0)))
fmt.Println(trans.Trans(ctx, "You are {minute} minute(s) late.", translation.WithNumber("minute", 101)))
fmt.Println(trans.Trans(ctx, "You are {minute} minute(s) late.", translation.WithArgs(123456.78)))
fmt.Println(trans.Trans(ctx, "You are {minute} minute(s) late.", translation.WithArgs(50)))
fmt.Println(trans.Trans(ctx, "You are {minute} minute(s) late.", translation.WithArgs(1), translation.WithLocale("en")))
fmt.Println(trans.Trans(ctx, "You are {minute} minute(s) late.", translation.WithArgs(33), translation.WithLocale("en")))
fmt.Println(trans.Trans(ctx, "There are {bikes} bikes per household.", translation.WithNumber("bikes", 1.2)))
fmt.Println(trans.Trans(ctx, "There are {bikes} bikes per household.", translation.WithNumber("bikes", 1.2), translation.WithLocale("en")))
fmt.Println(trans.Trans(ctx, "It costs {cost}.",
translation.WithCurrency("cost", 12.0101, arg.WithCurrencyISO("rub"), arg.WithCurrencyFormat(arg.CurrencyFormatSymbol))))
fmt.Println(trans.Trans(ctx, "It costs {cost}.",
translation.WithCurrency("cost", 15.0, arg.WithCurrencyISO("HKD"), arg.WithCurrencyFormat(arg.CurrencyFormatSymbol)),
translation.WithLocale("en")))
// Output:
// Привет Мир!
// Hello World!
// Ahoj Svet!
// שלום עולם
// Привет Andrey!
// Вы опоздали на одну минуту.
// Вы вовремя.
// Вы опоздали на 101 минуту.
// Вы опоздали на 123 456,78 минут.
// Вы опоздали на 50 минут.
// You are 1 minute late.
// You are 33 minutes late.
// В каждом доме есть 1,2 велосипеда.
// There are 1.2 bikes per household.
// Это стоит ₽ 12.0101.
// It costs HK$ 15.
}

32
translator_test.go

@ -0,0 +1,32 @@
package translation_test
import (
"context"
"fmt"
"testing"
"gitoa.ru/go-4devs/translation"
"golang.org/x/text/language"
)
type TestProvider struct{}
func (tp *TestProvider) Translate(_ context.Context, key string, opt translation.Translate) string {
args := make([]interface{}, 0, len(opt.Args)+1)
args = append(args, key)
args = append(args, opt.ArgValues()...)
return fmt.Sprint(args...)
}
func TestTranslator(t *testing.T) {
t.Parallel()
ctx := context.Background()
trans := translation.New(language.Russian, &TestProvider{})
tr := trans.Trans(ctx, "key", translation.WithArgs("arg1", "arg2"))
if tr != "keyarg1arg2" {
t.Fatalf("expect: keyarg1arg2, got:%s", tr)
}
}
Loading…
Cancel
Save