Update vendored dependencies, added goi18n

pull/153/head
Matthieu Grieger 2016-06-21 09:05:20 -07:00
parent 9e2679bfaa
commit 50c5e6cb61
101 changed files with 6277 additions and 2611 deletions

18
glide.lock generated
View File

@ -1,10 +1,10 @@
hash: 58b946577727dc003cc7efacfff614b6f47602a136e8bf501bd39a18bd3c768c
updated: 2016-06-20T17:29:39.656363943-07:00
hash: 90b4d676820ec357d9eac7be23afe091897035f8e22579f32bef93f9e1971e42
updated: 2016-06-21T09:01:11.596780617-07:00
imports:
- name: github.com/antonholmquist/jason
version: 423803175a265e07c3f35cd8faf009eac345efb8
version: c23cef7eaa75a6a5b8810120e167bd590d8fd2ab
- name: github.com/BurntSushi/toml
version: f0aeabca5a127c4078abb8c8d64298b147264b55
version: bbd5bb678321a0d6e58f1099321dfa73391c1b6f
- name: github.com/ChannelMeter/iso8601duration
version: 8da3af7a2a61a4eb5ae9bddec06bf637fa9593da
- name: github.com/fsnotify/fsnotify
@ -39,8 +39,12 @@ imports:
version: c265cfa48dda6474e208715ca93e987829f572f8
- name: github.com/mitchellh/mapstructure
version: d2dd0262208475919e1a362f675cfc0e7c10e905
- name: github.com/nicksnyder/go-i18n
version: 37e5c2de3e03e4b82693e3fcb4a6aa2cc4eb07e3
subpackages:
- i18n
- name: github.com/Sirupsen/logrus
version: f3cfb454f4c209e6668c95216c4744b8fddb2356
version: 4b6ea7319e214d98c938f12692336f7ca9348d6b
- name: github.com/spf13/cast
version: 27b586b42e29bec072fe7379259cc719e1289da6
- name: github.com/spf13/jwalterweatherman
@ -50,9 +54,9 @@ imports:
- name: github.com/spf13/viper
version: c1ccc378a054ea8d4e38d8c67f6938d4760b53dd
- name: github.com/stretchr/testify
version: d77da356e56a7428ad25149ca77381849a6a5232
version: f390dcf405f7b83c997eac1b06768bb9f44dec18
- name: github.com/urfave/cli
version: 6011f165dc288c72abd8acd7722f837c5c64198d
version: 01857ac33766ce0c93856370626f9799281c14f4
- name: golang.org/x/sys
version: 62bee037599929a6e9146f29d10dd5208c43507d
subpackages:

View File

@ -8,7 +8,9 @@ owners:
import:
- package: github.com/ChannelMeter/iso8601duration
- package: github.com/Sirupsen/logrus
version: v0.10.0
- package: github.com/antonholmquist/jason
version: v1.0.0
- package: github.com/layeh/gumble
subpackages:
- gumble
@ -17,5 +19,12 @@ import:
- opus
- package: github.com/spf13/viper
- package: github.com/urfave/cli
version: v1.17.0
- package: github.com/stretchr/testify
version: v1.1.3
- package: github.com/BurntSushi/toml
version: v0.2.0
- package: github.com/nicksnyder/go-i18n
version: v1.4.0
subpackages:
- i18n

View File

@ -103,13 +103,6 @@ func (md *MetaData) PrimitiveDecode(primValue Primitive, v interface{}) error {
// This decoder will not handle cyclic types. If a cyclic type is passed,
// `Decode` will not terminate.
func Decode(data string, v interface{}) (MetaData, error) {
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Ptr {
return MetaData{}, e("Decode of non-pointer type %s", reflect.TypeOf(v))
}
if rv.IsNil() {
return MetaData{}, e("Decode of nil %s", reflect.TypeOf(v))
}
p, err := parse(data)
if err != nil {
return MetaData{}, err
@ -118,7 +111,7 @@ func Decode(data string, v interface{}) (MetaData, error) {
p.mapping, p.types, p.ordered,
make(map[string]bool, len(p.ordered)), nil,
}
return md, md.unify(p.mapping, indirect(rv))
return md, md.unify(p.mapping, rvalue(v))
}
// DecodeFile is just like Decode, except it will automatically read the

View File

@ -3,13 +3,15 @@ package toml
import (
"fmt"
"log"
"math"
"reflect"
"strings"
"testing"
"time"
)
func init() {
log.SetFlags(0)
}
func TestDecodeSimple(t *testing.T) {
var testSimple = `
age = 250
@ -181,49 +183,6 @@ name = "Born in the USA"
}
}
func TestTableNesting(t *testing.T) {
for _, tt := range []struct {
t string
want []string
}{
{"[a.b.c]", []string{"a", "b", "c"}},
{`[a."b.c"]`, []string{"a", "b.c"}},
{`[a.'b.c']`, []string{"a", "b.c"}},
{`[a.' b ']`, []string{"a", " b "}},
{"[ d.e.f ]", []string{"d", "e", "f"}},
{"[ g . h . i ]", []string{"g", "h", "i"}},
{`[ j . "ʞ" . 'l' ]`, []string{"j", "ʞ", "l"}},
} {
var m map[string]interface{}
if _, err := Decode(tt.t, &m); err != nil {
t.Errorf("Decode(%q): got error: %s", tt.t, err)
continue
}
if keys := extractNestedKeys(m); !reflect.DeepEqual(keys, tt.want) {
t.Errorf("Decode(%q): got nested keys %#v; want %#v",
tt.t, keys, tt.want)
}
}
}
func extractNestedKeys(v map[string]interface{}) []string {
var result []string
for {
if len(v) != 1 {
return result
}
for k, m := range v {
result = append(result, k)
var ok bool
v, ok = m.(map[string]interface{})
if !ok {
return result
}
}
}
}
// Case insensitive matching tests.
// A bit more comprehensive than needed given the current implementation,
// but implementations change.
@ -343,54 +302,14 @@ Description = "da base"
}
}
func TestDecodeDatetime(t *testing.T) {
const noTimestamp = "2006-01-02T15:04:05"
for _, tt := range []struct {
s string
t string
format string
}{
{"1979-05-27T07:32:00Z", "1979-05-27T07:32:00Z", time.RFC3339},
{"1979-05-27T00:32:00-07:00", "1979-05-27T00:32:00-07:00", time.RFC3339},
{
"1979-05-27T00:32:00.999999-07:00",
"1979-05-27T00:32:00.999999-07:00",
time.RFC3339,
},
{"1979-05-27T07:32:00", "1979-05-27T07:32:00", noTimestamp},
{
"1979-05-27T00:32:00.999999",
"1979-05-27T00:32:00.999999",
noTimestamp,
},
{"1979-05-27", "1979-05-27T00:00:00", noTimestamp},
} {
var x struct{ D time.Time }
input := "d = " + tt.s
if _, err := Decode(input, &x); err != nil {
t.Errorf("Decode(%q): got error: %s", input, err)
continue
}
want, err := time.ParseInLocation(tt.format, tt.t, time.Local)
if err != nil {
panic(err)
}
if !x.D.Equal(want) {
t.Errorf("Decode(%q): got %s; want %s", input, x.D, want)
}
func TestDecodeBadTimestamp(t *testing.T) {
var x struct {
T time.Time
}
}
func TestDecodeBadDatetime(t *testing.T) {
var x struct{ T time.Time }
for _, s := range []string{
"123",
"2006-01-50T00:00:00Z",
"2006-01-30T00:00",
"2006-01-30T",
"T = 123", "T = 2006-01-50T00:00:00Z", "T = 2006-01-30T00:00:00",
} {
input := "T = " + s
if _, err := Decode(input, &x); err == nil {
if _, err := Decode(s, &x); err == nil {
t.Errorf("Expected invalid DateTime error for %q", s)
}
}
@ -484,111 +403,6 @@ func TestDecodeSizedInts(t *testing.T) {
}
}
func TestDecodeInts(t *testing.T) {
for _, tt := range []struct {
s string
want int64
}{
{"0", 0},
{"+99", 99},
{"-10", -10},
{"1_234_567", 1234567},
{"1_2_3_4", 1234},
{"-9_223_372_036_854_775_808", math.MinInt64},
{"9_223_372_036_854_775_807", math.MaxInt64},
} {
var x struct{ N int64 }
input := "n = " + tt.s
if _, err := Decode(input, &x); err != nil {
t.Errorf("Decode(%q): got error: %s", input, err)
continue
}
if x.N != tt.want {
t.Errorf("Decode(%q): got %d; want %d", input, x.N, tt.want)
}
}
}
func TestDecodeFloats(t *testing.T) {
for _, tt := range []struct {
s string
want float64
}{
{"+1.0", 1},
{"3.1415", 3.1415},
{"-0.01", -0.01},
{"5e+22", 5e22},
{"1e6", 1e6},
{"-2E-2", -2e-2},
{"6.626e-34", 6.626e-34},
{"9_224_617.445_991_228_313", 9224617.445991228313},
{"9_876.54_32e1_0", 9876.5432e10},
} {
var x struct{ N float64 }
input := "n = " + tt.s
if _, err := Decode(input, &x); err != nil {
t.Errorf("Decode(%q): got error: %s", input, err)
continue
}
if x.N != tt.want {
t.Errorf("Decode(%q): got %d; want %d", input, x.N, tt.want)
}
}
}
func TestDecodeMalformedNumbers(t *testing.T) {
for _, tt := range []struct {
s string
want string
}{
{"++99", "Expected a digit"},
{"0..1", "must be followed by one or more digits"},
{"0.1.2", "Invalid float value"},
{"1e2.3", "Invalid float value"},
{"1e2e3", "Invalid float value"},
{"_123", "Expected value"},
{"123_", "surrounded by digits"},
{"1._23", "surrounded by digits"},
{"1e__23", "surrounded by digits"},
{"123.", "must be followed by one or more digits"},
{"1.e2", "must be followed by one or more digits"},
} {
var x struct{ N interface{} }
input := "n = " + tt.s
_, err := Decode(input, &x)
if err == nil {
t.Errorf("Decode(%q): got nil, want error containing %q",
input, tt.want)
continue
}
if !strings.Contains(err.Error(), tt.want) {
t.Errorf("Decode(%q): got %q, want error containing %q",
input, err, tt.want)
}
}
}
func TestDecodeBadValues(t *testing.T) {
for _, tt := range []struct {
v interface{}
want string
}{
{3, "non-pointer type"},
{(*int)(nil), "nil"},
} {
_, err := Decode(`x = 3`, tt.v)
if err == nil {
t.Errorf("Decode(%v): got nil; want error containing %q",
tt.v, tt.want)
continue
}
if !strings.Contains(err.Error(), tt.want) {
t.Errorf("Decode(%v): got %q; want error containing %q",
tt.v, err, tt.want)
}
}
}
func TestUnmarshaler(t *testing.T) {
var tomlBlob = `
@ -623,7 +437,7 @@ name = "Rice"
`
m := &menu{}
if _, err := Decode(tomlBlob, m); err != nil {
t.Fatal(err)
log.Fatal(err)
}
if len(m.Dishes) != 2 {
@ -659,7 +473,7 @@ name = "Rice"
// test on a value - must be passed as *
o := menu{}
if _, err := Decode(tomlBlob, &o); err != nil {
t.Fatal(err)
log.Fatal(err)
}
}

View File

@ -315,16 +315,10 @@ func (enc *Encoder) eStruct(key Key, rv reflect.Value) {
t := f.Type
switch t.Kind() {
case reflect.Struct:
// Treat anonymous struct fields with
// tag names as though they are not
// anonymous, like encoding/json does.
if getOptions(f.Tag).name == "" {
addFields(t, frv, f.Index)
continue
}
addFields(t, frv, f.Index)
continue
case reflect.Ptr:
if t.Elem().Kind() == reflect.Struct &&
getOptions(f.Tag).name == "" {
if t.Elem().Kind() == reflect.Struct {
if !frv.IsNil() {
addFields(t.Elem(), frv.Elem(), f.Index)
}
@ -353,18 +347,17 @@ func (enc *Encoder) eStruct(key Key, rv reflect.Value) {
continue
}
opts := getOptions(sft.Tag)
if opts.skip {
tag := sft.Tag.Get("toml")
if tag == "-" {
continue
}
keyName := sft.Name
if opts.name != "" {
keyName = opts.name
keyName, opts := getOptions(tag)
if keyName == "" {
keyName = sft.Name
}
if opts.omitempty && isEmpty(sf) {
if _, ok := opts["omitempty"]; ok && isEmpty(sf) {
continue
}
if opts.omitzero && isZero(sf) {
} else if _, ok := opts["omitzero"]; ok && isZero(sf) {
continue
}
@ -458,30 +451,17 @@ func tomlArrayType(rv reflect.Value) tomlType {
return firstType
}
type tagOptions struct {
skip bool // "-"
name string
omitempty bool
omitzero bool
}
func getOptions(tag reflect.StructTag) tagOptions {
t := tag.Get("toml")
if t == "-" {
return tagOptions{skip: true}
}
var opts tagOptions
parts := strings.Split(t, ",")
opts.name = parts[0]
for _, s := range parts[1:] {
switch s {
case "omitempty":
opts.omitempty = true
case "omitzero":
opts.omitzero = true
func getOptions(keyName string) (string, map[string]struct{}) {
opts := make(map[string]struct{})
ss := strings.Split(keyName, ",")
name := ss[0]
if len(ss) > 1 {
for _, opt := range ss {
opts[opt] = struct{}{}
}
}
return opts
return name, opts
}
func isZero(rv reflect.Value) bool {

View File

@ -36,7 +36,7 @@ func TestEncodeRoundTrip(t *testing.T) {
}
var outputs Config
if _, err := Decode(firstBuffer.String(), &outputs); err != nil {
t.Logf("Could not decode:\n-----\n%s\n-----\n",
log.Printf("Could not decode:\n-----\n%s\n-----\n",
firstBuffer.String())
t.Fatal(err)
}
@ -515,44 +515,19 @@ func TestEncodeOmitemptyWithEmptyName(t *testing.T) {
v, expected, nil)
}
func TestEncodeAnonymousStruct(t *testing.T) {
type Inner struct{ N int }
type Outer0 struct{ Inner }
type Outer1 struct {
Inner `toml:"inner"`
}
v0 := Outer0{Inner{3}}
expected := "N = 3\n"
encodeExpected(t, "embedded anonymous untagged struct", v0, expected, nil)
v1 := Outer1{Inner{3}}
expected = "[inner]\n N = 3\n"
encodeExpected(t, "embedded anonymous tagged struct", v1, expected, nil)
}
func TestEncodeAnonymousStructPointerField(t *testing.T) {
type Inner struct{ N int }
type Outer0 struct{ *Inner }
type Outer1 struct {
*Inner `toml:"inner"`
type Sub struct{}
type simple struct {
*Sub
}
v0 := Outer0{}
value := simple{}
expected := ""
encodeExpected(t, "nil anonymous untagged struct pointer field", v0, expected, nil)
encodeExpected(t, "nil anonymous struct pointer field", value, expected, nil)
v0 = Outer0{&Inner{3}}
expected = "N = 3\n"
encodeExpected(t, "non-nil anonymous untagged struct pointer field", v0, expected, nil)
v1 := Outer1{}
value = simple{Sub: &Sub{}}
expected = ""
encodeExpected(t, "nil anonymous tagged struct pointer field", v1, expected, nil)
v1 = Outer1{&Inner{3}}
expected = "[inner]\n N = 3\n"
encodeExpected(t, "non-nil anonymous tagged struct pointer field", v1, expected, nil)
encodeExpected(t, "non-nil anonymous struct pointer field", value, expected, nil)
}
func TestEncodeIgnoredFields(t *testing.T) {

View File

@ -3,7 +3,6 @@ package toml
import (
"fmt"
"strings"
"unicode"
"unicode/utf8"
)
@ -167,19 +166,6 @@ func (lx *lexer) peek() rune {
return r
}
// skip ignores all input that matches the given predicate.
func (lx *lexer) skip(pred func(rune) bool) {
for {
r := lx.next()
if pred(r) {
continue
}
lx.backup()
lx.ignore()
return
}
}
// errorf stops all lexing by emitting an error and returning `nil`.
// Note that any value that is a character is escaped if it's a special
// character (new lines, tabs, etc.).
@ -275,7 +261,6 @@ func lexArrayTableEnd(lx *lexer) stateFn {
}
func lexTableNameStart(lx *lexer) stateFn {
lx.skip(isWhitespace)
switch r := lx.peek(); {
case r == tableEnd || r == eof:
return lx.errorf("Unexpected end of table name. (Table names cannot " +
@ -292,22 +277,24 @@ func lexTableNameStart(lx *lexer) stateFn {
}
}
// lexBareTableName lexes the name of a table. It assumes that at least one
// lexTableName lexes the name of a table. It assumes that at least one
// valid character for the table has already been read.
func lexBareTableName(lx *lexer) stateFn {
r := lx.next()
if isBareKeyChar(r) {
switch r := lx.next(); {
case isBareKeyChar(r):
return lexBareTableName
case r == tableSep || r == tableEnd:
lx.backup()
lx.emitTrim(itemText)
return lexTableNameEnd
default:
return lx.errorf("Bare keys cannot contain %q.", r)
}
lx.backup()
lx.emit(itemText)
return lexTableNameEnd
}
// lexTableNameEnd reads the end of a piece of a table name, optionally
// consuming whitespace.
func lexTableNameEnd(lx *lexer) stateFn {
lx.skip(isWhitespace)
switch r := lx.next(); {
case isWhitespace(r):
return lexTableNameEnd
@ -351,12 +338,11 @@ func lexBareKey(lx *lexer) stateFn {
case isBareKeyChar(r):
return lexBareKey
case isWhitespace(r):
lx.backup()
lx.emit(itemText)
lx.emitTrim(itemText)
return lexKeyEnd
case r == keySep:
lx.backup()
lx.emit(itemText)
lx.emitTrim(itemText)
return lexKeyEnd
default:
return lx.errorf("Bare keys cannot contain %q.", r)
@ -385,19 +371,16 @@ func lexValue(lx *lexer) stateFn {
// In array syntax, the array states are responsible for ignoring new
// lines.
r := lx.next()
switch {
case isWhitespace(r):
if isWhitespace(r) {
return lexSkip(lx, lexValue)
case isDigit(r):
lx.backup() // avoid an extra state and use the same as above
return lexNumberOrDateStart
}
switch r {
case arrayStart:
switch {
case r == arrayStart:
lx.ignore()
lx.emit(itemArray)
return lexArrayValue
case stringStart:
case r == stringStart:
if lx.accept(stringStart) {
if lx.accept(stringStart) {
lx.ignore() // Ignore """
@ -407,7 +390,7 @@ func lexValue(lx *lexer) stateFn {
}
lx.ignore() // ignore the '"'
return lexString
case rawStringStart:
case r == rawStringStart:
if lx.accept(rawStringStart) {
if lx.accept(rawStringStart) {
lx.ignore() // Ignore """
@ -417,19 +400,18 @@ func lexValue(lx *lexer) stateFn {
}
lx.ignore() // ignore the "'"
return lexRawString
case '+', '-':
case r == 't':
return lexTrue
case r == 'f':
return lexFalse
case r == '-':
return lexNumberStart
case '.': // special error case, be kind to users
case isDigit(r):
lx.backup() // avoid an extra state and use the same as above
return lexNumberOrDateStart
case r == '.': // special error case, be kind to users
return lx.errorf("Floats must start with a digit, not '.'.")
}
if unicode.IsLetter(r) {
// Be permissive here; lexBool will give a nice error if the
// user wrote something like
// x = foo
// (i.e. not 'true' or 'false' but is something else word-like.)
lx.backup()
return lexBool
}
return lx.errorf("Expected value but found %q instead.", r)
}
@ -636,36 +618,33 @@ func lexLongUnicodeEscape(lx *lexer) stateFn {
return lx.pop()
}
// lexNumberOrDateStart consumes either an integer, a float, or datetime.
// lexNumberOrDateStart consumes either a (positive) integer, float or
// datetime. It assumes that NO negative sign has been consumed.
func lexNumberOrDateStart(lx *lexer) stateFn {
r := lx.next()
if isDigit(r) {
return lexNumberOrDate
if !isDigit(r) {
if r == '.' {
return lx.errorf("Floats must start with a digit, not '.'.")
} else {
return lx.errorf("Expected a digit but got %q.", r)
}
}
switch r {
case '_':
return lexNumber
case 'e', 'E':
return lexFloat
case '.':
return lx.errorf("Floats must start with a digit, not '.'.")
}
return lx.errorf("Expected a digit but got %q.", r)
return lexNumberOrDate
}
// lexNumberOrDate consumes either an integer, float or datetime.
// lexNumberOrDate consumes either a (positive) integer, float or datetime.
func lexNumberOrDate(lx *lexer) stateFn {
r := lx.next()
if isDigit(r) {
switch {
case r == '-':
if lx.pos-lx.start != 5 {
return lx.errorf("All ISO8601 dates must be in full Zulu form.")
}
return lexDateAfterYear
case isDigit(r):
return lexNumberOrDate
}
switch r {
case '-':
return lexDatetime
case '_':
return lexNumber
case '.', 'e', 'E':
return lexFloat
case r == '.':
return lexFloatStart
}
lx.backup()
@ -673,28 +652,39 @@ func lexNumberOrDate(lx *lexer) stateFn {
return lx.pop()
}
// lexDatetime consumes a Datetime, to a first approximation.
// The parser validates that it matches one of the accepted formats.
func lexDatetime(lx *lexer) stateFn {
r := lx.next()
if isDigit(r) {
return lexDatetime
// lexDateAfterYear consumes a full Zulu Datetime in ISO8601 format.
// It assumes that "YYYY-" has already been consumed.
func lexDateAfterYear(lx *lexer) stateFn {
formats := []rune{
// digits are '0'.
// everything else is direct equality.
'0', '0', '-', '0', '0',
'T',
'0', '0', ':', '0', '0', ':', '0', '0',
'Z',
}
switch r {
case '-', 'T', ':', '.', 'Z':
return lexDatetime
for _, f := range formats {
r := lx.next()
if f == '0' {
if !isDigit(r) {
return lx.errorf("Expected digit in ISO8601 datetime, "+
"but found %q instead.", r)
}
} else if f != r {
return lx.errorf("Expected %q in ISO8601 datetime, "+
"but found %q instead.", f, r)
}
}
lx.backup()
lx.emit(itemDatetime)
return lx.pop()
}
// lexNumberStart consumes either an integer or a float. It assumes that a sign
// has already been read, but that *no* digits have been consumed.
// lexNumberStart will move to the appropriate integer or float states.
// lexNumberStart consumes either an integer or a float. It assumes that
// a negative sign has already been read, but that *no* digits have been
// consumed. lexNumberStart will move to the appropriate integer or float
// states.
func lexNumberStart(lx *lexer) stateFn {
// We MUST see a digit. Even floats have to start with a digit.
// we MUST see a digit. Even floats have to start with a digit.
r := lx.next()
if !isDigit(r) {
if r == '.' {
@ -709,14 +699,11 @@ func lexNumberStart(lx *lexer) stateFn {
// lexNumber consumes an integer or a float after seeing the first digit.
func lexNumber(lx *lexer) stateFn {
r := lx.next()
if isDigit(r) {
switch {
case isDigit(r):
return lexNumber
}
switch r {
case '_':
return lexNumber
case '.', 'e', 'E':
return lexFloat
case r == '.':
return lexFloatStart
}
lx.backup()
@ -724,42 +711,60 @@ func lexNumber(lx *lexer) stateFn {
return lx.pop()
}
// lexFloat consumes the elements of a float. It allows any sequence of
// float-like characters, so floats emitted by the lexer are only a first
// approximation and must be validated by the parser.
// lexFloatStart starts the consumption of digits of a float after a '.'.
// Namely, at least one digit is required.
func lexFloatStart(lx *lexer) stateFn {
r := lx.next()
if !isDigit(r) {
return lx.errorf("Floats must have a digit after the '.', but got "+
"%q instead.", r)
}
return lexFloat
}
// lexFloat consumes the digits of a float after a '.'.
// Assumes that one digit has been consumed after a '.' already.
func lexFloat(lx *lexer) stateFn {
r := lx.next()
if isDigit(r) {
return lexFloat
}
switch r {
case '_', '.', '-', '+', 'e', 'E':
return lexFloat
}
lx.backup()
lx.emit(itemFloat)
return lx.pop()
}
// lexBool consumes a bool string: 'true' or 'false.
func lexBool(lx *lexer) stateFn {
var rs []rune
for {
r := lx.next()
if r == eof || isWhitespace(r) || isNL(r) {
lx.backup()
break
// lexConst consumes the s[1:] in s. It assumes that s[0] has already been
// consumed.
func lexConst(lx *lexer, s string) stateFn {
for i := range s[1:] {
if r := lx.next(); r != rune(s[i+1]) {
return lx.errorf("Expected %q, but found %q instead.", s[:i+1],
s[:i]+string(r))
}
rs = append(rs, r)
}
s := string(rs)
switch s {
case "true", "false":
lx.emit(itemBool)
return lx.pop()
return nil
}
// lexTrue consumes the "rue" in "true". It assumes that 't' has already
// been consumed.
func lexTrue(lx *lexer) stateFn {
if fn := lexConst(lx, "true"); fn != nil {
return fn
}
return lx.errorf("Expected value but found %q instead.", s)
lx.emit(itemBool)
return lx.pop()
}
// lexFalse consumes the "alse" in "false". It assumes that 'f' has already
// been consumed.
func lexFalse(lx *lexer) stateFn {
if fn := lexConst(lx, "false"); fn != nil {
return fn
}
lx.emit(itemBool)
return lx.pop()
}
// lexCommentStart begins the lexing of a comment. It will emit

View File

@ -2,6 +2,7 @@ package toml
import (
"fmt"
"log"
"strconv"
"strings"
"time"
@ -80,7 +81,7 @@ func (p *parser) next() item {
}
func (p *parser) bug(format string, v ...interface{}) {
panic(fmt.Sprintf("BUG: "+format+"\n\n", v...))
log.Panicf("BUG: %s\n\n", fmt.Sprintf(format, v...))
}
func (p *parser) expect(typ itemType) item {
@ -178,18 +179,10 @@ func (p *parser) value(it item) (interface{}, tomlType) {
}
p.bug("Expected boolean value, but got '%s'.", it.val)
case itemInteger:
if !numUnderscoresOK(it.val) {
p.panicf("Invalid integer %q: underscores must be surrounded by digits",
it.val)
}
val := strings.Replace(it.val, "_", "", -1)
num, err := strconv.ParseInt(val, 10, 64)
num, err := strconv.ParseInt(it.val, 10, 64)
if err != nil {
// Distinguish integer values. Normally, it'd be a bug if the lexer
// provides an invalid integer, but it's possible that the number is
// out of range of valid values (which the lexer cannot determine).
// So mark the former as a bug but the latter as a legitimate user
// error.
// See comment below for floats describing why we make a
// distinction between a bug and a user error.
if e, ok := err.(*strconv.NumError); ok &&
e.Err == strconv.ErrRange {
@ -201,57 +194,29 @@ func (p *parser) value(it item) (interface{}, tomlType) {
}
return num, p.typeOfPrimitive(it)
case itemFloat:
parts := strings.FieldsFunc(it.val, func(r rune) bool {
switch r {
case '.', 'e', 'E':
return true
}
return false
})
for _, part := range parts {
if !numUnderscoresOK(part) {
p.panicf("Invalid float %q: underscores must be "+
"surrounded by digits", it.val)
}
}
if !numPeriodsOK(it.val) {
// As a special case, numbers like '123.' or '1.e2',
// which are valid as far as Go/strconv are concerned,
// must be rejected because TOML says that a fractional
// part consists of '.' followed by 1+ digits.
p.panicf("Invalid float %q: '.' must be followed "+
"by one or more digits", it.val)
}
val := strings.Replace(it.val, "_", "", -1)
num, err := strconv.ParseFloat(val, 64)
num, err := strconv.ParseFloat(it.val, 64)
if err != nil {
// Distinguish float values. Normally, it'd be a bug if the lexer
// provides an invalid float, but it's possible that the float is
// out of range of valid values (which the lexer cannot determine).
// So mark the former as a bug but the latter as a legitimate user
// error.
//
// This is also true for integers.
if e, ok := err.(*strconv.NumError); ok &&
e.Err == strconv.ErrRange {
p.panicf("Float '%s' is out of the range of 64-bit "+
"IEEE-754 floating-point numbers.", it.val)
} else {
p.panicf("Invalid float value: %q", it.val)
p.bug("Expected float value, but got '%s'.", it.val)
}
}
return num, p.typeOfPrimitive(it)
case itemDatetime:
var t time.Time
var ok bool
var err error
for _, format := range []string{
"2006-01-02T15:04:05Z07:00",
"2006-01-02T15:04:05",
"2006-01-02",
} {
t, err = time.ParseInLocation(format, it.val, time.Local)
if err == nil {
ok = true
break
}
}
if !ok {
p.panicf("Invalid TOML Datetime: %q.", it.val)
t, err := time.Parse("2006-01-02T15:04:05Z", it.val)
if err != nil {
p.panicf("Invalid RFC3339 Zulu DateTime: '%s'.", it.val)
}
return t, p.typeOfPrimitive(it)
case itemArray:
@ -274,35 +239,6 @@ func (p *parser) value(it item) (interface{}, tomlType) {
panic("unreachable")
}
// numUnderscoresOK checks whether each underscore in s is surrounded by
// characters that are not underscores.
func numUnderscoresOK(s string) bool {
accept := false
for _, r := range s {
if r == '_' {
if !accept {
return false
}
accept = false
continue
}
accept = true
}
return accept
}
// numPeriodsOK checks whether every period in s is followed by a digit.
func numPeriodsOK(s string) bool {
period := false
for _, r := range s {
if period && !isDigit(r) {
return false
}
period = r == '.'
}
return !period
}
// establishContext sets the current context of the parser,
// where the context is either a hash or an array of hashes. Which one is
// set depends on the value of the `array` parameter.

View File

@ -95,8 +95,8 @@ func typeFields(t reflect.Type) []field {
if sf.PkgPath != "" && !sf.Anonymous { // unexported
continue
}
opts := getOptions(sf.Tag)
if opts.skip {
name, _ := getOptions(sf.Tag.Get("toml"))
if name == "-" {
continue
}
index := make([]int, len(f.index)+1)
@ -110,9 +110,8 @@ func typeFields(t reflect.Type) []field {
}
// Record found field and index sequence.
if opts.name != "" || !sf.Anonymous || ft.Kind() != reflect.Struct {
tagged := opts.name != ""
name := opts.name
if name != "" || !sf.Anonymous || ft.Kind() != reflect.Struct {
tagged := name != ""
if name == "" {
name = sf.Name
}

View File

@ -3,7 +3,6 @@ go:
- 1.3
- 1.4
- 1.5
- 1.6
- tip
install:
- go get -t ./...

View File

@ -218,7 +218,6 @@ Note: Syslog hook also support connecting to local syslog (Ex. "/dev/log" or "/v
| [Rollrus](https://github.com/heroku/rollrus) | Hook for sending errors to rollbar |
| [Fluentd](https://github.com/evalphobia/logrus_fluent) | Hook for logging to fluentd |
| [Mongodb](https://github.com/weekface/mgorus) | Hook for logging to mongodb |
| [Influxus] (http://github.com/vlad-doru/influxus) | Hook for concurrently logging to [InfluxDB] (http://influxdata.com/) |
| [InfluxDB](https://github.com/Abramovic/logrus_influxdb) | Hook for logging to influxdb |
| [Octokit](https://github.com/dorajistyle/logrus-octokit-hook) | Hook for logging to github via octokit |
| [DeferPanic](https://github.com/deferpanic/dp-logrus) | Hook for logging to DeferPanic |
@ -227,8 +226,7 @@ Note: Syslog hook also support connecting to local syslog (Ex. "/dev/log" or "/v
| [KafkaLogrus](https://github.com/goibibo/KafkaLogrus) | Hook for logging to kafka |
| [Typetalk](https://github.com/dragon3/logrus-typetalk-hook) | Hook for logging to [Typetalk](https://www.typetalk.in/) |
| [ElasticSearch](https://github.com/sohlich/elogrus) | Hook for logging to ElasticSearch|
| [Sumorus](https://github.com/doublefree/sumorus) | Hook for logging to [SumoLogic](https://www.sumologic.com/)|
| [Logstash](https://github.com/bshuster-repo/logrus-logstash-hook) | Hook for logging to [Logstash](https://www.elastic.co/products/logstash) |
#### Level logging

View File

@ -31,15 +31,18 @@ type Formatter interface {
// It's not exported because it's still using Data in an opinionated way. It's to
// avoid code duplication between the two default formatters.
func prefixFieldClashes(data Fields) {
if t, ok := data["time"]; ok {
data["fields.time"] = t
_, ok := data["time"]
if ok {
data["fields.time"] = data["time"]
}
if m, ok := data["msg"]; ok {
data["fields.msg"] = m
_, ok = data["msg"]
if ok {
data["fields.msg"] = data["msg"]
}
if l, ok := data["level"]; ok {
data["fields.level"] = l
_, ok = data["level"]
if ok {
data["fields.level"] = data["level"]
}
}

View File

@ -24,13 +24,11 @@ func (f *LogstashFormatter) Format(entry *logrus.Entry) ([]byte, error) {
fields["@version"] = 1
timeStampFormat := f.TimestampFormat
if timeStampFormat == "" {
timeStampFormat = logrus.DefaultTimestampFormat
if f.TimestampFormat == "" {
f.TimestampFormat = logrus.DefaultTimestampFormat
}
fields["@timestamp"] = entry.Time.Format(timeStampFormat)
fields["@timestamp"] = entry.Time.Format(f.TimestampFormat)
// set message field
v, ok := entry.Data["message"]

View File

@ -128,10 +128,10 @@ func needsQuoting(text string) bool {
(ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') ||
ch == '-' || ch == '.') {
return true
return false
}
}
return false
return true
}
func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}) {
@ -141,14 +141,14 @@ func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interf
switch value := value.(type) {
case string:
if !needsQuoting(value) {
if needsQuoting(value) {
b.WriteString(value)
} else {
fmt.Fprintf(b, "%q", value)
}
case error:
errmsg := value.Error()
if !needsQuoting(errmsg) {
if needsQuoting(errmsg) {
b.WriteString(errmsg)
} else {
fmt.Fprintf(b, "%q", value)

View File

@ -7,40 +7,18 @@ import (
)
func (logger *Logger) Writer() *io.PipeWriter {
return logger.WriterLevel(InfoLevel)
}
func (logger *Logger) WriterLevel(level Level) *io.PipeWriter {
reader, writer := io.Pipe()
var printFunc func(args ...interface{})
switch level {
case DebugLevel:
printFunc = logger.Debug
case InfoLevel:
printFunc = logger.Info
case WarnLevel:
printFunc = logger.Warn
case ErrorLevel:
printFunc = logger.Error
case FatalLevel:
printFunc = logger.Fatal
case PanicLevel:
printFunc = logger.Panic
default:
printFunc = logger.Print
}
go logger.writerScanner(reader, printFunc)
go logger.writerScanner(reader)
runtime.SetFinalizer(writer, writerFinalizer)
return writer
}
func (logger *Logger) writerScanner(reader *io.PipeReader, printFunc func(args ...interface{})) {
func (logger *Logger) writerScanner(reader *io.PipeReader) {
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
printFunc(scanner.Text())
logger.Print(scanner.Text())
}
if err := scanner.Err(); err != nil {
logger.Errorf("Error while reading from Writer: %s", err)

View File

@ -45,7 +45,7 @@ v, err := jason.NewObjectFromBytes(b)
```
If the root object is unknown or not an object, use `NewValueFromBytes` instead. It can then be typecasted using one of the conversion methods provided by the library, for instance `Array()` or `String()`.
If the root object is not an array, use this method instead. It can then be cased to the expected type with one of the As-Methods.
```go
v, err := jason.NewValueFromBytes(b)
@ -174,7 +174,7 @@ func main() {
## Documentation
Documentation can be found on godoc:
Documentation can be found a godoc:
https://godoc.org/github.com/antonholmquist/jason
@ -191,7 +191,7 @@ Go 1.1 and up.
## Where does the name come from?
I remembered it from an email one of our projects managers sent a couple of years ago.
I remebered it from an email one of our projects managers sent a couple of years ago.
> "Don't worry. We can handle both XML and Jason"

View File

@ -50,33 +50,9 @@ import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
)
// Error values returned when validation functions fail
var (
ErrNotNull = errors.New("is not null")
ErrNotArray = errors.New("Not an array")
ErrNotNumber = errors.New("not a number")
ErrNotBool = errors.New("no bool")
ErrNotObject = errors.New("not an object")
ErrNotObjectArray = errors.New("not an object array")
ErrNotString = errors.New("not a string")
)
type KeyNotFoundError struct {
Key string
}
func (k KeyNotFoundError) Error() string {
if k.Key != "" {
return fmt.Sprintf("key '%s' not found", k.Key)
}
return "key not found"
}
// Value represents an arbitrary JSON value.
// It may contain a bool, number, string, object, array or null.
type Value struct {
@ -145,11 +121,6 @@ func (v *Value) Marshal() ([]byte, error) {
return json.Marshal(v.data)
}
// Get the interyling data as interface
func (v *Value) Interface() interface{} {
return v.data
}
// Private Get
func (v *Value) get(key string) (*Value, error) {
@ -161,7 +132,7 @@ func (v *Value) get(key string) (*Value, error) {
if ok {
return child, nil
} else {
return nil, KeyNotFoundError{key}
return nil, errors.New("key not found")
}
}
@ -211,6 +182,8 @@ func (v *Object) GetObject(keys ...string) (*Object, error) {
}
}
return nil, nil
}
// Gets the value at key path and attempts to typecast the value into a string.
@ -225,6 +198,8 @@ func (v *Object) GetString(keys ...string) (string, error) {
} else {
return child.String()
}
return "", nil
}
// Gets the value at key path and attempts to typecast the value into null.
@ -260,6 +235,8 @@ func (v *Object) GetNumber(keys ...string) (json.Number, error) {
return n, nil
}
}
return "", nil
}
// Gets the value at key path and attempts to typecast the value into a float64.
@ -281,6 +258,8 @@ func (v *Object) GetFloat64(keys ...string) (float64, error) {
return n, nil
}
}
return 0, nil
}
// Gets the value at key path and attempts to typecast the value into a float64.
@ -302,20 +281,8 @@ func (v *Object) GetInt64(keys ...string) (int64, error) {
return n, nil
}
}
}
// Gets the value at key path and attempts to typecast the value into a float64.
// Returns error if the value is not a json number.
// Example:
// v, err := GetInterface("address", "anything")
func (v *Object) GetInterface(keys ...string) (interface{}, error) {
child, err := v.getPath(keys)
if err != nil {
return nil, err
} else {
return child.Interface(), nil
}
return 0, nil
}
// Gets the value at key path and attempts to typecast the value into a bool.
@ -350,6 +317,8 @@ func (v *Object) GetValueArray(keys ...string) ([]*Value, error) {
return child.Array()
}
return nil, nil
}
// Gets the value at key path and attempts to typecast the value into an array of objects.
@ -388,6 +357,8 @@ func (v *Object) GetObjectArray(keys ...string) ([]*Object, error) {
return typedArray, nil
}
}
return nil, nil
}
// Gets the value at key path and attempts to typecast the value into an array of string.
@ -427,6 +398,7 @@ func (v *Object) GetStringArray(keys ...string) ([]string, error) {
return typedArray, nil
}
}
return nil, nil
}
// Gets the value at key path and attempts to typecast the value into an array of numbers.
@ -464,6 +436,7 @@ func (v *Object) GetNumberArray(keys ...string) ([]json.Number, error) {
return typedArray, nil
}
}
return nil, nil
}
// Gets the value at key path and attempts to typecast the value into an array of floats.
@ -496,6 +469,7 @@ func (v *Object) GetFloat64Array(keys ...string) ([]float64, error) {
return typedArray, nil
}
}
return nil, nil
}
// Gets the value at key path and attempts to typecast the value into an array of ints.
@ -528,6 +502,7 @@ func (v *Object) GetInt64Array(keys ...string) ([]int64, error) {
return typedArray, nil
}
}
return nil, nil
}
// Gets the value at key path and attempts to typecast the value into an array of bools.
@ -560,6 +535,7 @@ func (v *Object) GetBooleanArray(keys ...string) ([]bool, error) {
return typedArray, nil
}
}
return nil, nil
}
// Gets the value at key path and attempts to typecast the value into an array of nulls.
@ -592,6 +568,8 @@ func (v *Object) GetNullArray(keys ...string) (int64, error) {
return length, nil
}
}
return 0, nil
}
// Returns an error if the value is not actually null
@ -609,7 +587,7 @@ func (v *Value) Null() error {
return nil
}
return ErrNotNull
return errors.New("is not null")
}
@ -640,7 +618,7 @@ func (v *Value) Array() ([]*Value, error) {
return slice, nil
}
return slice, ErrNotArray
return slice, errors.New("Not an array")
}
@ -662,7 +640,7 @@ func (v *Value) Number() (json.Number, error) {
return v.data.(json.Number), nil
}
return "", ErrNotNumber
return "", errors.New("not a number")
}
// Attempts to typecast the current value into a float64.
@ -711,7 +689,7 @@ func (v *Value) Boolean() (bool, error) {
return v.data.(bool), nil
}
return false, ErrNotBool
return false, errors.New("no bool")
}
// Attempts to typecast the current value into an object.
@ -736,6 +714,7 @@ func (v *Value) Object() (*Object, error) {
m := make(map[string]*Value)
if valid {
for key, element := range v.data.(map[string]interface{}) {
m[key] = &Value{element, true}
@ -748,44 +727,7 @@ func (v *Value) Object() (*Object, error) {
return obj, nil
}
return nil, ErrNotObject
}
// Attempts to typecast the current value into an object arrau.
// Returns error if the current value is not an array of json objects
// Example:
// friendObjects, err := friendValues.ObjectArray()
func (v *Value) ObjectArray() ([]*Object, error) {
var valid bool
// Check the type of this data
switch v.data.(type) {
case []interface{}:
valid = true
break
}
// Unsure if this is a good way to use slices, it's probably not
var slice []*Object
if valid {
for _, element := range v.data.([]interface{}) {
childValue := Value{element, true}
childObject, err := childValue.Object()
if err != nil {
return nil, ErrNotObjectArray
}
slice = append(slice, childObject)
}
return slice, nil
}
return nil, ErrNotObjectArray
return nil, errors.New("not an object")
}
// Attempts to typecast the current value into a string.
@ -806,7 +748,7 @@ func (v *Value) String() (string, error) {
return v.data.(string), nil
}
return "", ErrNotString
return "", errors.New("not a string")
}
// Returns the value a json formatted string.

View File

@ -73,19 +73,12 @@ func TestFirst(t *testing.T) {
//log.Println("s: ", s.String())
_, err = j.GetNumber("age")
log.Println("error: ", err)
assert.True(err == nil, "age should be a number")
n, err := j.GetInt64("age")
assert.True(n == 29 && err == nil, "age mismatch")
ageInterface, err := j.GetInterface("age")
assert.True(ageInterface != nil, "should be defined")
assert.True(err == nil, "age interface error")
invalidInterface, err := j.GetInterface("not_existing")
assert.True(invalidInterface == nil, "should not give error here")
assert.True(err != nil, "should give error here")
age, err := j.GetValue("age")
assert.True(age != nil && err == nil, "age should exist")
@ -127,13 +120,6 @@ func TestFirst(t *testing.T) {
assert.True(err == nil, "List2 should not return error on AsArray")
assert.True(len(list2Array) == 2, "List2 should should have length 2")
list2Value, err := j.GetValue("list2")
assert.True(err == nil, "List2 should not return error on value")
list2ObjectArray, err := list2Value.ObjectArray()
assert.True(err == nil, "list2Value should not return error on ObjectArray")
assert.True(len(list2ObjectArray) == 2, "list2ObjectArray should should have length 2")
for _, elementValue := range list2Array {
//assert.True(element.IsObject() == true, "first fail")
@ -243,53 +229,3 @@ func TestSecond(t *testing.T) {
}
}
func TestErrors(t *testing.T) {
json := `
{
"string": "hello",
"number": 1,
"array": [1,2,3]
}`
errstr := "expected an error getting %s, but got '%s'"
j, err := NewObjectFromBytes([]byte(json))
if err != nil {
t.Fatal("failed to parse json")
}
if _, err = j.GetObject("string"); err != ErrNotObject {
t.Errorf(errstr, "object", err)
}
if err = j.GetNull("string"); err != ErrNotNull {
t.Errorf(errstr, "null", err)
}
if _, err = j.GetStringArray("string"); err != ErrNotArray {
t.Errorf(errstr, "array", err)
}
if _, err = j.GetStringArray("array"); err != ErrNotString {
t.Errorf(errstr, "string array", err)
}
if _, err = j.GetNumber("array"); err != ErrNotNumber {
t.Errorf(errstr, "number", err)
}
if _, err = j.GetBoolean("array"); err != ErrNotBool {
t.Errorf(errstr, "boolean", err)
}
if _, err = j.GetString("number"); err != ErrNotString {
t.Errorf(errstr, "string", err)
}
_, err = j.GetString("not_found")
if e, ok := err.(KeyNotFoundError); !ok {
t.Errorf(errstr, "key not found error", e)
}
}

6
vendor/github.com/nicksnyder/go-i18n/.gitignore generated vendored Normal file
View File

@ -0,0 +1,6 @@
*.a
_*
output/
.DS_Store
*.test
*.swp

8
vendor/github.com/nicksnyder/go-i18n/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,8 @@
language: go
sudo: false
go:
- 1.2
- 1.3
- 1.4
- 1.5
- tip

5
vendor/github.com/nicksnyder/go-i18n/CHANGELOG generated vendored Normal file
View File

@ -0,0 +1,5 @@
Feb 24, 2015
- Add Korean
Feb 18, 2015
- Added ParseTranslationFileBytes so translation files may be loaded from an arbitrary serialization format, such as go-bindata.

19
vendor/github.com/nicksnyder/go-i18n/LICENSE generated vendored Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2014 Nick Snyder https://github.com/nicksnyder
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.

130
vendor/github.com/nicksnyder/go-i18n/README.md generated vendored Normal file
View File

@ -0,0 +1,130 @@
go-i18n [![Build Status](https://secure.travis-ci.org/nicksnyder/go-i18n.png?branch=master)](http://travis-ci.org/nicksnyder/go-i18n)
=======
go-i18n is a Go [package](#i18n-package) and a [command](#goi18n-command) that helps you translate Go programs into multiple languages.
* Supports [pluralized strings](http://cldr.unicode.org/index/cldr-spec/plural-rules) for all 200+ languages in the [Unicode Common Locale Data Repository (CLDR)](http://www.unicode.org/cldr/charts/28/supplemental/language_plural_rules.html).
* Code and tests are [automatically generated](https://github.com/nicksnyder/go-i18n/tree/master/i18n/language/codegen) from [CLDR data](http://cldr.unicode.org/index/downloads)
* Supports strings with named variables using [text/template](http://golang.org/pkg/text/template/) syntax.
* Translation files are simple JSON or YAML.
* [Documented](http://godoc.org/github.com/nicksnyder/go-i18n) and [tested](https://travis-ci.org/nicksnyder/go-i18n)!
Package i18n [![GoDoc](http://godoc.org/github.com/nicksnyder/go-i18n?status.png)](http://godoc.org/github.com/nicksnyder/go-i18n/i18n)
------------
The i18n package provides runtime APIs for fetching translated strings.
Command goi18n [![GoDoc](http://godoc.org/github.com/nicksnyder/go-i18n?status.png)](http://godoc.org/github.com/nicksnyder/go-i18n/goi18n)
--------------
The goi18n command provides functionality for managing the translation process.
Installation
------------
Make sure you have [setup GOPATH](http://golang.org/doc/code.html#GOPATH).
go get -u github.com/nicksnyder/go-i18n/goi18n
goi18n -help
Workflow
--------
A typical workflow looks like this:
1. Add a new string to your source code.
```go
T("settings_title")
```
2. Add the string to en-US.all.json
```json
[
{
"id": "settings_title",
"translation": "Settings"
}
]
```
3. Run goi18n
```
goi18n path/to/*.all.json
```
4. Send `path/to/*.untranslated.json` to get translated.
5. Run goi18n again to merge the translations
```sh
goi18n path/to/*.all.json path/to/*.untranslated.json
```
Translation files
-----------------
A translation file stores translated and untranslated strings.
Example:
```json
[
{
"id": "d_days",
"translation": {
"one": "{{.Count}} day",
"other": "{{.Count}} days"
}
},
{
"id": "my_height_in_meters",
"translation": {
"one": "I am {{.Count}} meter tall.",
"other": "I am {{.Count}} meters tall."
}
},
{
"id": "person_greeting",
"translation": "Hello {{.Person}}"
},
{
"id": "person_unread_email_count",
"translation": {
"one": "{{.Person}} has {{.Count}} unread email.",
"other": "{{.Person}} has {{.Count}} unread emails."
}
},
{
"id": "person_unread_email_count_timeframe",
"translation": {
"one": "{{.Person}} has {{.Count}} unread email in the past {{.Timeframe}}.",
"other": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}."
}
},
{
"id": "program_greeting",
"translation": "Hello world"
},
{
"id": "your_unread_email_count",
"translation": {
"one": "You have {{.Count}} unread email.",
"other": "You have {{.Count}} unread emails."
}
}
]
```
Contributions
-------------
If you would like to submit a pull request, please
1. Write tests
2. Format code with [goimports](https://github.com/bradfitz/goimports).
3. Read the [common code review comments](https://github.com/golang/go/wiki/CodeReviewComments).
License
-------
go-i18n is available under the MIT license. See the [LICENSE](LICENSE) file for more info.

59
vendor/github.com/nicksnyder/go-i18n/goi18n/doc.go generated vendored Normal file
View File

@ -0,0 +1,59 @@
// The goi18n command formats and merges translation files.
//
// go get -u github.com/nicksnyder/go-i18n/goi18n
// goi18n -help
//
// Help documentation:
//
// goi18n formats and merges translation files.
//
// Usage:
//
// goi18n [options] [files...]
//
// Translation files:
//
// A translation file contains the strings and translations for a single language.
//
// Translation file names must have a suffix of a supported format (e.g. .json) and
// contain a valid language tag as defined by RFC 5646 (e.g. en-us, fr, zh-hant, etc.).
//
// For each language represented by at least one input translation file, goi18n will produce 2 output files:
//
// xx-yy.all.format
// This file contains all strings for the language (translated and untranslated).
// Use this file when loading strings at runtime.
//
// xx-yy.untranslated.format
// This file contains the strings that have not been translated for this language.
// The translations for the strings in this file will be extracted from the source language.
// After they are translated, merge them back into xx-yy.all.format using goi18n.
//
// Merging:
//
// goi18n will merge multiple translation files for the same language.
// Duplicate translations will be merged into the existing translation.
// Non-empty fields in the duplicate translation will overwrite those fields in the existing translation.
// Empty fields in the duplicate translation are ignored.
//
// Adding a new language:
//
// To produce translation files for a new language, create an empty translation file with the
// appropriate name and pass it in to goi18n.
//
// Options:
//
// -sourceLanguage tag
// goi18n uses the strings from this language to seed the translations for other languages.
// Default: en-us
//
// -outdir directory
// goi18n writes the output translation files to this directory.
// Default: .
//
// -format format
// goi18n encodes the output translation files in this format.
// Supported formats: json, yaml
// Default: json
//
package main

10
vendor/github.com/nicksnyder/go-i18n/goi18n/gendoc.sh generated vendored Normal file
View File

@ -0,0 +1,10 @@
go install
echo "// The goi18n command formats and merges translation files." > doc.go
echo "//" >> doc.go
echo "// go get -u github.com/nicksnyder/go-i18n/goi18n" >> doc.go
echo "// goi18n -help" >> doc.go
echo "//" >> doc.go
echo "// Help documentation:" >> doc.go
echo "//" >> doc.go
goi18n -help | sed -e 's/^/\/\/ /' >> doc.go
echo "package main" >> doc.go

82
vendor/github.com/nicksnyder/go-i18n/goi18n/goi18n.go generated vendored Normal file
View File

@ -0,0 +1,82 @@
package main
import (
"flag"
"fmt"
"os"
)
func usage() {
fmt.Printf(`goi18n formats and merges translation files.
Usage:
goi18n [options] [files...]
Translation files:
A translation file contains the strings and translations for a single language.
Translation file names must have a suffix of a supported format (e.g. .json) and
contain a valid language tag as defined by RFC 5646 (e.g. en-us, fr, zh-hant, etc.).
For each language represented by at least one input translation file, goi18n will produce 2 output files:
xx-yy.all.format
This file contains all strings for the language (translated and untranslated).
Use this file when loading strings at runtime.
xx-yy.untranslated.format
This file contains the strings that have not been translated for this language.
The translations for the strings in this file will be extracted from the source language.
After they are translated, merge them back into xx-yy.all.format using goi18n.
Merging:
goi18n will merge multiple translation files for the same language.
Duplicate translations will be merged into the existing translation.
Non-empty fields in the duplicate translation will overwrite those fields in the existing translation.
Empty fields in the duplicate translation are ignored.
Adding a new language:
To produce translation files for a new language, create an empty translation file with the
appropriate name and pass it in to goi18n.
Options:
-sourceLanguage tag
goi18n uses the strings from this language to seed the translations for other languages.
Default: en-us
-outdir directory
goi18n writes the output translation files to this directory.
Default: .
-format format
goi18n encodes the output translation files in this format.
Supported formats: json, yaml
Default: json
`)
os.Exit(1)
}
func main() {
flag.Usage = usage
sourceLanguage := flag.String("sourceLanguage", "en-us", "")
outdir := flag.String("outdir", ".", "")
format := flag.String("format", "json", "")
flag.Parse()
mc := &mergeCommand{
translationFiles: flag.Args(),
sourceLanguageTag: *sourceLanguage,
outdir: *outdir,
format: *format,
}
if err := mc.execute(); err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
}

127
vendor/github.com/nicksnyder/go-i18n/goi18n/merge.go generated vendored Normal file
View File

@ -0,0 +1,127 @@
package main
import (
"encoding/json"
"fmt"
"gopkg.in/yaml.v2"
"io/ioutil"
"path/filepath"
"reflect"
"sort"
"github.com/nicksnyder/go-i18n/i18n/bundle"
"github.com/nicksnyder/go-i18n/i18n/language"
"github.com/nicksnyder/go-i18n/i18n/translation"
)
type mergeCommand struct {
translationFiles []string
sourceLanguageTag string
outdir string
format string
}
func (mc *mergeCommand) execute() error {
if len(mc.translationFiles) < 1 {
return fmt.Errorf("need at least one translation file to parse")
}
if lang := language.Parse(mc.sourceLanguageTag); lang == nil {
return fmt.Errorf("invalid source locale: %s", mc.sourceLanguageTag)
}
marshal, err := newMarshalFunc(mc.format)
if err != nil {
return err
}
bundle := bundle.New()
for _, tf := range mc.translationFiles {
if err := bundle.LoadTranslationFile(tf); err != nil {
return fmt.Errorf("failed to load translation file %s because %s\n", tf, err)
}
}
translations := bundle.Translations()
sourceLanguageTag := language.NormalizeTag(mc.sourceLanguageTag)
sourceTranslations := translations[sourceLanguageTag]
if sourceTranslations == nil {
return fmt.Errorf("no translations found for source locale %s", sourceLanguageTag)
}
for translationID, src := range sourceTranslations {
for _, localeTranslations := range translations {
if dst := localeTranslations[translationID]; dst == nil || reflect.TypeOf(src) != reflect.TypeOf(dst) {
localeTranslations[translationID] = src.UntranslatedCopy()
}
}
}
for localeID, localeTranslations := range translations {
lang := language.MustParse(localeID)[0]
all := filter(localeTranslations, func(t translation.Translation) translation.Translation {
return t.Normalize(lang)
})
if err := mc.writeFile("all", all, localeID, marshal); err != nil {
return err
}
untranslated := filter(localeTranslations, func(t translation.Translation) translation.Translation {
if t.Incomplete(lang) {
return t.Normalize(lang).Backfill(sourceTranslations[t.ID()])
}
return nil
})
if err := mc.writeFile("untranslated", untranslated, localeID, marshal); err != nil {
return err
}
}
return nil
}
type marshalFunc func(interface{}) ([]byte, error)
func (mc *mergeCommand) writeFile(label string, translations []translation.Translation, localeID string, marshal marshalFunc) error {
sort.Sort(translation.SortableByID(translations))
buf, err := marshal(marshalInterface(translations))
if err != nil {
return fmt.Errorf("failed to marshal %s strings to %s because %s", localeID, mc.format, err)
}
filename := filepath.Join(mc.outdir, fmt.Sprintf("%s.%s.%s", localeID, label, mc.format))
if err := ioutil.WriteFile(filename, buf, 0666); err != nil {
return fmt.Errorf("failed to write %s because %s", filename, err)
}
return nil
}
func filter(translations map[string]translation.Translation, filter func(translation.Translation) translation.Translation) []translation.Translation {
filtered := make([]translation.Translation, 0, len(translations))
for _, translation := range translations {
if t := filter(translation); t != nil {
filtered = append(filtered, t)
}
}
return filtered
}
func newMarshalFunc(format string) (marshalFunc, error) {
switch format {
case "json":
return func(v interface{}) ([]byte, error) {
return json.MarshalIndent(v, "", " ")
}, nil
case "yaml":
return func(v interface{}) ([]byte, error) {
return yaml.Marshal(v)
}, nil
}
return nil, fmt.Errorf("unsupported format: %s\n", format)
}
func marshalInterface(translations []translation.Translation) []interface{} {
mi := make([]interface{}, len(translations))
for i, translation := range translations {
mi[i] = translation.MarshalInterface()
}
return mi
}

View File

@ -0,0 +1,74 @@
package main
import (
"bytes"
"io/ioutil"
"os"
"testing"
)
func TestMergeExecuteJSON(t *testing.T) {
files := []string{
"testdata/input/en-us.one.json",
"testdata/input/en-us.two.json",
"testdata/input/fr-fr.json",
"testdata/input/ar-ar.one.json",
"testdata/input/ar-ar.two.json",
}
testMergeExecute(t, files)
}
func TestMergeExecuteYAML(t *testing.T) {
files := []string{
"testdata/input/yaml/en-us.one.yaml",
"testdata/input/yaml/en-us.two.json",
"testdata/input/yaml/fr-fr.json",
"testdata/input/yaml/ar-ar.one.json",
"testdata/input/yaml/ar-ar.two.json",
}
testMergeExecute(t, files)
}
func testMergeExecute(t *testing.T, files []string) {
resetDir(t, "testdata/output")
mc := &mergeCommand{
translationFiles: files,
sourceLanguageTag: "en-us",
outdir: "testdata/output",
format: "json",
}
if err := mc.execute(); err != nil {
t.Fatal(err)
}
expectEqualFiles(t, "testdata/output/en-us.all.json", "testdata/expected/en-us.all.json")
expectEqualFiles(t, "testdata/output/ar-ar.all.json", "testdata/expected/ar-ar.all.json")
expectEqualFiles(t, "testdata/output/fr-fr.all.json", "testdata/expected/fr-fr.all.json")
expectEqualFiles(t, "testdata/output/en-us.untranslated.json", "testdata/expected/en-us.untranslated.json")
expectEqualFiles(t, "testdata/output/ar-ar.untranslated.json", "testdata/expected/ar-ar.untranslated.json")
expectEqualFiles(t, "testdata/output/fr-fr.untranslated.json", "testdata/expected/fr-fr.untranslated.json")
}
func resetDir(t *testing.T, dir string) {
if err := os.RemoveAll(dir); err != nil {
t.Fatal(err)
}
if err := os.Mkdir(dir, 0777); err != nil {
t.Fatal(err)
}
}
func expectEqualFiles(t *testing.T, expectedName, actualName string) {
actual, err := ioutil.ReadFile(actualName)
if err != nil {
t.Fatal(err)
}
expected, err := ioutil.ReadFile(expectedName)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(actual, expected) {
t.Fatalf("contents of files did not match: %s, %s", expectedName, actualName)
}
}

View File

@ -0,0 +1,30 @@
- id: program_greeting
translation: "Hello world"
- id: person_greeting
translation: "Hello {{.Person}}"
- id: my_height_in_meters
translation:
one: "I am {{.Count}} meter tall."
other: "I am {{.Count}} meters tall."
- id: your_unread_email_count
translation:
one: "You have {{.Count}} unread email."
other: "You have {{.Count}} unread emails."
- id: person_unread_email_count
translation:
one: "{{.Person}} has {{.Count}} unread email."
other: "{{.Person}} has {{.Count}} unread emails."
- id: person_unread_email_count_timeframe
translation:
one: "{{.Person}} has {{.Count}} unread email in the past {{.Timeframe}}."
other: "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}."
- id: d_days
translation:
one: "{{.Count}} day"
other: "{{.Count}} days"

View File

@ -0,0 +1,65 @@
[
{
"id": "d_days",
"translation": {
"few": "new arabic few translation of d_days",
"many": "arabic many translation of d_days",
"one": "arabic one translation of d_days",
"other": "",
"two": "",
"zero": ""
}
},
{
"id": "my_height_in_meters",
"translation": {
"few": "",
"many": "",
"one": "",
"other": "",
"two": "",
"zero": ""
}
},
{
"id": "person_greeting",
"translation": "new arabic translation of person_greeting"
},
{
"id": "person_unread_email_count",
"translation": {
"few": "arabic few translation of person_unread_email_count",
"many": "arabic many translation of person_unread_email_count",
"one": "arabic one translation of person_unread_email_count",
"other": "arabic other translation of person_unread_email_count",
"two": "arabic two translation of person_unread_email_count",
"zero": "arabic zero translation of person_unread_email_count"
}
},
{
"id": "person_unread_email_count_timeframe",
"translation": {
"few": "",
"many": "",
"one": "",
"other": "",
"two": "",
"zero": ""
}
},
{
"id": "program_greeting",
"translation": ""
},
{
"id": "your_unread_email_count",
"translation": {
"few": "",
"many": "",
"one": "",
"other": "",
"two": "",
"zero": ""
}
}
]

View File

@ -0,0 +1,50 @@
[
{
"id": "d_days",
"translation": {
"few": "new arabic few translation of d_days",
"many": "arabic many translation of d_days",
"one": "arabic one translation of d_days",
"other": "{{.Count}} days",
"two": "{{.Count}} days",
"zero": "{{.Count}} days"
}
},
{
"id": "my_height_in_meters",
"translation": {
"few": "I am {{.Count}} meters tall.",
"many": "I am {{.Count}} meters tall.",
"one": "I am {{.Count}} meters tall.",
"other": "I am {{.Count}} meters tall.",
"two": "I am {{.Count}} meters tall.",
"zero": "I am {{.Count}} meters tall."
}
},
{
"id": "person_unread_email_count_timeframe",
"translation": {
"few": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}.",
"many": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}.",
"one": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}.",
"other": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}.",
"two": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}.",
"zero": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}."
}
},
{
"id": "program_greeting",
"translation": "Hello world"
},
{
"id": "your_unread_email_count",
"translation": {
"few": "You have {{.Count}} unread emails.",
"many": "You have {{.Count}} unread emails.",
"one": "You have {{.Count}} unread emails.",
"other": "You have {{.Count}} unread emails.",
"two": "You have {{.Count}} unread emails.",
"zero": "You have {{.Count}} unread emails."
}
}
]

View File

@ -0,0 +1,45 @@
[
{
"id": "d_days",
"translation": {
"one": "{{.Count}} day",
"other": "{{.Count}} days"
}
},
{
"id": "my_height_in_meters",
"translation": {
"one": "I am {{.Count}} meter tall.",
"other": "I am {{.Count}} meters tall."
}
},
{
"id": "person_greeting",
"translation": "Hello {{.Person}}"
},
{
"id": "person_unread_email_count",
"translation": {
"one": "{{.Person}} has {{.Count}} unread email.",
"other": "{{.Person}} has {{.Count}} unread emails."
}
},
{
"id": "person_unread_email_count_timeframe",
"translation": {
"one": "{{.Person}} has {{.Count}} unread email in the past {{.Timeframe}}.",
"other": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}."
}
},
{
"id": "program_greeting",
"translation": "Hello world"
},
{
"id": "your_unread_email_count",
"translation": {
"one": "You have {{.Count}} unread email.",
"other": "You have {{.Count}} unread emails."
}
}
]

View File

@ -0,0 +1 @@
[]

View File

@ -0,0 +1,45 @@
[
{
"id": "d_days",
"translation": {
"one": "",
"other": ""
}
},
{
"id": "my_height_in_meters",
"translation": {
"one": "",
"other": ""
}
},
{
"id": "person_greeting",
"translation": ""
},
{
"id": "person_unread_email_count",
"translation": {
"one": "",
"other": ""
}
},
{
"id": "person_unread_email_count_timeframe",
"translation": {
"one": "",
"other": ""
}
},
{
"id": "program_greeting",
"translation": ""
},
{
"id": "your_unread_email_count",
"translation": {
"one": "",
"other": ""
}
}
]

View File

@ -0,0 +1,45 @@
[
{
"id": "d_days",
"translation": {
"one": "{{.Count}} days",
"other": "{{.Count}} days"
}
},
{
"id": "my_height_in_meters",
"translation": {
"one": "I am {{.Count}} meters tall.",
"other": "I am {{.Count}} meters tall."
}
},
{
"id": "person_greeting",
"translation": "Hello {{.Person}}"
},
{
"id": "person_unread_email_count",
"translation": {
"one": "{{.Person}} has {{.Count}} unread emails.",
"other": "{{.Person}} has {{.Count}} unread emails."
}
},
{
"id": "person_unread_email_count_timeframe",
"translation": {
"one": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}.",
"other": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}."
}
},
{
"id": "program_greeting",
"translation": "Hello world"
},
{
"id": "your_unread_email_count",
"translation": {
"one": "You have {{.Count}} unread emails.",
"other": "You have {{.Count}} unread emails."
}
}
]

View File

@ -0,0 +1,54 @@
[
{
"id": "d_days",
"translation": {
"few": "arabic few translation of d_days",
"many": "arabic many translation of d_days",
"one": "",
"other": "",
"two": "",
"zero": ""
}
},
{
"id": "person_greeting",
"translation": "arabic translation of person_greeting"
},
{
"id": "person_unread_email_count",
"translation": {
"few": "arabic few translation of person_unread_email_count",
"many": "arabic many translation of person_unread_email_count",
"one": "arabic one translation of person_unread_email_count",
"other": "",
"two": "",
"zero": ""
}
},
{
"id": "person_unread_email_count_timeframe",
"translation": {
"few": "",
"many": "",
"one": "",
"other": "",
"two": "",
"zero": ""
}
},
{
"id": "program_greeting",
"translation": ""
},
{
"id": "your_unread_email_count",
"translation": {
"few": "",
"many": "",
"one": "",
"other": "",
"two": "",
"zero": ""
}
}
]

View File

@ -0,0 +1,54 @@
[
{
"id": "d_days",
"translation": {
"few": "new arabic few translation of d_days",
"many": "",
"one": "arabic one translation of d_days",
"other": "",
"two": "",
"zero": ""
}
},
{
"id": "person_greeting",
"translation": "new arabic translation of person_greeting"
},
{
"id": "person_unread_email_count",
"translation": {
"few": "",
"many": "",
"one": "",
"other": "arabic other translation of person_unread_email_count",
"two": "arabic two translation of person_unread_email_count",
"zero": "arabic zero translation of person_unread_email_count"
}
},
{
"id": "person_unread_email_count_timeframe",
"translation": {
"few": "",
"many": "",
"one": "",
"other": "",
"two": "",
"zero": ""
}
},
{
"id": "program_greeting",
"translation": ""
},
{
"id": "your_unread_email_count",
"translation": {
"few": "",
"many": "",
"one": "",
"other": "",
"two": "",
"zero": ""
}
}
]

View File

@ -0,0 +1,30 @@
[
{
"id": "program_greeting",
"translation": "Hello world"
},
{
"id": "your_unread_email_count",
"translation": {
"one": "You have {{.Count}} unread email.",
"other": "You have {{.Count}} unread emails."
}
},
{
"id": "my_height_in_meters",
"translation": {
"one": "I am {{.Count}} meter tall.",
"other": "I am {{.Count}} meters tall."
}
},
{
"id": "person_unread_email_count_timeframe",
"translation": {
"one": "{{.Person}} has {{.Count}} unread email in the past {{.Timeframe}}."
}
},
{
"id": "d_days",
"translation": "this should get overwritten"
}
]

View File

@ -0,0 +1,26 @@
[
{
"id": "person_greeting",
"translation": "Hello {{.Person}}"
},
{
"id": "person_unread_email_count",
"translation": {
"one": "{{.Person}} has {{.Count}} unread email.",
"other": "{{.Person}} has {{.Count}} unread emails."
}
},
{
"id": "person_unread_email_count_timeframe",
"translation": {
"other": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}."
}
},
{
"id": "d_days",
"translation": {
"one": "{{.Count}} day",
"other": "{{.Count}} days"
}
}
]

View File

View File

@ -0,0 +1,54 @@
[
{
"id": "d_days",
"translation": {
"few": "arabic few translation of d_days",
"many": "arabic many translation of d_days",
"one": "",
"other": "",
"two": "",
"zero": ""
}
},
{
"id": "person_greeting",
"translation": "arabic translation of person_greeting"
},
{
"id": "person_unread_email_count",
"translation": {
"few": "arabic few translation of person_unread_email_count",
"many": "arabic many translation of person_unread_email_count",
"one": "arabic one translation of person_unread_email_count",
"other": "",
"two": "",
"zero": ""
}
},
{
"id": "person_unread_email_count_timeframe",
"translation": {
"few": "",
"many": "",
"one": "",
"other": "",
"two": "",
"zero": ""
}
},
{
"id": "program_greeting",
"translation": ""
},
{
"id": "your_unread_email_count",
"translation": {
"few": "",
"many": "",
"one": "",
"other": "",
"two": "",
"zero": ""
}
}
]

View File

@ -0,0 +1,54 @@
[
{
"id": "d_days",
"translation": {
"few": "new arabic few translation of d_days",
"many": "",
"one": "arabic one translation of d_days",
"other": "",
"two": "",
"zero": ""
}
},
{
"id": "person_greeting",
"translation": "new arabic translation of person_greeting"
},
{
"id": "person_unread_email_count",
"translation": {
"few": "",
"many": "",
"one": "",
"other": "arabic other translation of person_unread_email_count",
"two": "arabic two translation of person_unread_email_count",
"zero": "arabic zero translation of person_unread_email_count"
}
},
{
"id": "person_unread_email_count_timeframe",
"translation": {
"few": "",
"many": "",
"one": "",
"other": "",
"two": "",
"zero": ""
}
},
{
"id": "program_greeting",
"translation": ""
},
{
"id": "your_unread_email_count",
"translation": {
"few": "",
"many": "",
"one": "",
"other": "",
"two": "",
"zero": ""
}
}
]

View File

@ -0,0 +1,19 @@
- id: program_greeting
translation: Hello world
- id: your_unread_email_count
translation:
one: You have {{.Count}} unread email.
other: You have {{.Count}} unread emails.
- id: my_height_in_meters
translation:
one: I am {{.Count}} meter tall.
other: I am {{.Count}} meters tall.
- id: person_unread_email_count_timeframe
translation:
one: "{{.Person}} has {{.Count}} unread email in the past {{.Timeframe}}."
- id: d_days
translation: this should get overwritten

View File

@ -0,0 +1,26 @@
[
{
"id": "person_greeting",
"translation": "Hello {{.Person}}"
},
{
"id": "person_unread_email_count",
"translation": {
"one": "{{.Person}} has {{.Count}} unread email.",
"other": "{{.Person}} has {{.Count}} unread emails."
}
},
{
"id": "person_unread_email_count_timeframe",
"translation": {
"other": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}."
}
},
{
"id": "d_days",
"translation": {
"one": "{{.Count}} day",
"other": "{{.Count}} days"
}
}
]

View File

@ -0,0 +1,315 @@
// Package bundle manages translations for multiple languages.
package bundle
import (
"encoding/json"
"fmt"
"gopkg.in/yaml.v2"
"io/ioutil"
"reflect"
"path/filepath"
"github.com/nicksnyder/go-i18n/i18n/language"
"github.com/nicksnyder/go-i18n/i18n/translation"
)
// TranslateFunc is a copy of i18n.TranslateFunc to avoid a circular dependency.
type TranslateFunc func(translationID string, args ...interface{}) string
// Bundle stores the translations for multiple languages.
type Bundle struct {
// The primary translations for a language tag and translation id.
translations map[string]map[string]translation.Translation
// Translations that can be used when an exact language match is not possible.
fallbackTranslations map[string]map[string]translation.Translation
}
// New returns an empty bundle.
func New() *Bundle {
return &Bundle{
translations: make(map[string]map[string]translation.Translation),
fallbackTranslations: make(map[string]map[string]translation.Translation),
}
}
// MustLoadTranslationFile is similar to LoadTranslationFile
// except it panics if an error happens.
func (b *Bundle) MustLoadTranslationFile(filename string) {
if err := b.LoadTranslationFile(filename); err != nil {
panic(err)
}
}
// LoadTranslationFile loads the translations from filename into memory.
//
// The language that the translations are associated with is parsed from the filename (e.g. en-US.json).
//
// Generally you should load translation files once during your program's initialization.
func (b *Bundle) LoadTranslationFile(filename string) error {
buf, err := ioutil.ReadFile(filename)
if err != nil {
return err
}
return b.ParseTranslationFileBytes(filename, buf)
}
// ParseTranslationFileBytes is similar to LoadTranslationFile except it parses the bytes in buf.
//
// It is useful for parsing translation files embedded with go-bindata.
func (b *Bundle) ParseTranslationFileBytes(filename string, buf []byte) error {
basename := filepath.Base(filename)
langs := language.Parse(basename)
switch l := len(langs); {
case l == 0:
return fmt.Errorf("no language found in %q", basename)
case l > 1:
return fmt.Errorf("multiple languages found in filename %q: %v; expected one", basename, langs)
}
translations, err := parseTranslations(filename, buf)
if err != nil {
return err
}
b.AddTranslation(langs[0], translations...)
return nil
}
func parseTranslations(filename string, buf []byte) ([]translation.Translation, error) {
var unmarshalFunc func([]byte, interface{}) error
switch format := filepath.Ext(filename); format {
case ".json":
unmarshalFunc = json.Unmarshal
case ".yaml":
unmarshalFunc = yaml.Unmarshal
default:
return nil, fmt.Errorf("unsupported file extension %s", format)
}
var translationsData []map[string]interface{}
if len(buf) > 0 {
if err := unmarshalFunc(buf, &translationsData); err != nil {
return nil, err
}
}
translations := make([]translation.Translation, 0, len(translationsData))
for i, translationData := range translationsData {
t, err := translation.NewTranslation(translationData)
if err != nil {
return nil, fmt.Errorf("unable to parse translation #%d in %s because %s\n%v", i, filename, err, translationData)
}
translations = append(translations, t)
}
return translations, nil
}
// AddTranslation adds translations for a language.
//
// It is useful if your translations are in a format not supported by LoadTranslationFile.
func (b *Bundle) AddTranslation(lang *language.Language, translations ...translation.Translation) {
if b.translations[lang.Tag] == nil {
b.translations[lang.Tag] = make(map[string]translation.Translation, len(translations))
}
currentTranslations := b.translations[lang.Tag]
for _, newTranslation := range translations {
if currentTranslation := currentTranslations[newTranslation.ID()]; currentTranslation != nil {
currentTranslations[newTranslation.ID()] = currentTranslation.Merge(newTranslation)
} else {
currentTranslations[newTranslation.ID()] = newTranslation
}
}
// lang can provide translations for less specific language tags.
for _, tag := range lang.MatchingTags() {
b.fallbackTranslations[tag] = currentTranslations
}
}
// Translations returns all translations in the bundle.
func (b *Bundle) Translations() map[string]map[string]translation.Translation {
return b.translations
}
// LanguageTags returns the tags of all languages that that have been added.
func (b *Bundle) LanguageTags() []string {
var tags []string
for k := range b.translations {
tags = append(tags, k)
}
return tags
}
// LanguageTranslationIDs returns the ids of all translations that have been added for a given language.
func (b *Bundle) LanguageTranslationIDs(languageTag string) []string {
var ids []string
for id := range b.translations[languageTag] {
ids = append(ids, id)
}
return ids
}
// MustTfunc is similar to Tfunc except it panics if an error happens.
func (b *Bundle) MustTfunc(pref string, prefs ...string) TranslateFunc {
tfunc, err := b.Tfunc(pref, prefs...)
if err != nil {
panic(err)
}
return tfunc
}
// MustTfuncAndLanguage is similar to TfuncAndLanguage except it panics if an error happens.
func (b *Bundle) MustTfuncAndLanguage(pref string, prefs ...string) (TranslateFunc, *language.Language) {
tfunc, language, err := b.TfuncAndLanguage(pref, prefs...)
if err != nil {
panic(err)
}
return tfunc, language
}
// Tfunc is similar to TfuncAndLanguage except is doesn't return the Language.
func (b *Bundle) Tfunc(pref string, prefs ...string) (TranslateFunc, error) {
tfunc, _, err := b.TfuncAndLanguage(pref, prefs...)
return tfunc, err
}
// TfuncAndLanguage returns a TranslateFunc for the first Language that
// has a non-zero number of translations in the bundle.
//
// The returned Language matches the the first language preference that could be satisfied,
// but this may not strictly match the language of the translations used to satisfy that preference.
//
// For example, the user may request "zh". If there are no translations for "zh" but there are translations
// for "zh-cn", then the translations for "zh-cn" will be used but the returned Language will be "zh".
//
// It can parse languages from Accept-Language headers (RFC 2616),
// but it assumes weights are monotonically decreasing.
func (b *Bundle) TfuncAndLanguage(pref string, prefs ...string) (TranslateFunc, *language.Language, error) {
lang := b.supportedLanguage(pref, prefs...)
var err error
if lang == nil {
err = fmt.Errorf("no supported languages found %#v", append(prefs, pref))
}
return func(translationID string, args ...interface{}) string {
return b.translate(lang, translationID, args...)
}, lang, err
}
// supportedLanguage returns the first language which
// has a non-zero number of translations in the bundle.
func (b *Bundle) supportedLanguage(pref string, prefs ...string) *language.Language {
lang := b.translatedLanguage(pref)
if lang == nil {
for _, pref := range prefs {
lang = b.translatedLanguage(pref)
if lang != nil {
break
}
}
}
return lang
}
func (b *Bundle) translatedLanguage(src string) *language.Language {
langs := language.Parse(src)
for _, lang := range langs {
if len(b.translations[lang.Tag]) > 0 ||
len(b.fallbackTranslations[lang.Tag]) > 0 {
return lang
}
}
return nil
}
func (b *Bundle) translate(lang *language.Language, translationID string, args ...interface{}) string {
if lang == nil {
return translationID
}
translations := b.translations[lang.Tag]
if translations == nil {
translations = b.fallbackTranslations[lang.Tag]
if translations == nil {
return translationID
}
}
translation := translations[translationID]
if translation == nil {
return translationID
}
var data interface{}
var count interface{}
if argc := len(args); argc > 0 {
if isNumber(args[0]) {
count = args[0]
if argc > 1 {
data = args[1]
}
} else {
data = args[0]
}
}
if count != nil {
if data == nil {
data = map[string]interface{}{"Count": count}
} else {
dataMap := toMap(data)
dataMap["Count"] = count
data = dataMap
}
}
p, _ := lang.Plural(count)
template := translation.Template(p)
if template == nil {
return translationID
}
s := template.Execute(data)
if s == "" {
return translationID
}
return s
}
func isNumber(n interface{}) bool {
switch n.(type) {
case int, int8, int16, int32, int64, string:
return true
}
return false
}
func toMap(input interface{}) map[string]interface{} {
if data, ok := input.(map[string]interface{}); ok {
return data
}
v := reflect.ValueOf(input)
switch v.Kind() {
case reflect.Ptr:
return toMap(v.Elem().Interface())
case reflect.Struct:
return structToMap(v)
default:
return nil
}
}
// Converts the top level of a struct to a map[string]interface{}.
// Code inspired by github.com/fatih/structs.
func structToMap(v reflect.Value) map[string]interface{} {
out := make(map[string]interface{})
t := v.Type()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
if field.PkgPath != "" {
// unexported field. skip.
continue
}
out[field.Name] = v.FieldByName(field.Name).Interface()
}
return out
}

View File

@ -0,0 +1,289 @@
package bundle
import (
"fmt"
"testing"
"reflect"
"sort"
"github.com/nicksnyder/go-i18n/i18n/language"
"github.com/nicksnyder/go-i18n/i18n/translation"
)
func TestMustLoadTranslationFile(t *testing.T) {
t.Skipf("not implemented")
}
func TestLoadTranslationFile(t *testing.T) {
t.Skipf("not implemented")
}
func TestParseTranslationFileBytes(t *testing.T) {
t.Skipf("not implemented")
}
func TestAddTranslation(t *testing.T) {
t.Skipf("not implemented")
}
func TestMustTfunc(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Errorf("expected MustTfunc to panic")
}
}()
New().MustTfunc("invalid")
}
func TestLanguageTagsAndTranslationIDs(t *testing.T) {
b := New()
translationID := "translation_id"
englishLanguage := languageWithTag("en-US")
frenchLanguage := languageWithTag("fr-FR")
spanishLanguage := languageWithTag("es")
addFakeTranslation(t, b, englishLanguage, "English"+translationID)
addFakeTranslation(t, b, frenchLanguage, translationID)
addFakeTranslation(t, b, spanishLanguage, translationID)
tags := b.LanguageTags()
sort.Strings(tags)
compareTo := []string{englishLanguage.Tag, spanishLanguage.Tag, frenchLanguage.Tag}
if !reflect.DeepEqual(tags, compareTo) {
t.Errorf("LanguageTags() = %#v; expected: %#v", tags, compareTo)
}
ids := b.LanguageTranslationIDs(englishLanguage.Tag)
sort.Strings(ids)
compareTo = []string{"English" + translationID}
if !reflect.DeepEqual(ids, compareTo) {
t.Errorf("LanguageTranslationIDs() = %#v; expected: %#v", ids, compareTo)
}
}
func TestTfuncAndLanguage(t *testing.T) {
b := New()
translationID := "translation_id"
englishLanguage := languageWithTag("en-US")
frenchLanguage := languageWithTag("fr-FR")
spanishLanguage := languageWithTag("es")
chineseLanguage := languageWithTag("zh-hans-cn")
englishTranslation := addFakeTranslation(t, b, englishLanguage, translationID)
frenchTranslation := addFakeTranslation(t, b, frenchLanguage, translationID)
spanishTranslation := addFakeTranslation(t, b, spanishLanguage, translationID)
chineseTranslation := addFakeTranslation(t, b, chineseLanguage, translationID)
tests := []struct {
languageIDs []string
result string
expectedLanguage *language.Language
}{
{
[]string{"invalid"},
translationID,
nil,
},
{
[]string{"invalid", "invalid2"},
translationID,
nil,
},
{
[]string{"invalid", "en-US"},
englishTranslation,
englishLanguage,
},
{
[]string{"en-US", "invalid"},
englishTranslation,
englishLanguage,
},
{
[]string{"en-US", "fr-FR"},
englishTranslation,
englishLanguage,
},
{
[]string{"invalid", "es"},
spanishTranslation,
spanishLanguage,
},
{
[]string{"zh-CN,fr-XX,es"},
spanishTranslation,
spanishLanguage,
},
{
[]string{"fr"},
frenchTranslation,
// The language is still "fr" even though the translation is provided by "fr-FR"
languageWithTag("fr"),
},
{
[]string{"zh"},
chineseTranslation,
// The language is still "zh" even though the translation is provided by "zh-hans-cn"
languageWithTag("zh"),
},
{
[]string{"zh-hans"},
chineseTranslation,
// The language is still "zh-hans" even though the translation is provided by "zh-hans-cn"
languageWithTag("zh-hans"),
},
{
[]string{"zh-hans-cn"},
chineseTranslation,
languageWithTag("zh-hans-cn"),
},
}
for i, test := range tests {
tf, lang, err := b.TfuncAndLanguage(test.languageIDs[0], test.languageIDs[1:]...)
if err != nil && test.expectedLanguage != nil {
t.Errorf("Tfunc(%v) = error{%q}; expected no error", test.languageIDs, err)
}
if err == nil && test.expectedLanguage == nil {
t.Errorf("Tfunc(%v) = nil error; expected error", test.languageIDs)
}
if result := tf(translationID); result != test.result {
t.Errorf("translation %d was %s; expected %s", i, result, test.result)
}
if (lang == nil && test.expectedLanguage != nil) ||
(lang != nil && test.expectedLanguage == nil) ||
(lang != nil && test.expectedLanguage != nil && lang.String() != test.expectedLanguage.String()) {
t.Errorf("lang %d was %s; expected %s", i, lang, test.expectedLanguage)
}
}
}
func addFakeTranslation(t *testing.T, b *Bundle, lang *language.Language, translationID string) string {
translation := fakeTranslation(lang, translationID)
b.AddTranslation(lang, testNewTranslation(t, map[string]interface{}{
"id": translationID,
"translation": translation,
}))
return translation
}
func fakeTranslation(lang *language.Language, translationID string) string {
return fmt.Sprintf("%s(%s)", lang.Tag, translationID)
}
func testNewTranslation(t *testing.T, data map[string]interface{}) translation.Translation {
translation, err := translation.NewTranslation(data)
if err != nil {
t.Fatal(err)
}
return translation
}
func languageWithTag(tag string) *language.Language {
return language.MustParse(tag)[0]
}
func createBenchmarkTranslateFunc(b *testing.B, translationTemplate interface{}, count interface{}, expected string) func(data interface{}) {
bundle := New()
lang := "en-US"
translationID := "translation_id"
translation, err := translation.NewTranslation(map[string]interface{}{
"id": translationID,
"translation": translationTemplate,
})
if err != nil {
b.Fatal(err)
}
bundle.AddTranslation(languageWithTag(lang), translation)
tf, err := bundle.Tfunc(lang)
if err != nil {
b.Fatal(err)
}
return func(data interface{}) {
var result string
if count == nil {
result = tf(translationID, data)
} else {
result = tf(translationID, count, data)
}
if result != expected {
b.Fatalf("expected %q, got %q", expected, result)
}
}
}
func createBenchmarkPluralTranslateFunc(b *testing.B) func(data interface{}) {
translationTemplate := map[string]interface{}{
"one": "{{.Person}} is {{.Count}} year old.",
"other": "{{.Person}} is {{.Count}} years old.",
}
count := 26
expected := "Bob is 26 years old."
return createBenchmarkTranslateFunc(b, translationTemplate, count, expected)
}
func createBenchmarkNonPluralTranslateFunc(b *testing.B) func(data interface{}) {
translationTemplate := "Hi {{.Person}}!"
expected := "Hi Bob!"
return createBenchmarkTranslateFunc(b, translationTemplate, nil, expected)
}
func BenchmarkTranslateNonPluralWithMap(b *testing.B) {
data := map[string]interface{}{
"Person": "Bob",
}
tf := createBenchmarkNonPluralTranslateFunc(b)
b.ResetTimer()
for i := 0; i < b.N; i++ {
tf(data)
}
}
func BenchmarkTranslateNonPluralWithStruct(b *testing.B) {
data := struct{ Person string }{Person: "Bob"}
tf := createBenchmarkNonPluralTranslateFunc(b)
b.ResetTimer()
for i := 0; i < b.N; i++ {
tf(data)
}
}
func BenchmarkTranslateNonPluralWithStructPointer(b *testing.B) {
data := &struct{ Person string }{Person: "Bob"}
tf := createBenchmarkNonPluralTranslateFunc(b)
b.ResetTimer()
for i := 0; i < b.N; i++ {
tf(data)
}
}
func BenchmarkTranslatePluralWithMap(b *testing.B) {
data := map[string]interface{}{
"Person": "Bob",
}
tf := createBenchmarkPluralTranslateFunc(b)
b.ResetTimer()
for i := 0; i < b.N; i++ {
tf(data)
}
}
func BenchmarkTranslatePluralWithStruct(b *testing.B) {
data := struct{ Person string }{Person: "Bob"}
tf := createBenchmarkPluralTranslateFunc(b)
b.ResetTimer()
for i := 0; i < b.N; i++ {
tf(data)
}
}
func BenchmarkTranslatePluralWithStructPointer(b *testing.B) {
data := &struct{ Person string }{Person: "Bob"}
tf := createBenchmarkPluralTranslateFunc(b)
b.ResetTimer()
for i := 0; i < b.N; i++ {
tf(data)
}
}

View File

@ -0,0 +1,63 @@
package i18n_test
import (
"fmt"
"github.com/nicksnyder/go-i18n/i18n"
)
func Example() {
i18n.MustLoadTranslationFile("../goi18n/testdata/expected/en-us.all.json")
T, _ := i18n.Tfunc("en-US")
bobMap := map[string]interface{}{"Person": "Bob"}
bobStruct := struct{ Person string }{Person: "Bob"}
fmt.Println(T("program_greeting"))
fmt.Println(T("person_greeting", bobMap))
fmt.Println(T("person_greeting", bobStruct))
fmt.Println(T("your_unread_email_count", 0))
fmt.Println(T("your_unread_email_count", 1))
fmt.Println(T("your_unread_email_count", 2))
fmt.Println(T("my_height_in_meters", "1.7"))
fmt.Println(T("person_unread_email_count", 0, bobMap))
fmt.Println(T("person_unread_email_count", 1, bobMap))
fmt.Println(T("person_unread_email_count", 2, bobMap))
fmt.Println(T("person_unread_email_count", 0, bobStruct))
fmt.Println(T("person_unread_email_count", 1, bobStruct))
fmt.Println(T("person_unread_email_count", 2, bobStruct))
fmt.Println(T("person_unread_email_count_timeframe", 3, map[string]interface{}{
"Person": "Bob",
"Timeframe": T("d_days", 0),
}))
fmt.Println(T("person_unread_email_count_timeframe", 3, map[string]interface{}{
"Person": "Bob",
"Timeframe": T("d_days", 1),
}))
fmt.Println(T("person_unread_email_count_timeframe", 3, map[string]interface{}{
"Person": "Bob",
"Timeframe": T("d_days", 2),
}))
// Output:
// Hello world
// Hello Bob
// Hello Bob
// You have 0 unread emails.
// You have 1 unread email.
// You have 2 unread emails.
// I am 1.7 meters tall.
// Bob has 0 unread emails.
// Bob has 1 unread email.
// Bob has 2 unread emails.
// Bob has 0 unread emails.
// Bob has 1 unread email.
// Bob has 2 unread emails.
// Bob has 3 unread emails in the past 0 days.
// Bob has 3 unread emails in the past 1 day.
// Bob has 3 unread emails in the past 2 days.
}

View File

@ -0,0 +1,63 @@
package i18n_test
import (
"github.com/nicksnyder/go-i18n/i18n"
"os"
"text/template"
)
var funcMap = map[string]interface{}{
"T": i18n.IdentityTfunc,
}
var tmpl = template.Must(template.New("").Funcs(funcMap).Parse(`
{{T "program_greeting"}}
{{T "person_greeting" .}}
{{T "your_unread_email_count" 0}}
{{T "your_unread_email_count" 1}}
{{T "your_unread_email_count" 2}}
{{T "person_unread_email_count" 0 .}}
{{T "person_unread_email_count" 1 .}}
{{T "person_unread_email_count" 2 .}}
`))
func Example_template() {
i18n.MustLoadTranslationFile("../goi18n/testdata/expected/en-us.all.json")
T, _ := i18n.Tfunc("en-US")
tmpl.Funcs(map[string]interface{}{
"T": T,
})
tmpl.Execute(os.Stdout, map[string]interface{}{
"Person": "Bob",
"Timeframe": T("d_days", 1),
})
tmpl.Execute(os.Stdout, struct {
Person string
Timeframe string
}{
Person: "Bob",
Timeframe: T("d_days", 1),
})
// Output:
// Hello world
// Hello Bob
// You have 0 unread emails.
// You have 1 unread email.
// You have 2 unread emails.
// Bob has 0 unread emails.
// Bob has 1 unread email.
// Bob has 2 unread emails.
//
// Hello world
// Hello Bob
// You have 0 unread emails.
// You have 1 unread email.
// You have 2 unread emails.
// Bob has 0 unread emails.
// Bob has 1 unread email.
// Bob has 2 unread emails.
}

View File

@ -0,0 +1,62 @@
package i18n_test
import (
"fmt"
"github.com/nicksnyder/go-i18n/i18n"
)
func ExampleYAML() {
i18n.MustLoadTranslationFile("../goi18n/testdata/en-us.yaml")
T, _ := i18n.Tfunc("en-US")
bobMap := map[string]interface{}{"Person": "Bob"}
bobStruct := struct{ Person string }{Person: "Bob"}
fmt.Println(T("program_greeting"))
fmt.Println(T("person_greeting", bobMap))
fmt.Println(T("person_greeting", bobStruct))
fmt.Println(T("your_unread_email_count", 0))
fmt.Println(T("your_unread_email_count", 1))
fmt.Println(T("your_unread_email_count", 2))
fmt.Println(T("my_height_in_meters", "1.7"))
fmt.Println(T("person_unread_email_count", 0, bobMap))
fmt.Println(T("person_unread_email_count", 1, bobMap))
fmt.Println(T("person_unread_email_count", 2, bobMap))
fmt.Println(T("person_unread_email_count", 0, bobStruct))
fmt.Println(T("person_unread_email_count", 1, bobStruct))
fmt.Println(T("person_unread_email_count", 2, bobStruct))
fmt.Println(T("person_unread_email_count_timeframe", 3, map[string]interface{}{
"Person": "Bob",
"Timeframe": T("d_days", 0),
}))
fmt.Println(T("person_unread_email_count_timeframe", 3, map[string]interface{}{
"Person": "Bob",
"Timeframe": T("d_days", 1),
}))
fmt.Println(T("person_unread_email_count_timeframe", 3, map[string]interface{}{
"Person": "Bob",
"Timeframe": T("d_days", 2),
}))
// Output:
// Hello world
// Hello Bob
// Hello Bob
// You have 0 unread emails.
// You have 1 unread email.
// You have 2 unread emails.
// I am 1.7 meters tall.
// Bob has 0 unread emails.
// Bob has 1 unread email.
// Bob has 2 unread emails.
// Bob has 0 unread emails.
// Bob has 1 unread email.
// Bob has 2 unread emails.
// Bob has 3 unread emails in the past 0 days.
// Bob has 3 unread emails in the past 1 day.
// Bob has 3 unread emails in the past 2 days.
}

152
vendor/github.com/nicksnyder/go-i18n/i18n/i18n.go generated vendored Normal file
View File

@ -0,0 +1,152 @@
// Package i18n supports string translations with variable substitution and CLDR pluralization.
// It is intended to be used in conjunction with the goi18n command, although that is not strictly required.
//
// Initialization
//
// Your Go program should load translations during its initialization.
// i18n.MustLoadTranslationFile("path/to/fr-FR.all.json")
// If your translations are in a file format not supported by (Must)?LoadTranslationFile,
// then you can use the AddTranslation function to manually add translations.
//
// Fetching a translation
//
// Use Tfunc or MustTfunc to fetch a TranslateFunc that will return the translated string for a specific language.
// func handleRequest(w http.ResponseWriter, r *http.Request) {
// cookieLang := r.Cookie("lang")
// acceptLang := r.Header.Get("Accept-Language")
// defaultLang = "en-US" // known valid language
// T, err := i18n.Tfunc(cookieLang, acceptLang, defaultLang)
// fmt.Println(T("Hello world"))
// }
//
// Usually it is a good idea to identify strings by a generic id rather than the English translation,
// but the rest of this documentation will continue to use the English translation for readability.
// T("Hello world") // ok
// T("programGreeting") // better!
//
// Variables
//
// TranslateFunc supports strings that have variables using the text/template syntax.
// T("Hello {{.Person}}", map[string]interface{}{
// "Person": "Bob",
// })
//
// Pluralization
//
// TranslateFunc supports the pluralization of strings using the CLDR pluralization rules defined here:
// http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html
// T("You have {{.Count}} unread emails.", 2)
// T("I am {{.Count}} meters tall.", "1.7")
//
// Plural strings may also have variables.
// T("{{.Person}} has {{.Count}} unread emails", 2, map[string]interface{}{
// "Person": "Bob",
// })
//
// Sentences with multiple plural components can be supported with nesting.
// T("{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}.", 3, map[string]interface{}{
// "Person": "Bob",
// "Timeframe": T("{{.Count}} days", 2),
// })
//
// Templates
//
// You can use the .Funcs() method of a text/template or html/template to register a TranslateFunc
// for usage inside of that template.
package i18n
import (
"github.com/nicksnyder/go-i18n/i18n/bundle"
"github.com/nicksnyder/go-i18n/i18n/language"
"github.com/nicksnyder/go-i18n/i18n/translation"
)
// TranslateFunc returns the translation of the string identified by translationID.
//
// If there is no translation for translationID, then the translationID itself is returned.
// This makes it easy to identify missing translations in your app.
//
// If translationID is a non-plural form, then the first variadic argument may be a map[string]interface{}
// or struct that contains template data.
//
// If translationID is a plural form, then the first variadic argument must be an integer type
// (int, int8, int16, int32, int64) or a float formatted as a string (e.g. "123.45").
// The second variadic argument may be a map[string]interface{} or struct that contains template data.
type TranslateFunc func(translationID string, args ...interface{}) string
// IdentityTfunc returns a TranslateFunc that always returns the translationID passed to it.
//
// It is a useful placeholder when parsing a text/template or html/template
// before the actual Tfunc is available.
func IdentityTfunc() TranslateFunc {
return func(translationID string, args ...interface{}) string {
return translationID
}
}
var defaultBundle = bundle.New()
// MustLoadTranslationFile is similar to LoadTranslationFile
// except it panics if an error happens.
func MustLoadTranslationFile(filename string) {
defaultBundle.MustLoadTranslationFile(filename)
}
// LoadTranslationFile loads the translations from filename into memory.
//
// The language that the translations are associated with is parsed from the filename (e.g. en-US.json).
//
// Generally you should load translation files once during your program's initialization.
func LoadTranslationFile(filename string) error {
return defaultBundle.LoadTranslationFile(filename)
}
// ParseTranslationFileBytes is similar to LoadTranslationFile except it parses the bytes in buf.
//
// It is useful for parsing translation files embedded with go-bindata.
func ParseTranslationFileBytes(filename string, buf []byte) error {
return defaultBundle.ParseTranslationFileBytes(filename, buf)
}
// AddTranslation adds translations for a language.
//
// It is useful if your translations are in a format not supported by LoadTranslationFile.
func AddTranslation(lang *language.Language, translations ...translation.Translation) {
defaultBundle.AddTranslation(lang, translations...)
}
// LanguageTags returns the tags of all languages that have been added.
func LanguageTags() []string {
return defaultBundle.LanguageTags()
}
// LanguageTranslationIDs returns the ids of all translations that have been added for a given language.
func LanguageTranslationIDs(languageTag string) []string {
return defaultBundle.LanguageTranslationIDs(languageTag)
}
// MustTfunc is similar to Tfunc except it panics if an error happens.
func MustTfunc(languageSource string, languageSources ...string) TranslateFunc {
return TranslateFunc(defaultBundle.MustTfunc(languageSource, languageSources...))
}
// Tfunc returns a TranslateFunc that will be bound to the first language which
// has a non-zero number of translations.
//
// It can parse languages from Accept-Language headers (RFC 2616).
func Tfunc(languageSource string, languageSources ...string) (TranslateFunc, error) {
tfunc, err := defaultBundle.Tfunc(languageSource, languageSources...)
return TranslateFunc(tfunc), err
}
// MustTfuncAndLanguage is similar to TfuncAndLanguage except it panics if an error happens.
func MustTfuncAndLanguage(languageSource string, languageSources ...string) (TranslateFunc, *language.Language) {
tfunc, lang := defaultBundle.MustTfuncAndLanguage(languageSource, languageSources...)
return TranslateFunc(tfunc), lang
}
// TfuncAndLanguage is similar to Tfunc except it also returns the language which TranslateFunc is bound to.
func TfuncAndLanguage(languageSource string, languageSources ...string) (TranslateFunc, *language.Language, error) {
tfunc, lang, err := defaultBundle.TfuncAndLanguage(languageSource, languageSources...)
return TranslateFunc(tfunc), lang, err
}

View File

@ -0,0 +1,5 @@
#!/bin/sh
go build && ./codegen -cout ../pluralspec_gen.go -tout ../pluralspec_gen_test.go && \
gofmt -w=true ../pluralspec_gen.go && \
gofmt -w=true ../pluralspec_gen_test.go && \
rm codegen

View File

@ -0,0 +1,132 @@
package main
import (
"encoding/xml"
"flag"
"fmt"
"io/ioutil"
"os"
"text/template"
)
var usage = `%[1]s generates Go code to support CLDR plural rules.
Usage: %[1]s [options]
Options:
`
func main() {
flag.Usage = func() {
fmt.Fprintf(os.Stderr, usage, os.Args[0])
flag.PrintDefaults()
}
var in, cout, tout string
flag.StringVar(&in, "i", "plurals.xml", "the input XML file containing CLDR plural rules")
flag.StringVar(&cout, "cout", "", "the code output file")
flag.StringVar(&tout, "tout", "", "the test output file")
flag.BoolVar(&verbose, "v", false, "verbose output")
flag.Parse()
buf, err := ioutil.ReadFile(in)
if err != nil {
fatalf("failed to read file: %s", err)
}
var data SupplementalData
if err := xml.Unmarshal(buf, &data); err != nil {
fatalf("failed to unmarshal xml: %s", err)
}
count := 0
for _, pg := range data.PluralGroups {
count += len(pg.SplitLocales())
}
infof("parsed %d locales", count)
if cout != "" {
file := openWritableFile(cout)
if err := codeTemplate.Execute(file, data); err != nil {
fatalf("unable to execute code template because %s", err)
} else {
infof("generated %s", cout)
}
} else {
infof("not generating code file (use -cout)")
}
if tout != "" {
file := openWritableFile(tout)
if err := testTemplate.Execute(file, data); err != nil {
fatalf("unable to execute test template because %s", err)
} else {
infof("generated %s", tout)
}
} else {
infof("not generating test file (use -tout)")
}
}
func openWritableFile(name string) *os.File {
file, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
fatalf("failed to write file %s because %s", name, err)
}
return file
}
var codeTemplate = template.Must(template.New("spec").Parse(`package language
// This file is generated by i18n/language/codegen/generate.sh
func init() {
{{range .PluralGroups}}
registerPluralSpec({{printf "%#v" .SplitLocales}}, &PluralSpec{
Plurals: newPluralSet({{range $i, $e := .PluralRules}}{{if $i}}, {{end}}{{$e.CountTitle}}{{end}}),
PluralFunc: func(ops *operands) Plural { {{range .PluralRules}}{{if .GoCondition}}
// {{.Condition}}
if {{.GoCondition}} {
return {{.CountTitle}}
}{{end}}{{end}}
return Other
},
}){{end}}
}
`))
var testTemplate = template.Must(template.New("spec").Parse(`package language
// This file is generated by i18n/language/codegen/generate.sh
import "testing"
{{range .PluralGroups}}
func Test{{.Name}}(t *testing.T) {
var tests []pluralTest
{{range .PluralRules}}
{{if .IntegerExamples}}tests = appendIntegerTests(tests, {{.CountTitle}}, {{printf "%#v" .IntegerExamples}}){{end}}
{{if .DecimalExamples}}tests = appendDecimalTests(tests, {{.CountTitle}}, {{printf "%#v" .DecimalExamples}}){{end}}
{{end}}
locales := {{printf "%#v" .SplitLocales}}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
{{end}}
`))
func infof(format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, format+"\n", args...)
}
var verbose bool
func verbosef(format string, args ...interface{}) {
if verbose {
infof(format, args...)
}
}
func fatalf(format string, args ...interface{}) {
infof("fatal: "+format+"\n", args...)
os.Exit(1)
}

View File

@ -0,0 +1,230 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE supplementalData SYSTEM "../../common/dtd/ldmlSupplemental.dtd">
<!--
Copyright © 1991-2015 Unicode, Inc.
CLDR data files are interpreted according to the LDML specification (http://unicode.org/reports/tr35/)
For terms of use, see http://www.unicode.org/copyright.html
-->
<supplementalData>
<version number="$Revision: 12002 $"/>
<plurals type="cardinal">
<!-- For a canonicalized list, use GeneratedPluralSamples -->
<!-- 1: other -->
<pluralRules locales="bm bo dz id ig ii in ja jbo jv jw kde kea km ko lkt lo ms my nqo root sah ses sg th to vi wo yo zh">
<pluralRule count="other"> @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<!-- 2: one,other -->
<pluralRules locales="am as bn fa gu hi kn mr zu">
<pluralRule count="one">i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04</pluralRule>
<pluralRule count="other"> @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~2.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="ff fr hy kab">
<pluralRule count="one">i = 0,1 @integer 0, 1 @decimal 0.0~1.5</pluralRule>
<pluralRule count="other"> @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="ast ca de en et fi fy gl it ji nl sv sw ur yi">
<pluralRule count="one">i = 1 and v = 0 @integer 1</pluralRule>
<pluralRule count="other"> @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="si">
<pluralRule count="one">n = 0,1 or i = 0 and f = 1 @integer 0, 1 @decimal 0.0, 0.1, 1.0, 0.00, 0.01, 1.00, 0.000, 0.001, 1.000, 0.0000, 0.0001, 1.0000</pluralRule>
<pluralRule count="other"> @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.2~0.9, 1.1~1.8, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="ak bh guw ln mg nso pa ti wa">
<pluralRule count="one">n = 0..1 @integer 0, 1 @decimal 0.0, 1.0, 0.00, 1.00, 0.000, 1.000, 0.0000, 1.0000</pluralRule>
<pluralRule count="other"> @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="tzm">
<pluralRule count="one">n = 0..1 or n = 11..99 @integer 0, 1, 11~24 @decimal 0.0, 1.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0, 21.0, 22.0, 23.0, 24.0</pluralRule>
<pluralRule count="other"> @integer 2~10, 100~106, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="pt">
<pluralRule count="one">n = 0..2 and n != 2 @integer 0, 1 @decimal 0.0, 1.0, 0.00, 1.00, 0.000, 1.000, 0.0000, 1.0000</pluralRule>
<pluralRule count="other"> @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="af asa az bem bez bg brx ce cgg chr ckb dv ee el eo es eu fo fur gsw ha haw hu jgo jmc ka kaj kcg kk kkj kl ks ksb ku ky lb lg mas mgo ml mn nah nb nd ne nn nnh no nr ny nyn om or os pap ps rm rof rwk saq sdh seh sn so sq ss ssy st syr ta te teo tig tk tn tr ts ug uz ve vo vun wae xh xog">
<pluralRule count="one">n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000</pluralRule>
<pluralRule count="other"> @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="pt_PT">
<pluralRule count="one">n = 1 and v = 0 @integer 1</pluralRule>
<pluralRule count="other"> @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="da">
<pluralRule count="one">n = 1 or t != 0 and i = 0,1 @integer 1 @decimal 0.1~1.6</pluralRule>
<pluralRule count="other"> @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 2.0~3.4, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="is">
<pluralRule count="one">t = 0 and i % 10 = 1 and i % 100 != 11 or t != 0 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1~1.6, 10.1, 100.1, 1000.1, …</pluralRule>
<pluralRule count="other"> @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="mk">
<pluralRule count="one">v = 0 and i % 10 = 1 or f % 10 = 1 @integer 1, 11, 21, 31, 41, 51, 61, 71, 101, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …</pluralRule>
<pluralRule count="other"> @integer 0, 2~10, 12~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.2~1.0, 1.2~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="fil tl">
<pluralRule count="one">v = 0 and i = 1,2,3 or v = 0 and i % 10 != 4,6,9 or v != 0 and f % 10 != 4,6,9 @integer 0~3, 5, 7, 8, 10~13, 15, 17, 18, 20, 21, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.3, 0.5, 0.7, 0.8, 1.0~1.3, 1.5, 1.7, 1.8, 2.0, 2.1, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
<pluralRule count="other"> @integer 4, 6, 9, 14, 16, 19, 24, 26, 104, 1004, … @decimal 0.4, 0.6, 0.9, 1.4, 1.6, 1.9, 2.4, 2.6, 10.4, 100.4, 1000.4, …</pluralRule>
</pluralRules>
<!-- 3: zero,one,other -->
<pluralRules locales="lv prg">
<pluralRule count="zero">n % 10 = 0 or n % 100 = 11..19 or v = 2 and f % 100 = 11..19 @integer 0, 10~20, 30, 40, 50, 60, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
<pluralRule count="one">n % 10 = 1 and n % 100 != 11 or v = 2 and f % 10 = 1 and f % 100 != 11 or v != 2 and f % 10 = 1 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.0, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …</pluralRule>
<pluralRule count="other"> @integer 2~9, 22~29, 102, 1002, … @decimal 0.2~0.9, 1.2~1.9, 10.2, 100.2, 1000.2, …</pluralRule>
</pluralRules>
<pluralRules locales="lag">
<pluralRule count="zero">n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000</pluralRule>
<pluralRule count="one">i = 0,1 and n != 0 @integer 1 @decimal 0.1~1.6</pluralRule>
<pluralRule count="other"> @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="ksh">
<pluralRule count="zero">n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000</pluralRule>
<pluralRule count="one">n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000</pluralRule>
<pluralRule count="other"> @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<!-- 3: one,two,other -->
<pluralRules locales="iu kw naq se sma smi smj smn sms">
<pluralRule count="one">n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000</pluralRule>
<pluralRule count="two">n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000</pluralRule>
<pluralRule count="other"> @integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<!-- 3: one,few,other -->
<pluralRules locales="shi">
<pluralRule count="one">i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04</pluralRule>
<pluralRule count="few">n = 2..10 @integer 2~10 @decimal 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 2.00, 3.00, 4.00, 5.00, 6.00, 7.00, 8.00</pluralRule>
<pluralRule count="other"> @integer 11~26, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~1.9, 2.1~2.7, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="mo ro">
<pluralRule count="one">i = 1 and v = 0 @integer 1</pluralRule>
<pluralRule count="few">v != 0 or n = 0 or n != 1 and n % 100 = 1..19 @integer 0, 2~16, 101, 1001, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
<pluralRule count="other"> @integer 20~35, 100, 1000, 10000, 100000, 1000000, …</pluralRule>
</pluralRules>
<pluralRules locales="bs hr sh sr">
<pluralRule count="one">v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …</pluralRule>
<pluralRule count="few">v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, … @decimal 0.2~0.4, 1.2~1.4, 2.2~2.4, 3.2~3.4, 4.2~4.4, 5.2, 10.2, 100.2, 1000.2, …</pluralRule>
<pluralRule count="other"> @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.5~1.0, 1.5~2.0, 2.5~2.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<!-- 4: one,two,few,other -->
<pluralRules locales="gd">
<pluralRule count="one">n = 1,11 @integer 1, 11 @decimal 1.0, 11.0, 1.00, 11.00, 1.000, 11.000, 1.0000</pluralRule>
<pluralRule count="two">n = 2,12 @integer 2, 12 @decimal 2.0, 12.0, 2.00, 12.00, 2.000, 12.000, 2.0000</pluralRule>
<pluralRule count="few">n = 3..10,13..19 @integer 3~10, 13~19 @decimal 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 3.00</pluralRule>
<pluralRule count="other"> @integer 0, 20~34, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="sl">
<pluralRule count="one">v = 0 and i % 100 = 1 @integer 1, 101, 201, 301, 401, 501, 601, 701, 1001, …</pluralRule>
<pluralRule count="two">v = 0 and i % 100 = 2 @integer 2, 102, 202, 302, 402, 502, 602, 702, 1002, …</pluralRule>
<pluralRule count="few">v = 0 and i % 100 = 3..4 or v != 0 @integer 3, 4, 103, 104, 203, 204, 303, 304, 403, 404, 503, 504, 603, 604, 703, 704, 1003, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
<pluralRule count="other"> @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …</pluralRule>
</pluralRules>
<pluralRules locales="dsb hsb">
<pluralRule count="one">v = 0 and i % 100 = 1 or f % 100 = 1 @integer 1, 101, 201, 301, 401, 501, 601, 701, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …</pluralRule>
<pluralRule count="two">v = 0 and i % 100 = 2 or f % 100 = 2 @integer 2, 102, 202, 302, 402, 502, 602, 702, 1002, … @decimal 0.2, 1.2, 2.2, 3.2, 4.2, 5.2, 6.2, 7.2, 10.2, 100.2, 1000.2, …</pluralRule>
<pluralRule count="few">v = 0 and i % 100 = 3..4 or f % 100 = 3..4 @integer 3, 4, 103, 104, 203, 204, 303, 304, 403, 404, 503, 504, 603, 604, 703, 704, 1003, … @decimal 0.3, 0.4, 1.3, 1.4, 2.3, 2.4, 3.3, 3.4, 4.3, 4.4, 5.3, 5.4, 6.3, 6.4, 7.3, 7.4, 10.3, 100.3, 1000.3, …</pluralRule>
<pluralRule count="other"> @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.5~1.0, 1.5~2.0, 2.5~2.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<!-- 4: one,two,many,other -->
<pluralRules locales="he iw">
<pluralRule count="one">i = 1 and v = 0 @integer 1</pluralRule>
<pluralRule count="two">i = 2 and v = 0 @integer 2</pluralRule>
<pluralRule count="many">v = 0 and n != 0..10 and n % 10 = 0 @integer 20, 30, 40, 50, 60, 70, 80, 90, 100, 1000, 10000, 100000, 1000000, …</pluralRule>
<pluralRule count="other"> @integer 0, 3~17, 101, 1001, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<!-- 4: one,few,many,other -->
<pluralRules locales="cs sk">
<pluralRule count="one">i = 1 and v = 0 @integer 1</pluralRule>
<pluralRule count="few">i = 2..4 and v = 0 @integer 2~4</pluralRule>
<pluralRule count="many">v != 0 @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
<pluralRule count="other"> @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …</pluralRule>
</pluralRules>
<pluralRules locales="pl">
<pluralRule count="one">i = 1 and v = 0 @integer 1</pluralRule>
<pluralRule count="few">v = 0 and i % 10 = 2..4 and i % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, …</pluralRule>
<pluralRule count="many">v = 0 and i != 1 and i % 10 = 0..1 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 12..14 @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …</pluralRule>
<pluralRule count="other"> @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="be">
<pluralRule count="one">n % 10 = 1 and n % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 71.0, 81.0, 101.0, 1001.0, …</pluralRule>
<pluralRule count="few">n % 10 = 2..4 and n % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, … @decimal 2.0, 3.0, 4.0, 22.0, 23.0, 24.0, 32.0, 33.0, 102.0, 1002.0, …</pluralRule>
<pluralRule count="many">n % 10 = 0 or n % 10 = 5..9 or n % 100 = 11..14 @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
<pluralRule count="other"> @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.1, 1000.1, …</pluralRule>
</pluralRules>
<pluralRules locales="lt">
<pluralRule count="one">n % 10 = 1 and n % 100 != 11..19 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 71.0, 81.0, 101.0, 1001.0, …</pluralRule>
<pluralRule count="few">n % 10 = 2..9 and n % 100 != 11..19 @integer 2~9, 22~29, 102, 1002, … @decimal 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 22.0, 102.0, 1002.0, …</pluralRule>
<pluralRule count="many">f != 0 @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.1, 1000.1, …</pluralRule>
<pluralRule count="other"> @integer 0, 10~20, 30, 40, 50, 60, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="mt">
<pluralRule count="one">n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000</pluralRule>
<pluralRule count="few">n = 0 or n % 100 = 2..10 @integer 0, 2~10, 102~107, 1002, … @decimal 0.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 10.0, 102.0, 1002.0, …</pluralRule>
<pluralRule count="many">n % 100 = 11..19 @integer 11~19, 111~117, 1011, … @decimal 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 111.0, 1011.0, …</pluralRule>
<pluralRule count="other"> @integer 20~35, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="ru uk">
<pluralRule count="one">v = 0 and i % 10 = 1 and i % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …</pluralRule>
<pluralRule count="few">v = 0 and i % 10 = 2..4 and i % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, …</pluralRule>
<pluralRule count="many">v = 0 and i % 10 = 0 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 11..14 @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …</pluralRule>
<pluralRule count="other"> @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<!-- 5: one,two,few,many,other -->
<pluralRules locales="br">
<pluralRule count="one">n % 10 = 1 and n % 100 != 11,71,91 @integer 1, 21, 31, 41, 51, 61, 81, 101, 1001, … @decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 81.0, 101.0, 1001.0, …</pluralRule>
<pluralRule count="two">n % 10 = 2 and n % 100 != 12,72,92 @integer 2, 22, 32, 42, 52, 62, 82, 102, 1002, … @decimal 2.0, 22.0, 32.0, 42.0, 52.0, 62.0, 82.0, 102.0, 1002.0, …</pluralRule>
<pluralRule count="few">n % 10 = 3..4,9 and n % 100 != 10..19,70..79,90..99 @integer 3, 4, 9, 23, 24, 29, 33, 34, 39, 43, 44, 49, 103, 1003, … @decimal 3.0, 4.0, 9.0, 23.0, 24.0, 29.0, 33.0, 34.0, 103.0, 1003.0, …</pluralRule>
<pluralRule count="many">n != 0 and n % 1000000 = 0 @integer 1000000, … @decimal 1000000.0, 1000000.00, 1000000.000, …</pluralRule>
<pluralRule count="other"> @integer 0, 5~8, 10~20, 100, 1000, 10000, 100000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="ga">
<pluralRule count="one">n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000</pluralRule>
<pluralRule count="two">n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000</pluralRule>
<pluralRule count="few">n = 3..6 @integer 3~6 @decimal 3.0, 4.0, 5.0, 6.0, 3.00, 4.00, 5.00, 6.00, 3.000, 4.000, 5.000, 6.000, 3.0000, 4.0000, 5.0000, 6.0000</pluralRule>
<pluralRule count="many">n = 7..10 @integer 7~10 @decimal 7.0, 8.0, 9.0, 10.0, 7.00, 8.00, 9.00, 10.00, 7.000, 8.000, 9.000, 10.000, 7.0000, 8.0000, 9.0000, 10.0000</pluralRule>
<pluralRule count="other"> @integer 0, 11~25, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="gv">
<pluralRule count="one">v = 0 and i % 10 = 1 @integer 1, 11, 21, 31, 41, 51, 61, 71, 101, 1001, …</pluralRule>
<pluralRule count="two">v = 0 and i % 10 = 2 @integer 2, 12, 22, 32, 42, 52, 62, 72, 102, 1002, …</pluralRule>
<pluralRule count="few">v = 0 and i % 100 = 0,20,40,60,80 @integer 0, 20, 40, 60, 80, 100, 120, 140, 1000, 10000, 100000, 1000000, …</pluralRule>
<pluralRule count="many">v != 0 @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
<pluralRule count="other"> @integer 3~10, 13~19, 23, 103, 1003, …</pluralRule>
</pluralRules>
<!-- 6: zero,one,two,few,many,other -->
<pluralRules locales="ar">
<pluralRule count="zero">n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000</pluralRule>
<pluralRule count="one">n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000</pluralRule>
<pluralRule count="two">n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000</pluralRule>
<pluralRule count="few">n % 100 = 3..10 @integer 3~10, 103~110, 1003, … @decimal 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 103.0, 1003.0, …</pluralRule>
<pluralRule count="many">n % 100 = 11..99 @integer 11~26, 111, 1011, … @decimal 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 111.0, 1011.0, …</pluralRule>
<pluralRule count="other"> @integer 100~102, 200~202, 300~302, 400~402, 500~502, 600, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="cy">
<pluralRule count="zero">n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000</pluralRule>
<pluralRule count="one">n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000</pluralRule>
<pluralRule count="two">n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000</pluralRule>
<pluralRule count="few">n = 3 @integer 3 @decimal 3.0, 3.00, 3.000, 3.0000</pluralRule>
<pluralRule count="many">n = 6 @integer 6 @decimal 6.0, 6.00, 6.000, 6.0000</pluralRule>
<pluralRule count="other"> @integer 4, 5, 7~20, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
</plurals>
</supplementalData>

View File

@ -0,0 +1,143 @@
package main
import (
"encoding/xml"
"fmt"
"regexp"
"strings"
)
// SupplementalData is the top level struct of plural.xml
type SupplementalData struct {
XMLName xml.Name `xml:"supplementalData"`
PluralGroups []PluralGroup `xml:"plurals>pluralRules"`
}
// PluralGroup is a group of locales with the same plural rules.
type PluralGroup struct {
Locales string `xml:"locales,attr"`
PluralRules []PluralRule `xml:"pluralRule"`
}
// Name returns a unique name for this plural group.
func (pg *PluralGroup) Name() string {
n := strings.Title(pg.Locales)
return strings.Replace(n, " ", "", -1)
}
// SplitLocales returns all the locales in the PluralGroup as a slice.
func (pg *PluralGroup) SplitLocales() []string {
return strings.Split(pg.Locales, " ")
}
// PluralRule is the rule for a single plural form.
type PluralRule struct {
Count string `xml:"count,attr"`
Rule string `xml:",innerxml"`
}
// CountTitle returns the title case of the PluralRule's count.
func (pr *PluralRule) CountTitle() string {
return strings.Title(pr.Count)
}
// Condition returns the condition where the PluralRule applies.
func (pr *PluralRule) Condition() string {
i := strings.Index(pr.Rule, "@")
return pr.Rule[:i]
}
// Examples returns the integer and decimal exmaples for the PLuralRule.
func (pr *PluralRule) Examples() (integer []string, decimal []string) {
ex := strings.Replace(pr.Rule, ", …", "", -1)
ddelim := "@decimal"
if i := strings.Index(ex, ddelim); i > 0 {
dex := strings.TrimSpace(ex[i+len(ddelim):])
decimal = strings.Split(dex, ", ")
ex = ex[:i]
}
idelim := "@integer"
if i := strings.Index(ex, idelim); i > 0 {
iex := strings.TrimSpace(ex[i+len(idelim):])
integer = strings.Split(iex, ", ")
}
return integer, decimal
}
// IntegerExamples returns the integer exmaples for the PLuralRule.
func (pr *PluralRule) IntegerExamples() []string {
integer, _ := pr.Examples()
return integer
}
// DecimalExamples returns the decimal exmaples for the PLuralRule.
func (pr *PluralRule) DecimalExamples() []string {
_, decimal := pr.Examples()
return decimal
}
var relationRegexp = regexp.MustCompile("([niftvw])(?: % ([0-9]+))? (!=|=)(.*)")
// GoCondition converts the XML condition to valid Go code.
func (pr *PluralRule) GoCondition() string {
var ors []string
for _, and := range strings.Split(pr.Condition(), "or") {
var ands []string
for _, relation := range strings.Split(and, "and") {
parts := relationRegexp.FindStringSubmatch(relation)
if parts == nil {
continue
}
lvar, lmod, op, rhs := strings.Title(parts[1]), parts[2], parts[3], strings.TrimSpace(parts[4])
if op == "=" {
op = "=="
}
lvar = "ops." + lvar
var rhor []string
var rany []string
for _, rh := range strings.Split(rhs, ",") {
if parts := strings.Split(rh, ".."); len(parts) == 2 {
from, to := parts[0], parts[1]
if lvar == "ops.N" {
if lmod != "" {
rhor = append(rhor, fmt.Sprintf("ops.NmodInRange(%s, %s, %s)", lmod, from, to))
} else {
rhor = append(rhor, fmt.Sprintf("ops.NinRange(%s, %s)", from, to))
}
} else if lmod != "" {
rhor = append(rhor, fmt.Sprintf("intInRange(%s %% %s, %s, %s)", lvar, lmod, from, to))
} else {
rhor = append(rhor, fmt.Sprintf("intInRange(%s, %s, %s)", lvar, from, to))
}
} else {
rany = append(rany, rh)
}
}
if len(rany) > 0 {
rh := strings.Join(rany, ",")
if lvar == "ops.N" {
if lmod != "" {
rhor = append(rhor, fmt.Sprintf("ops.NmodEqualsAny(%s, %s)", lmod, rh))
} else {
rhor = append(rhor, fmt.Sprintf("ops.NequalsAny(%s)", rh))
}
} else if lmod != "" {
rhor = append(rhor, fmt.Sprintf("intEqualsAny(%s %% %s, %s)", lvar, lmod, rh))
} else {
rhor = append(rhor, fmt.Sprintf("intEqualsAny(%s, %s)", lvar, rh))
}
}
r := strings.Join(rhor, " || ")
if len(rhor) > 1 {
r = "(" + r + ")"
}
if op == "!=" {
r = "!" + r
}
ands = append(ands, r)
}
ors = append(ors, strings.Join(ands, " && "))
}
return strings.Join(ors, " ||\n")
}

View File

@ -0,0 +1,99 @@
// Package language defines languages that implement CLDR pluralization.
package language
import (
"fmt"
"strings"
)
// Language is a written human language.
type Language struct {
// Tag uniquely identifies the language as defined by RFC 5646.
//
// Most language tags are a two character language code (ISO 639-1)
// optionally followed by a dash and a two character country code (ISO 3166-1).
// (e.g. en, pt-br)
Tag string
*PluralSpec
}
func (l *Language) String() string {
return l.Tag
}
// MatchingTags returns the set of language tags that map to this Language.
// e.g. "zh-hans-cn" yields {"zh", "zh-hans", "zh-hans-cn"}
// BUG: This should be computed once and stored as a field on Language for efficiency,
// but this would require changing how Languages are constructed.
func (l *Language) MatchingTags() []string {
parts := strings.Split(l.Tag, "-")
var prefix, matches []string
for _, part := range parts {
prefix = append(prefix, part)
match := strings.Join(prefix, "-")
matches = append(matches, match)
}
return matches
}
// Parse returns a slice of supported languages found in src or nil if none are found.
// It can parse language tags and Accept-Language headers.
func Parse(src string) []*Language {
var langs []*Language
start := 0
for end, chr := range src {
switch chr {
case ',', ';', '.':
tag := strings.TrimSpace(src[start:end])
if spec := getPluralSpec(tag); spec != nil {
langs = append(langs, &Language{NormalizeTag(tag), spec})
}
start = end + 1
}
}
if start > 0 {
tag := strings.TrimSpace(src[start:])
if spec := getPluralSpec(tag); spec != nil {
langs = append(langs, &Language{NormalizeTag(tag), spec})
}
return dedupe(langs)
}
if spec := getPluralSpec(src); spec != nil {
langs = append(langs, &Language{NormalizeTag(src), spec})
}
return langs
}
func dedupe(langs []*Language) []*Language {
found := make(map[string]struct{}, len(langs))
deduped := make([]*Language, 0, len(langs))
for _, lang := range langs {
if _, ok := found[lang.Tag]; !ok {
found[lang.Tag] = struct{}{}
deduped = append(deduped, lang)
}
}
return deduped
}
// MustParse is similar to Parse except it panics instead of retuning a nil Language.
func MustParse(src string) []*Language {
langs := Parse(src)
if len(langs) == 0 {
panic(fmt.Errorf("unable to parse language from %q", src))
}
return langs
}
// Add adds support for a new language.
func Add(l *Language) {
tag := NormalizeTag(l.Tag)
pluralSpecs[tag] = l.PluralSpec
}
// NormalizeTag returns a language tag with all lower-case characters
// and dashes "-" instead of underscores "_"
func NormalizeTag(tag string) string {
tag = strings.ToLower(tag)
return strings.Replace(tag, "_", "-", -1)
}

View File

@ -0,0 +1,85 @@
package language
import (
"reflect"
"testing"
)
func TestParse(t *testing.T) {
tests := []struct {
src string
lang []*Language
}{
{"en", []*Language{{"en", pluralSpecs["en"]}}},
{"en-US", []*Language{{"en-us", pluralSpecs["en"]}}},
{"en_US", []*Language{{"en-us", pluralSpecs["en"]}}},
{"en-GB", []*Language{{"en-gb", pluralSpecs["en"]}}},
{"zh-CN", []*Language{{"zh-cn", pluralSpecs["zh"]}}},
{"zh-TW", []*Language{{"zh-tw", pluralSpecs["zh"]}}},
{"pt-BR", []*Language{{"pt-br", pluralSpecs["pt"]}}},
{"pt_BR", []*Language{{"pt-br", pluralSpecs["pt"]}}},
{"pt-PT", []*Language{{"pt-pt", pluralSpecs["pt-pt"]}}},
{"pt_PT", []*Language{{"pt-pt", pluralSpecs["pt-pt"]}}},
{"zh-Hans-CN", []*Language{{"zh-hans-cn", pluralSpecs["zh"]}}},
{"zh-Hant-TW", []*Language{{"zh-hant-tw", pluralSpecs["zh"]}}},
{"en-US-en-US", []*Language{{"en-us-en-us", pluralSpecs["en"]}}},
{".en-US..en-US.", []*Language{{"en-us", pluralSpecs["en"]}}},
{
"it, xx-zz, xx-ZZ, zh, en-gb;q=0.8, en;q=0.7, es-ES;q=0.6, de-xx",
[]*Language{
{"it", pluralSpecs["it"]},
{"zh", pluralSpecs["zh"]},
{"en-gb", pluralSpecs["en"]},
{"en", pluralSpecs["en"]},
{"es-es", pluralSpecs["es"]},
{"de-xx", pluralSpecs["de"]},
},
},
{
"it-qq,xx,xx-zz,xx-ZZ,zh,en-gb;q=0.8,en;q=0.7,es-ES;q=0.6,de-xx",
[]*Language{
{"it-qq", pluralSpecs["it"]},
{"zh", pluralSpecs["zh"]},
{"en-gb", pluralSpecs["en"]},
{"en", pluralSpecs["en"]},
{"es-es", pluralSpecs["es"]},
{"de-xx", pluralSpecs["de"]},
},
},
{"en.json", []*Language{{"en", pluralSpecs["en"]}}},
{"en-US.json", []*Language{{"en-us", pluralSpecs["en"]}}},
{"en-us.json", []*Language{{"en-us", pluralSpecs["en"]}}},
{"en-xx.json", []*Language{{"en-xx", pluralSpecs["en"]}}},
{"xx-Yyen-US", nil},
{"en US", nil},
{"", nil},
{"-", nil},
{"_", nil},
{"-en", nil},
{"_en", nil},
{"-en-", nil},
{"_en_", nil},
{"xx", nil},
}
for _, test := range tests {
lang := Parse(test.src)
if !reflect.DeepEqual(lang, test.lang) {
t.Errorf("Parse(%q) = %s expected %s", test.src, lang, test.lang)
}
}
}
func TestMatchingTags(t *testing.T) {
tests := []struct {
lang *Language
matches []string
}{
{&Language{"zh-hans-cn", nil}, []string{"zh", "zh-hans", "zh-hans-cn"}},
{&Language{"foo", nil}, []string{"foo"}},
}
for _, test := range tests {
if actual := test.lang.MatchingTags(); !reflect.DeepEqual(test.matches, actual) {
t.Errorf("matchingTags(%q) = %q expected %q", test.lang.Tag, actual, test.matches)
}
}
}

View File

@ -0,0 +1,119 @@
package language
import (
"fmt"
"strconv"
"strings"
)
// http://unicode.org/reports/tr35/tr35-numbers.html#Operands
type operands struct {
N float64 // absolute value of the source number (integer and decimals)
I int64 // integer digits of n
V int64 // number of visible fraction digits in n, with trailing zeros
W int64 // number of visible fraction digits in n, without trailing zeros
F int64 // visible fractional digits in n, with trailing zeros
T int64 // visible fractional digits in n, without trailing zeros
}
// NmodEqualAny returns true if o represents an integer equal to any of the arguments.
func (o *operands) NequalsAny(any ...int64) bool {
for _, i := range any {
if o.I == i && o.T == 0 {
return true
}
}
return false
}
// NmodEqualAny returns true if o represents an integer equal to any of the arguments modulo mod.
func (o *operands) NmodEqualsAny(mod int64, any ...int64) bool {
modI := o.I % mod
for _, i := range any {
if modI == i && o.T == 0 {
return true
}
}
return false
}
// NmodInRange returns true if o represents an integer in the closed interval [from, to].
func (o *operands) NinRange(from, to int64) bool {
return o.T == 0 && from <= o.I && o.I <= to
}
// NmodInRange returns true if o represents an integer in the closed interval [from, to] modulo mod.
func (o *operands) NmodInRange(mod, from, to int64) bool {
modI := o.I % mod
return o.T == 0 && from <= modI && modI <= to
}
func newOperands(v interface{}) (*operands, error) {
switch v := v.(type) {
case int:
return newOperandsInt64(int64(v)), nil
case int8:
return newOperandsInt64(int64(v)), nil
case int16:
return newOperandsInt64(int64(v)), nil
case int32:
return newOperandsInt64(int64(v)), nil
case int64:
return newOperandsInt64(v), nil
case string:
return newOperandsString(v)
case float32, float64:
return nil, fmt.Errorf("floats should be formatted into a string")
default:
return nil, fmt.Errorf("invalid type %T; expected integer or string", v)
}
}
func newOperandsInt64(i int64) *operands {
if i < 0 {
i = -i
}
return &operands{float64(i), i, 0, 0, 0, 0}
}
func newOperandsString(s string) (*operands, error) {
if s[0] == '-' {
s = s[1:]
}
n, err := strconv.ParseFloat(s, 64)
if err != nil {
return nil, err
}
ops := &operands{N: n}
parts := strings.SplitN(s, ".", 2)
ops.I, err = strconv.ParseInt(parts[0], 10, 64)
if err != nil {
return nil, err
}
if len(parts) == 1 {
return ops, nil
}
fraction := parts[1]
ops.V = int64(len(fraction))
for i := ops.V - 1; i >= 0; i-- {
if fraction[i] != '0' {
ops.W = i + 1
break
}
}
if ops.V > 0 {
f, err := strconv.ParseInt(fraction, 10, 0)
if err != nil {
return nil, err
}
ops.F = f
}
if ops.W > 0 {
t, err := strconv.ParseInt(fraction[:ops.W], 10, 0)
if err != nil {
return nil, err
}
ops.T = t
}
return ops, nil
}

View File

@ -0,0 +1,45 @@
package language
import (
"reflect"
"testing"
)
func TestNewOperands(t *testing.T) {
tests := []struct {
input interface{}
ops *operands
err bool
}{
{int64(0), &operands{0.0, 0, 0, 0, 0, 0}, false},
{int64(1), &operands{1.0, 1, 0, 0, 0, 0}, false},
{"0", &operands{0.0, 0, 0, 0, 0, 0}, false},
{"1", &operands{1.0, 1, 0, 0, 0, 0}, false},
{"1.0", &operands{1.0, 1, 1, 0, 0, 0}, false},
{"1.00", &operands{1.0, 1, 2, 0, 0, 0}, false},
{"1.3", &operands{1.3, 1, 1, 1, 3, 3}, false},
{"1.30", &operands{1.3, 1, 2, 1, 30, 3}, false},
{"1.03", &operands{1.03, 1, 2, 2, 3, 3}, false},
{"1.230", &operands{1.23, 1, 3, 2, 230, 23}, false},
{"20.0230", &operands{20.023, 20, 4, 3, 230, 23}, false},
{20.0230, nil, true},
}
for _, test := range tests {
ops, err := newOperands(test.input)
if err != nil && !test.err {
t.Errorf("newOperands(%#v) unexpected error: %s", test.input, err)
} else if err == nil && test.err {
t.Errorf("newOperands(%#v) returned %#v; expected error", test.input, ops)
} else if !reflect.DeepEqual(ops, test.ops) {
t.Errorf("newOperands(%#v) returned %#v; expected %#v", test.input, ops, test.ops)
}
}
}
func BenchmarkNewOperand(b *testing.B) {
for i := 0; i < b.N; i++ {
if _, err := newOperands("1234.56780000"); err != nil {
b.Fatal(err)
}
}
}

View File

@ -0,0 +1,40 @@
package language
import (
"fmt"
)
// Plural represents a language pluralization form as defined here:
// http://cldr.unicode.org/index/cldr-spec/plural-rules
type Plural string
// All defined plural categories.
const (
Invalid Plural = "invalid"
Zero = "zero"
One = "one"
Two = "two"
Few = "few"
Many = "many"
Other = "other"
)
// NewPlural returns src as a Plural
// or Invalid and a non-nil error if src is not a valid Plural.
func NewPlural(src string) (Plural, error) {
switch src {
case "zero":
return Zero, nil
case "one":
return One, nil
case "two":
return Two, nil
case "few":
return Few, nil
case "many":
return Many, nil
case "other":
return Other, nil
}
return Invalid, fmt.Errorf("invalid plural category %s", src)
}

View File

@ -0,0 +1,28 @@
package language
import (
"testing"
)
func TestNewPlural(t *testing.T) {
tests := []struct {
src string
plural Plural
err bool
}{
{"zero", Zero, false},
{"one", One, false},
{"two", Two, false},
{"few", Few, false},
{"many", Many, false},
{"other", Other, false},
{"asdf", Invalid, true},
}
for _, test := range tests {
plural, err := NewPlural(test.src)
wrongErr := (err != nil && !test.err) || (err == nil && test.err)
if plural != test.plural || wrongErr {
t.Errorf("NewPlural(%#v) returned %#v,%#v; expected %#v", test.src, plural, err, test.plural)
}
}
}

View File

@ -0,0 +1,74 @@
package language
import "strings"
// PluralSpec defines the CLDR plural rules for a language.
// http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html
// http://unicode.org/reports/tr35/tr35-numbers.html#Operands
type PluralSpec struct {
Plurals map[Plural]struct{}
PluralFunc func(*operands) Plural
}
var pluralSpecs = make(map[string]*PluralSpec)
func normalizePluralSpecID(id string) string {
id = strings.Replace(id, "_", "-", -1)
id = strings.ToLower(id)
return id
}
func registerPluralSpec(ids []string, ps *PluralSpec) {
for _, id := range ids {
id = normalizePluralSpecID(id)
pluralSpecs[id] = ps
}
}
// Plural returns the plural category for number as defined by
// the language's CLDR plural rules.
func (ps *PluralSpec) Plural(number interface{}) (Plural, error) {
ops, err := newOperands(number)
if err != nil {
return Invalid, err
}
return ps.PluralFunc(ops), nil
}
// getPluralSpec returns the PluralSpec that matches the longest prefix of tag.
// It returns nil if no PluralSpec matches tag.
func getPluralSpec(tag string) *PluralSpec {
tag = NormalizeTag(tag)
subtag := tag
for {
if spec := pluralSpecs[subtag]; spec != nil {
return spec
}
end := strings.LastIndex(subtag, "-")
if end == -1 {
return nil
}
subtag = subtag[:end]
}
}
func newPluralSet(plurals ...Plural) map[Plural]struct{} {
set := make(map[Plural]struct{}, len(plurals))
for _, plural := range plurals {
set[plural] = struct{}{}
}
return set
}
func intInRange(i, from, to int64) bool {
return from <= i && i <= to
}
func intEqualsAny(i int64, any ...int64) bool {
for _, a := range any {
if i == a {
return true
}
}
return false
}

View File

@ -0,0 +1,567 @@
package language
// This file is generated by i18n/language/codegen/generate.sh
func init() {
registerPluralSpec([]string{"bm", "bo", "dz", "id", "ig", "ii", "in", "ja", "jbo", "jv", "jw", "kde", "kea", "km", "ko", "lkt", "lo", "ms", "my", "nqo", "root", "sah", "ses", "sg", "th", "to", "vi", "wo", "yo", "zh"}, &PluralSpec{
Plurals: newPluralSet(Other),
PluralFunc: func(ops *operands) Plural {
return Other
},
})
registerPluralSpec([]string{"am", "as", "bn", "fa", "gu", "hi", "kn", "mr", "zu"}, &PluralSpec{
Plurals: newPluralSet(One, Other),
PluralFunc: func(ops *operands) Plural {
// i = 0 or n = 1
if intEqualsAny(ops.I, 0) ||
ops.NequalsAny(1) {
return One
}
return Other
},
})
registerPluralSpec([]string{"ff", "fr", "hy", "kab"}, &PluralSpec{
Plurals: newPluralSet(One, Other),
PluralFunc: func(ops *operands) Plural {
// i = 0,1
if intEqualsAny(ops.I, 0, 1) {
return One
}
return Other
},
})
registerPluralSpec([]string{"ast", "ca", "de", "en", "et", "fi", "fy", "gl", "it", "ji", "nl", "sv", "sw", "ur", "yi"}, &PluralSpec{
Plurals: newPluralSet(One, Other),
PluralFunc: func(ops *operands) Plural {
// i = 1 and v = 0
if intEqualsAny(ops.I, 1) && intEqualsAny(ops.V, 0) {
return One
}
return Other
},
})
registerPluralSpec([]string{"si"}, &PluralSpec{
Plurals: newPluralSet(One, Other),
PluralFunc: func(ops *operands) Plural {
// n = 0,1 or i = 0 and f = 1
if ops.NequalsAny(0, 1) ||
intEqualsAny(ops.I, 0) && intEqualsAny(ops.F, 1) {
return One
}
return Other
},
})
registerPluralSpec([]string{"ak", "bh", "guw", "ln", "mg", "nso", "pa", "ti", "wa"}, &PluralSpec{
Plurals: newPluralSet(One, Other),
PluralFunc: func(ops *operands) Plural {
// n = 0..1
if ops.NinRange(0, 1) {
return One
}
return Other
},
})
registerPluralSpec([]string{"tzm"}, &PluralSpec{
Plurals: newPluralSet(One, Other),
PluralFunc: func(ops *operands) Plural {
// n = 0..1 or n = 11..99
if ops.NinRange(0, 1) ||
ops.NinRange(11, 99) {
return One
}
return Other
},
})
registerPluralSpec([]string{"pt"}, &PluralSpec{
Plurals: newPluralSet(One, Other),
PluralFunc: func(ops *operands) Plural {
// n = 0..2 and n != 2
if ops.NinRange(0, 2) && !ops.NequalsAny(2) {
return One
}
return Other
},
})
registerPluralSpec([]string{"af", "asa", "az", "bem", "bez", "bg", "brx", "ce", "cgg", "chr", "ckb", "dv", "ee", "el", "eo", "es", "eu", "fo", "fur", "gsw", "ha", "haw", "hu", "jgo", "jmc", "ka", "kaj", "kcg", "kk", "kkj", "kl", "ks", "ksb", "ku", "ky", "lb", "lg", "mas", "mgo", "ml", "mn", "nah", "nb", "nd", "ne", "nn", "nnh", "no", "nr", "ny", "nyn", "om", "or", "os", "pap", "ps", "rm", "rof", "rwk", "saq", "sdh", "seh", "sn", "so", "sq", "ss", "ssy", "st", "syr", "ta", "te", "teo", "tig", "tk", "tn", "tr", "ts", "ug", "uz", "ve", "vo", "vun", "wae", "xh", "xog"}, &PluralSpec{
Plurals: newPluralSet(One, Other),
PluralFunc: func(ops *operands) Plural {
// n = 1
if ops.NequalsAny(1) {
return One
}
return Other
},
})
registerPluralSpec([]string{"pt_PT"}, &PluralSpec{
Plurals: newPluralSet(One, Other),
PluralFunc: func(ops *operands) Plural {
// n = 1 and v = 0
if ops.NequalsAny(1) && intEqualsAny(ops.V, 0) {
return One
}
return Other
},
})
registerPluralSpec([]string{"da"}, &PluralSpec{
Plurals: newPluralSet(One, Other),
PluralFunc: func(ops *operands) Plural {
// n = 1 or t != 0 and i = 0,1
if ops.NequalsAny(1) ||
!intEqualsAny(ops.T, 0) && intEqualsAny(ops.I, 0, 1) {
return One
}
return Other
},
})
registerPluralSpec([]string{"is"}, &PluralSpec{
Plurals: newPluralSet(One, Other),
PluralFunc: func(ops *operands) Plural {
// t = 0 and i % 10 = 1 and i % 100 != 11 or t != 0
if intEqualsAny(ops.T, 0) && intEqualsAny(ops.I%10, 1) && !intEqualsAny(ops.I%100, 11) ||
!intEqualsAny(ops.T, 0) {
return One
}
return Other
},
})
registerPluralSpec([]string{"mk"}, &PluralSpec{
Plurals: newPluralSet(One, Other),
PluralFunc: func(ops *operands) Plural {
// v = 0 and i % 10 = 1 or f % 10 = 1
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 1) ||
intEqualsAny(ops.F%10, 1) {
return One
}
return Other
},
})
registerPluralSpec([]string{"fil", "tl"}, &PluralSpec{
Plurals: newPluralSet(One, Other),
PluralFunc: func(ops *operands) Plural {
// v = 0 and i = 1,2,3 or v = 0 and i % 10 != 4,6,9 or v != 0 and f % 10 != 4,6,9
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I, 1, 2, 3) ||
intEqualsAny(ops.V, 0) && !intEqualsAny(ops.I%10, 4, 6, 9) ||
!intEqualsAny(ops.V, 0) && !intEqualsAny(ops.F%10, 4, 6, 9) {
return One
}
return Other
},
})
registerPluralSpec([]string{"lv", "prg"}, &PluralSpec{
Plurals: newPluralSet(Zero, One, Other),
PluralFunc: func(ops *operands) Plural {
// n % 10 = 0 or n % 100 = 11..19 or v = 2 and f % 100 = 11..19
if ops.NmodEqualsAny(10, 0) ||
ops.NmodInRange(100, 11, 19) ||
intEqualsAny(ops.V, 2) && intInRange(ops.F%100, 11, 19) {
return Zero
}
// n % 10 = 1 and n % 100 != 11 or v = 2 and f % 10 = 1 and f % 100 != 11 or v != 2 and f % 10 = 1
if ops.NmodEqualsAny(10, 1) && !ops.NmodEqualsAny(100, 11) ||
intEqualsAny(ops.V, 2) && intEqualsAny(ops.F%10, 1) && !intEqualsAny(ops.F%100, 11) ||
!intEqualsAny(ops.V, 2) && intEqualsAny(ops.F%10, 1) {
return One
}
return Other
},
})
registerPluralSpec([]string{"lag"}, &PluralSpec{
Plurals: newPluralSet(Zero, One, Other),
PluralFunc: func(ops *operands) Plural {
// n = 0
if ops.NequalsAny(0) {
return Zero
}
// i = 0,1 and n != 0
if intEqualsAny(ops.I, 0, 1) && !ops.NequalsAny(0) {
return One
}
return Other
},
})
registerPluralSpec([]string{"ksh"}, &PluralSpec{
Plurals: newPluralSet(Zero, One, Other),
PluralFunc: func(ops *operands) Plural {
// n = 0
if ops.NequalsAny(0) {
return Zero
}
// n = 1
if ops.NequalsAny(1) {
return One
}
return Other
},
})
registerPluralSpec([]string{"iu", "kw", "naq", "se", "sma", "smi", "smj", "smn", "sms"}, &PluralSpec{
Plurals: newPluralSet(One, Two, Other),
PluralFunc: func(ops *operands) Plural {
// n = 1
if ops.NequalsAny(1) {
return One
}
// n = 2
if ops.NequalsAny(2) {
return Two
}
return Other
},
})
registerPluralSpec([]string{"shi"}, &PluralSpec{
Plurals: newPluralSet(One, Few, Other),
PluralFunc: func(ops *operands) Plural {
// i = 0 or n = 1
if intEqualsAny(ops.I, 0) ||
ops.NequalsAny(1) {
return One
}
// n = 2..10
if ops.NinRange(2, 10) {
return Few
}
return Other
},
})
registerPluralSpec([]string{"mo", "ro"}, &PluralSpec{
Plurals: newPluralSet(One, Few, Other),
PluralFunc: func(ops *operands) Plural {
// i = 1 and v = 0
if intEqualsAny(ops.I, 1) && intEqualsAny(ops.V, 0) {
return One
}
// v != 0 or n = 0 or n != 1 and n % 100 = 1..19
if !intEqualsAny(ops.V, 0) ||
ops.NequalsAny(0) ||
!ops.NequalsAny(1) && ops.NmodInRange(100, 1, 19) {
return Few
}
return Other
},
})
registerPluralSpec([]string{"bs", "hr", "sh", "sr"}, &PluralSpec{
Plurals: newPluralSet(One, Few, Other),
PluralFunc: func(ops *operands) Plural {
// v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 1) && !intEqualsAny(ops.I%100, 11) ||
intEqualsAny(ops.F%10, 1) && !intEqualsAny(ops.F%100, 11) {
return One
}
// v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14
if intEqualsAny(ops.V, 0) && intInRange(ops.I%10, 2, 4) && !intInRange(ops.I%100, 12, 14) ||
intInRange(ops.F%10, 2, 4) && !intInRange(ops.F%100, 12, 14) {
return Few
}
return Other
},
})
registerPluralSpec([]string{"gd"}, &PluralSpec{
Plurals: newPluralSet(One, Two, Few, Other),
PluralFunc: func(ops *operands) Plural {
// n = 1,11
if ops.NequalsAny(1, 11) {
return One
}
// n = 2,12
if ops.NequalsAny(2, 12) {
return Two
}
// n = 3..10,13..19
if ops.NinRange(3, 10) || ops.NinRange(13, 19) {
return Few
}
return Other
},
})
registerPluralSpec([]string{"sl"}, &PluralSpec{
Plurals: newPluralSet(One, Two, Few, Other),
PluralFunc: func(ops *operands) Plural {
// v = 0 and i % 100 = 1
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%100, 1) {
return One
}
// v = 0 and i % 100 = 2
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%100, 2) {
return Two
}
// v = 0 and i % 100 = 3..4 or v != 0
if intEqualsAny(ops.V, 0) && intInRange(ops.I%100, 3, 4) ||
!intEqualsAny(ops.V, 0) {
return Few
}
return Other
},
})
registerPluralSpec([]string{"dsb", "hsb"}, &PluralSpec{
Plurals: newPluralSet(One, Two, Few, Other),
PluralFunc: func(ops *operands) Plural {
// v = 0 and i % 100 = 1 or f % 100 = 1
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%100, 1) ||
intEqualsAny(ops.F%100, 1) {
return One
}
// v = 0 and i % 100 = 2 or f % 100 = 2
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%100, 2) ||
intEqualsAny(ops.F%100, 2) {
return Two
}
// v = 0 and i % 100 = 3..4 or f % 100 = 3..4
if intEqualsAny(ops.V, 0) && intInRange(ops.I%100, 3, 4) ||
intInRange(ops.F%100, 3, 4) {
return Few
}
return Other
},
})
registerPluralSpec([]string{"he", "iw"}, &PluralSpec{
Plurals: newPluralSet(One, Two, Many, Other),
PluralFunc: func(ops *operands) Plural {
// i = 1 and v = 0
if intEqualsAny(ops.I, 1) && intEqualsAny(ops.V, 0) {
return One
}
// i = 2 and v = 0
if intEqualsAny(ops.I, 2) && intEqualsAny(ops.V, 0) {
return Two
}
// v = 0 and n != 0..10 and n % 10 = 0
if intEqualsAny(ops.V, 0) && !ops.NinRange(0, 10) && ops.NmodEqualsAny(10, 0) {
return Many
}
return Other
},
})
registerPluralSpec([]string{"cs", "sk"}, &PluralSpec{
Plurals: newPluralSet(One, Few, Many, Other),
PluralFunc: func(ops *operands) Plural {
// i = 1 and v = 0
if intEqualsAny(ops.I, 1) && intEqualsAny(ops.V, 0) {
return One
}
// i = 2..4 and v = 0
if intInRange(ops.I, 2, 4) && intEqualsAny(ops.V, 0) {
return Few
}
// v != 0
if !intEqualsAny(ops.V, 0) {
return Many
}
return Other
},
})
registerPluralSpec([]string{"pl"}, &PluralSpec{
Plurals: newPluralSet(One, Few, Many, Other),
PluralFunc: func(ops *operands) Plural {
// i = 1 and v = 0
if intEqualsAny(ops.I, 1) && intEqualsAny(ops.V, 0) {
return One
}
// v = 0 and i % 10 = 2..4 and i % 100 != 12..14
if intEqualsAny(ops.V, 0) && intInRange(ops.I%10, 2, 4) && !intInRange(ops.I%100, 12, 14) {
return Few
}
// v = 0 and i != 1 and i % 10 = 0..1 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 12..14
if intEqualsAny(ops.V, 0) && !intEqualsAny(ops.I, 1) && intInRange(ops.I%10, 0, 1) ||
intEqualsAny(ops.V, 0) && intInRange(ops.I%10, 5, 9) ||
intEqualsAny(ops.V, 0) && intInRange(ops.I%100, 12, 14) {
return Many
}
return Other
},
})
registerPluralSpec([]string{"be"}, &PluralSpec{
Plurals: newPluralSet(One, Few, Many, Other),
PluralFunc: func(ops *operands) Plural {
// n % 10 = 1 and n % 100 != 11
if ops.NmodEqualsAny(10, 1) && !ops.NmodEqualsAny(100, 11) {
return One
}
// n % 10 = 2..4 and n % 100 != 12..14
if ops.NmodInRange(10, 2, 4) && !ops.NmodInRange(100, 12, 14) {
return Few
}
// n % 10 = 0 or n % 10 = 5..9 or n % 100 = 11..14
if ops.NmodEqualsAny(10, 0) ||
ops.NmodInRange(10, 5, 9) ||
ops.NmodInRange(100, 11, 14) {
return Many
}
return Other
},
})
registerPluralSpec([]string{"lt"}, &PluralSpec{
Plurals: newPluralSet(One, Few, Many, Other),
PluralFunc: func(ops *operands) Plural {
// n % 10 = 1 and n % 100 != 11..19
if ops.NmodEqualsAny(10, 1) && !ops.NmodInRange(100, 11, 19) {
return One
}
// n % 10 = 2..9 and n % 100 != 11..19
if ops.NmodInRange(10, 2, 9) && !ops.NmodInRange(100, 11, 19) {
return Few
}
// f != 0
if !intEqualsAny(ops.F, 0) {
return Many
}
return Other
},
})
registerPluralSpec([]string{"mt"}, &PluralSpec{
Plurals: newPluralSet(One, Few, Many, Other),
PluralFunc: func(ops *operands) Plural {
// n = 1
if ops.NequalsAny(1) {
return One
}
// n = 0 or n % 100 = 2..10
if ops.NequalsAny(0) ||
ops.NmodInRange(100, 2, 10) {
return Few
}
// n % 100 = 11..19
if ops.NmodInRange(100, 11, 19) {
return Many
}
return Other
},
})
registerPluralSpec([]string{"ru", "uk"}, &PluralSpec{
Plurals: newPluralSet(One, Few, Many, Other),
PluralFunc: func(ops *operands) Plural {
// v = 0 and i % 10 = 1 and i % 100 != 11
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 1) && !intEqualsAny(ops.I%100, 11) {
return One
}
// v = 0 and i % 10 = 2..4 and i % 100 != 12..14
if intEqualsAny(ops.V, 0) && intInRange(ops.I%10, 2, 4) && !intInRange(ops.I%100, 12, 14) {
return Few
}
// v = 0 and i % 10 = 0 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 11..14
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 0) ||
intEqualsAny(ops.V, 0) && intInRange(ops.I%10, 5, 9) ||
intEqualsAny(ops.V, 0) && intInRange(ops.I%100, 11, 14) {
return Many
}
return Other
},
})
registerPluralSpec([]string{"br"}, &PluralSpec{
Plurals: newPluralSet(One, Two, Few, Many, Other),
PluralFunc: func(ops *operands) Plural {
// n % 10 = 1 and n % 100 != 11,71,91
if ops.NmodEqualsAny(10, 1) && !ops.NmodEqualsAny(100, 11, 71, 91) {
return One
}
// n % 10 = 2 and n % 100 != 12,72,92
if ops.NmodEqualsAny(10, 2) && !ops.NmodEqualsAny(100, 12, 72, 92) {
return Two
}
// n % 10 = 3..4,9 and n % 100 != 10..19,70..79,90..99
if (ops.NmodInRange(10, 3, 4) || ops.NmodEqualsAny(10, 9)) && !(ops.NmodInRange(100, 10, 19) || ops.NmodInRange(100, 70, 79) || ops.NmodInRange(100, 90, 99)) {
return Few
}
// n != 0 and n % 1000000 = 0
if !ops.NequalsAny(0) && ops.NmodEqualsAny(1000000, 0) {
return Many
}
return Other
},
})
registerPluralSpec([]string{"ga"}, &PluralSpec{
Plurals: newPluralSet(One, Two, Few, Many, Other),
PluralFunc: func(ops *operands) Plural {
// n = 1
if ops.NequalsAny(1) {
return One
}
// n = 2
if ops.NequalsAny(2) {
return Two
}
// n = 3..6
if ops.NinRange(3, 6) {
return Few
}
// n = 7..10
if ops.NinRange(7, 10) {
return Many
}
return Other
},
})
registerPluralSpec([]string{"gv"}, &PluralSpec{
Plurals: newPluralSet(One, Two, Few, Many, Other),
PluralFunc: func(ops *operands) Plural {
// v = 0 and i % 10 = 1
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 1) {
return One
}
// v = 0 and i % 10 = 2
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 2) {
return Two
}
// v = 0 and i % 100 = 0,20,40,60,80
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%100, 0, 20, 40, 60, 80) {
return Few
}
// v != 0
if !intEqualsAny(ops.V, 0) {
return Many
}
return Other
},
})
registerPluralSpec([]string{"ar"}, &PluralSpec{
Plurals: newPluralSet(Zero, One, Two, Few, Many, Other),
PluralFunc: func(ops *operands) Plural {
// n = 0
if ops.NequalsAny(0) {
return Zero
}
// n = 1
if ops.NequalsAny(1) {
return One
}
// n = 2
if ops.NequalsAny(2) {
return Two
}
// n % 100 = 3..10
if ops.NmodInRange(100, 3, 10) {
return Few
}
// n % 100 = 11..99
if ops.NmodInRange(100, 11, 99) {
return Many
}
return Other
},
})
registerPluralSpec([]string{"cy"}, &PluralSpec{
Plurals: newPluralSet(Zero, One, Two, Few, Many, Other),
PluralFunc: func(ops *operands) Plural {
// n = 0
if ops.NequalsAny(0) {
return Zero
}
// n = 1
if ops.NequalsAny(1) {
return One
}
// n = 2
if ops.NequalsAny(2) {
return Two
}
// n = 3
if ops.NequalsAny(3) {
return Few
}
// n = 6
if ops.NequalsAny(6) {
return Many
}
return Other
},
})
}

View File

@ -0,0 +1,645 @@
package language
// This file is generated by i18n/language/codegen/generate.sh
import "testing"
func TestBmBoDzIdIgIiInJaJboJvJwKdeKeaKmKoLktLoMsMyNqoRootSahSesSgThToViWoYoZh(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, Other, []string{"0~15", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"bm", "bo", "dz", "id", "ig", "ii", "in", "ja", "jbo", "jv", "jw", "kde", "kea", "km", "ko", "lkt", "lo", "ms", "my", "nqo", "root", "sah", "ses", "sg", "th", "to", "vi", "wo", "yo", "zh"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestAmAsBnFaGuHiKnMrZu(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, One, []string{"0", "1"})
tests = appendDecimalTests(tests, One, []string{"0.0~1.0", "0.00~0.04"})
tests = appendIntegerTests(tests, Other, []string{"2~17", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"1.1~2.6", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"am", "as", "bn", "fa", "gu", "hi", "kn", "mr", "zu"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestFfFrHyKab(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, One, []string{"0", "1"})
tests = appendDecimalTests(tests, One, []string{"0.0~1.5"})
tests = appendIntegerTests(tests, Other, []string{"2~17", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"2.0~3.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"ff", "fr", "hy", "kab"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestAstCaDeEnEtFiFyGlItJiNlSvSwUrYi(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, One, []string{"1"})
tests = appendIntegerTests(tests, Other, []string{"0", "2~16", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"ast", "ca", "de", "en", "et", "fi", "fy", "gl", "it", "ji", "nl", "sv", "sw", "ur", "yi"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestSi(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, One, []string{"0", "1"})
tests = appendDecimalTests(tests, One, []string{"0.0", "0.1", "1.0", "0.00", "0.01", "1.00", "0.000", "0.001", "1.000", "0.0000", "0.0001", "1.0000"})
tests = appendIntegerTests(tests, Other, []string{"2~17", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.2~0.9", "1.1~1.8", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"si"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestAkBhGuwLnMgNsoPaTiWa(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, One, []string{"0", "1"})
tests = appendDecimalTests(tests, One, []string{"0.0", "1.0", "0.00", "1.00", "0.000", "1.000", "0.0000", "1.0000"})
tests = appendIntegerTests(tests, Other, []string{"2~17", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.1~0.9", "1.1~1.7", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"ak", "bh", "guw", "ln", "mg", "nso", "pa", "ti", "wa"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestTzm(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, One, []string{"0", "1", "11~24"})
tests = appendDecimalTests(tests, One, []string{"0.0", "1.0", "11.0", "12.0", "13.0", "14.0", "15.0", "16.0", "17.0", "18.0", "19.0", "20.0", "21.0", "22.0", "23.0", "24.0"})
tests = appendIntegerTests(tests, Other, []string{"2~10", "100~106", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.1~0.9", "1.1~1.7", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"tzm"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestPt(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, One, []string{"0", "1"})
tests = appendDecimalTests(tests, One, []string{"0.0", "1.0", "0.00", "1.00", "0.000", "1.000", "0.0000", "1.0000"})
tests = appendIntegerTests(tests, Other, []string{"2~17", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.1~0.9", "1.1~1.7", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"pt"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestAfAsaAzBemBezBgBrxCeCggChrCkbDvEeElEoEsEuFoFurGswHaHawHuJgoJmcKaKajKcgKkKkjKlKsKsbKuKyLbLgMasMgoMlMnNahNbNdNeNnNnhNoNrNyNynOmOrOsPapPsRmRofRwkSaqSdhSehSnSoSqSsSsyStSyrTaTeTeoTigTkTnTrTsUgUzVeVoVunWaeXhXog(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, One, []string{"1"})
tests = appendDecimalTests(tests, One, []string{"1.0", "1.00", "1.000", "1.0000"})
tests = appendIntegerTests(tests, Other, []string{"0", "2~16", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.0~0.9", "1.1~1.6", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"af", "asa", "az", "bem", "bez", "bg", "brx", "ce", "cgg", "chr", "ckb", "dv", "ee", "el", "eo", "es", "eu", "fo", "fur", "gsw", "ha", "haw", "hu", "jgo", "jmc", "ka", "kaj", "kcg", "kk", "kkj", "kl", "ks", "ksb", "ku", "ky", "lb", "lg", "mas", "mgo", "ml", "mn", "nah", "nb", "nd", "ne", "nn", "nnh", "no", "nr", "ny", "nyn", "om", "or", "os", "pap", "ps", "rm", "rof", "rwk", "saq", "sdh", "seh", "sn", "so", "sq", "ss", "ssy", "st", "syr", "ta", "te", "teo", "tig", "tk", "tn", "tr", "ts", "ug", "uz", "ve", "vo", "vun", "wae", "xh", "xog"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestPt_PT(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, One, []string{"1"})
tests = appendIntegerTests(tests, Other, []string{"0", "2~16", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"pt_PT"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestDa(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, One, []string{"1"})
tests = appendDecimalTests(tests, One, []string{"0.1~1.6"})
tests = appendIntegerTests(tests, Other, []string{"0", "2~16", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.0", "2.0~3.4", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"da"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestIs(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, One, []string{"1", "21", "31", "41", "51", "61", "71", "81", "101", "1001"})
tests = appendDecimalTests(tests, One, []string{"0.1~1.6", "10.1", "100.1", "1000.1"})
tests = appendIntegerTests(tests, Other, []string{"0", "2~16", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.0", "2.0", "3.0", "4.0", "5.0", "6.0", "7.0", "8.0", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"is"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestMk(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, One, []string{"1", "11", "21", "31", "41", "51", "61", "71", "101", "1001"})
tests = appendDecimalTests(tests, One, []string{"0.1", "1.1", "2.1", "3.1", "4.1", "5.1", "6.1", "7.1", "10.1", "100.1", "1000.1"})
tests = appendIntegerTests(tests, Other, []string{"0", "2~10", "12~17", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.0", "0.2~1.0", "1.2~1.7", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"mk"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestFilTl(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, One, []string{"0~3", "5", "7", "8", "10~13", "15", "17", "18", "20", "21", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, One, []string{"0.0~0.3", "0.5", "0.7", "0.8", "1.0~1.3", "1.5", "1.7", "1.8", "2.0", "2.1", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
tests = appendIntegerTests(tests, Other, []string{"4", "6", "9", "14", "16", "19", "24", "26", "104", "1004"})
tests = appendDecimalTests(tests, Other, []string{"0.4", "0.6", "0.9", "1.4", "1.6", "1.9", "2.4", "2.6", "10.4", "100.4", "1000.4"})
locales := []string{"fil", "tl"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestLvPrg(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, Zero, []string{"0", "10~20", "30", "40", "50", "60", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Zero, []string{"0.0", "10.0", "11.0", "12.0", "13.0", "14.0", "15.0", "16.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
tests = appendIntegerTests(tests, One, []string{"1", "21", "31", "41", "51", "61", "71", "81", "101", "1001"})
tests = appendDecimalTests(tests, One, []string{"0.1", "1.0", "1.1", "2.1", "3.1", "4.1", "5.1", "6.1", "7.1", "10.1", "100.1", "1000.1"})
tests = appendIntegerTests(tests, Other, []string{"2~9", "22~29", "102", "1002"})
tests = appendDecimalTests(tests, Other, []string{"0.2~0.9", "1.2~1.9", "10.2", "100.2", "1000.2"})
locales := []string{"lv", "prg"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestLag(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, Zero, []string{"0"})
tests = appendDecimalTests(tests, Zero, []string{"0.0", "0.00", "0.000", "0.0000"})
tests = appendIntegerTests(tests, One, []string{"1"})
tests = appendDecimalTests(tests, One, []string{"0.1~1.6"})
tests = appendIntegerTests(tests, Other, []string{"2~17", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"2.0~3.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"lag"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestKsh(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, Zero, []string{"0"})
tests = appendDecimalTests(tests, Zero, []string{"0.0", "0.00", "0.000", "0.0000"})
tests = appendIntegerTests(tests, One, []string{"1"})
tests = appendDecimalTests(tests, One, []string{"1.0", "1.00", "1.000", "1.0000"})
tests = appendIntegerTests(tests, Other, []string{"2~17", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.1~0.9", "1.1~1.7", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"ksh"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestIuKwNaqSeSmaSmiSmjSmnSms(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, One, []string{"1"})
tests = appendDecimalTests(tests, One, []string{"1.0", "1.00", "1.000", "1.0000"})
tests = appendIntegerTests(tests, Two, []string{"2"})
tests = appendDecimalTests(tests, Two, []string{"2.0", "2.00", "2.000", "2.0000"})
tests = appendIntegerTests(tests, Other, []string{"0", "3~17", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.0~0.9", "1.1~1.6", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"iu", "kw", "naq", "se", "sma", "smi", "smj", "smn", "sms"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestShi(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, One, []string{"0", "1"})
tests = appendDecimalTests(tests, One, []string{"0.0~1.0", "0.00~0.04"})
tests = appendIntegerTests(tests, Few, []string{"2~10"})
tests = appendDecimalTests(tests, Few, []string{"2.0", "3.0", "4.0", "5.0", "6.0", "7.0", "8.0", "9.0", "10.0", "2.00", "3.00", "4.00", "5.00", "6.00", "7.00", "8.00"})
tests = appendIntegerTests(tests, Other, []string{"11~26", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"1.1~1.9", "2.1~2.7", "10.1", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"shi"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestMoRo(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, One, []string{"1"})
tests = appendIntegerTests(tests, Few, []string{"0", "2~16", "101", "1001"})
tests = appendDecimalTests(tests, Few, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
tests = appendIntegerTests(tests, Other, []string{"20~35", "100", "1000", "10000", "100000", "1000000"})
locales := []string{"mo", "ro"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestBsHrShSr(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, One, []string{"1", "21", "31", "41", "51", "61", "71", "81", "101", "1001"})
tests = appendDecimalTests(tests, One, []string{"0.1", "1.1", "2.1", "3.1", "4.1", "5.1", "6.1", "7.1", "10.1", "100.1", "1000.1"})
tests = appendIntegerTests(tests, Few, []string{"2~4", "22~24", "32~34", "42~44", "52~54", "62", "102", "1002"})
tests = appendDecimalTests(tests, Few, []string{"0.2~0.4", "1.2~1.4", "2.2~2.4", "3.2~3.4", "4.2~4.4", "5.2", "10.2", "100.2", "1000.2"})
tests = appendIntegerTests(tests, Other, []string{"0", "5~19", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.0", "0.5~1.0", "1.5~2.0", "2.5~2.7", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"bs", "hr", "sh", "sr"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestGd(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, One, []string{"1", "11"})
tests = appendDecimalTests(tests, One, []string{"1.0", "11.0", "1.00", "11.00", "1.000", "11.000", "1.0000"})
tests = appendIntegerTests(tests, Two, []string{"2", "12"})
tests = appendDecimalTests(tests, Two, []string{"2.0", "12.0", "2.00", "12.00", "2.000", "12.000", "2.0000"})
tests = appendIntegerTests(tests, Few, []string{"3~10", "13~19"})
tests = appendDecimalTests(tests, Few, []string{"3.0", "4.0", "5.0", "6.0", "7.0", "8.0", "9.0", "10.0", "13.0", "14.0", "15.0", "16.0", "17.0", "18.0", "19.0", "3.00"})
tests = appendIntegerTests(tests, Other, []string{"0", "20~34", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.0~0.9", "1.1~1.6", "10.1", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"gd"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestSl(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, One, []string{"1", "101", "201", "301", "401", "501", "601", "701", "1001"})
tests = appendIntegerTests(tests, Two, []string{"2", "102", "202", "302", "402", "502", "602", "702", "1002"})
tests = appendIntegerTests(tests, Few, []string{"3", "4", "103", "104", "203", "204", "303", "304", "403", "404", "503", "504", "603", "604", "703", "704", "1003"})
tests = appendDecimalTests(tests, Few, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
tests = appendIntegerTests(tests, Other, []string{"0", "5~19", "100", "1000", "10000", "100000", "1000000"})
locales := []string{"sl"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestDsbHsb(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, One, []string{"1", "101", "201", "301", "401", "501", "601", "701", "1001"})
tests = appendDecimalTests(tests, One, []string{"0.1", "1.1", "2.1", "3.1", "4.1", "5.1", "6.1", "7.1", "10.1", "100.1", "1000.1"})
tests = appendIntegerTests(tests, Two, []string{"2", "102", "202", "302", "402", "502", "602", "702", "1002"})
tests = appendDecimalTests(tests, Two, []string{"0.2", "1.2", "2.2", "3.2", "4.2", "5.2", "6.2", "7.2", "10.2", "100.2", "1000.2"})
tests = appendIntegerTests(tests, Few, []string{"3", "4", "103", "104", "203", "204", "303", "304", "403", "404", "503", "504", "603", "604", "703", "704", "1003"})
tests = appendDecimalTests(tests, Few, []string{"0.3", "0.4", "1.3", "1.4", "2.3", "2.4", "3.3", "3.4", "4.3", "4.4", "5.3", "5.4", "6.3", "6.4", "7.3", "7.4", "10.3", "100.3", "1000.3"})
tests = appendIntegerTests(tests, Other, []string{"0", "5~19", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.0", "0.5~1.0", "1.5~2.0", "2.5~2.7", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"dsb", "hsb"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestHeIw(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, One, []string{"1"})
tests = appendIntegerTests(tests, Two, []string{"2"})
tests = appendIntegerTests(tests, Many, []string{"20", "30", "40", "50", "60", "70", "80", "90", "100", "1000", "10000", "100000", "1000000"})
tests = appendIntegerTests(tests, Other, []string{"0", "3~17", "101", "1001"})
tests = appendDecimalTests(tests, Other, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"he", "iw"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestCsSk(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, One, []string{"1"})
tests = appendIntegerTests(tests, Few, []string{"2~4"})
tests = appendDecimalTests(tests, Many, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
tests = appendIntegerTests(tests, Other, []string{"0", "5~19", "100", "1000", "10000", "100000", "1000000"})
locales := []string{"cs", "sk"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestPl(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, One, []string{"1"})
tests = appendIntegerTests(tests, Few, []string{"2~4", "22~24", "32~34", "42~44", "52~54", "62", "102", "1002"})
tests = appendIntegerTests(tests, Many, []string{"0", "5~19", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"pl"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestBe(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, One, []string{"1", "21", "31", "41", "51", "61", "71", "81", "101", "1001"})
tests = appendDecimalTests(tests, One, []string{"1.0", "21.0", "31.0", "41.0", "51.0", "61.0", "71.0", "81.0", "101.0", "1001.0"})
tests = appendIntegerTests(tests, Few, []string{"2~4", "22~24", "32~34", "42~44", "52~54", "62", "102", "1002"})
tests = appendDecimalTests(tests, Few, []string{"2.0", "3.0", "4.0", "22.0", "23.0", "24.0", "32.0", "33.0", "102.0", "1002.0"})
tests = appendIntegerTests(tests, Many, []string{"0", "5~19", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Many, []string{"0.0", "5.0", "6.0", "7.0", "8.0", "9.0", "10.0", "11.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
tests = appendDecimalTests(tests, Other, []string{"0.1~0.9", "1.1~1.7", "10.1", "100.1", "1000.1"})
locales := []string{"be"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestLt(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, One, []string{"1", "21", "31", "41", "51", "61", "71", "81", "101", "1001"})
tests = appendDecimalTests(tests, One, []string{"1.0", "21.0", "31.0", "41.0", "51.0", "61.0", "71.0", "81.0", "101.0", "1001.0"})
tests = appendIntegerTests(tests, Few, []string{"2~9", "22~29", "102", "1002"})
tests = appendDecimalTests(tests, Few, []string{"2.0", "3.0", "4.0", "5.0", "6.0", "7.0", "8.0", "9.0", "22.0", "102.0", "1002.0"})
tests = appendDecimalTests(tests, Many, []string{"0.1~0.9", "1.1~1.7", "10.1", "100.1", "1000.1"})
tests = appendIntegerTests(tests, Other, []string{"0", "10~20", "30", "40", "50", "60", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.0", "10.0", "11.0", "12.0", "13.0", "14.0", "15.0", "16.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"lt"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestMt(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, One, []string{"1"})
tests = appendDecimalTests(tests, One, []string{"1.0", "1.00", "1.000", "1.0000"})
tests = appendIntegerTests(tests, Few, []string{"0", "2~10", "102~107", "1002"})
tests = appendDecimalTests(tests, Few, []string{"0.0", "2.0", "3.0", "4.0", "5.0", "6.0", "7.0", "8.0", "10.0", "102.0", "1002.0"})
tests = appendIntegerTests(tests, Many, []string{"11~19", "111~117", "1011"})
tests = appendDecimalTests(tests, Many, []string{"11.0", "12.0", "13.0", "14.0", "15.0", "16.0", "17.0", "18.0", "111.0", "1011.0"})
tests = appendIntegerTests(tests, Other, []string{"20~35", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.1~0.9", "1.1~1.7", "10.1", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"mt"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestRuUk(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, One, []string{"1", "21", "31", "41", "51", "61", "71", "81", "101", "1001"})
tests = appendIntegerTests(tests, Few, []string{"2~4", "22~24", "32~34", "42~44", "52~54", "62", "102", "1002"})
tests = appendIntegerTests(tests, Many, []string{"0", "5~19", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"ru", "uk"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestBr(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, One, []string{"1", "21", "31", "41", "51", "61", "81", "101", "1001"})
tests = appendDecimalTests(tests, One, []string{"1.0", "21.0", "31.0", "41.0", "51.0", "61.0", "81.0", "101.0", "1001.0"})
tests = appendIntegerTests(tests, Two, []string{"2", "22", "32", "42", "52", "62", "82", "102", "1002"})
tests = appendDecimalTests(tests, Two, []string{"2.0", "22.0", "32.0", "42.0", "52.0", "62.0", "82.0", "102.0", "1002.0"})
tests = appendIntegerTests(tests, Few, []string{"3", "4", "9", "23", "24", "29", "33", "34", "39", "43", "44", "49", "103", "1003"})
tests = appendDecimalTests(tests, Few, []string{"3.0", "4.0", "9.0", "23.0", "24.0", "29.0", "33.0", "34.0", "103.0", "1003.0"})
tests = appendIntegerTests(tests, Many, []string{"1000000"})
tests = appendDecimalTests(tests, Many, []string{"1000000.0", "1000000.00", "1000000.000"})
tests = appendIntegerTests(tests, Other, []string{"0", "5~8", "10~20", "100", "1000", "10000", "100000"})
tests = appendDecimalTests(tests, Other, []string{"0.0~0.9", "1.1~1.6", "10.0", "100.0", "1000.0", "10000.0", "100000.0"})
locales := []string{"br"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestGa(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, One, []string{"1"})
tests = appendDecimalTests(tests, One, []string{"1.0", "1.00", "1.000", "1.0000"})
tests = appendIntegerTests(tests, Two, []string{"2"})
tests = appendDecimalTests(tests, Two, []string{"2.0", "2.00", "2.000", "2.0000"})
tests = appendIntegerTests(tests, Few, []string{"3~6"})
tests = appendDecimalTests(tests, Few, []string{"3.0", "4.0", "5.0", "6.0", "3.00", "4.00", "5.00", "6.00", "3.000", "4.000", "5.000", "6.000", "3.0000", "4.0000", "5.0000", "6.0000"})
tests = appendIntegerTests(tests, Many, []string{"7~10"})
tests = appendDecimalTests(tests, Many, []string{"7.0", "8.0", "9.0", "10.0", "7.00", "8.00", "9.00", "10.00", "7.000", "8.000", "9.000", "10.000", "7.0000", "8.0000", "9.0000", "10.0000"})
tests = appendIntegerTests(tests, Other, []string{"0", "11~25", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.0~0.9", "1.1~1.6", "10.1", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"ga"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestGv(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, One, []string{"1", "11", "21", "31", "41", "51", "61", "71", "101", "1001"})
tests = appendIntegerTests(tests, Two, []string{"2", "12", "22", "32", "42", "52", "62", "72", "102", "1002"})
tests = appendIntegerTests(tests, Few, []string{"0", "20", "40", "60", "80", "100", "120", "140", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Many, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
tests = appendIntegerTests(tests, Other, []string{"3~10", "13~19", "23", "103", "1003"})
locales := []string{"gv"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestAr(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, Zero, []string{"0"})
tests = appendDecimalTests(tests, Zero, []string{"0.0", "0.00", "0.000", "0.0000"})
tests = appendIntegerTests(tests, One, []string{"1"})
tests = appendDecimalTests(tests, One, []string{"1.0", "1.00", "1.000", "1.0000"})
tests = appendIntegerTests(tests, Two, []string{"2"})
tests = appendDecimalTests(tests, Two, []string{"2.0", "2.00", "2.000", "2.0000"})
tests = appendIntegerTests(tests, Few, []string{"3~10", "103~110", "1003"})
tests = appendDecimalTests(tests, Few, []string{"3.0", "4.0", "5.0", "6.0", "7.0", "8.0", "9.0", "10.0", "103.0", "1003.0"})
tests = appendIntegerTests(tests, Many, []string{"11~26", "111", "1011"})
tests = appendDecimalTests(tests, Many, []string{"11.0", "12.0", "13.0", "14.0", "15.0", "16.0", "17.0", "18.0", "111.0", "1011.0"})
tests = appendIntegerTests(tests, Other, []string{"100~102", "200~202", "300~302", "400~402", "500~502", "600", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.1~0.9", "1.1~1.7", "10.1", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"ar"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}
func TestCy(t *testing.T) {
var tests []pluralTest
tests = appendIntegerTests(tests, Zero, []string{"0"})
tests = appendDecimalTests(tests, Zero, []string{"0.0", "0.00", "0.000", "0.0000"})
tests = appendIntegerTests(tests, One, []string{"1"})
tests = appendDecimalTests(tests, One, []string{"1.0", "1.00", "1.000", "1.0000"})
tests = appendIntegerTests(tests, Two, []string{"2"})
tests = appendDecimalTests(tests, Two, []string{"2.0", "2.00", "2.000", "2.0000"})
tests = appendIntegerTests(tests, Few, []string{"3"})
tests = appendDecimalTests(tests, Few, []string{"3.0", "3.00", "3.000", "3.0000"})
tests = appendIntegerTests(tests, Many, []string{"6"})
tests = appendDecimalTests(tests, Many, []string{"6.0", "6.00", "6.000", "6.0000"})
tests = appendIntegerTests(tests, Other, []string{"4", "5", "7~20", "100", "1000", "10000", "100000", "1000000"})
tests = appendDecimalTests(tests, Other, []string{"0.1~0.9", "1.1~1.7", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
locales := []string{"cy"}
for _, locale := range locales {
runTests(t, locale, tests)
}
}

View File

@ -0,0 +1,733 @@
package language
import (
"fmt"
"strconv"
"strings"
"testing"
)
const onePlusEpsilon = "1.00000000000000000000000000000001"
func TestGetPluralSpec(t *testing.T) {
tests := []struct {
src string
spec *PluralSpec
}{
{"pl", pluralSpecs["pl"]},
{"en", pluralSpecs["en"]},
{"en-US", pluralSpecs["en"]},
{"en_US", pluralSpecs["en"]},
{"en-GB", pluralSpecs["en"]},
{"zh-CN", pluralSpecs["zh"]},
{"zh-TW", pluralSpecs["zh"]},
{"pt-BR", pluralSpecs["pt"]},
{"pt_BR", pluralSpecs["pt"]},
{"pt-PT", pluralSpecs["pt-pt"]},
{"pt_PT", pluralSpecs["pt-pt"]},
{"zh-Hans-CN", pluralSpecs["zh"]},
{"zh-Hant-TW", pluralSpecs["zh"]},
{"zh-CN", pluralSpecs["zh"]},
{"zh-TW", pluralSpecs["zh"]},
{"zh-Hans", pluralSpecs["zh"]},
{"zh-Hant", pluralSpecs["zh"]},
{"ko-KR", pluralSpecs["ko"]},
{"ko_KR", pluralSpecs["ko"]},
{"ko-KP", pluralSpecs["ko"]},
{"ko_KP", pluralSpecs["ko"]},
{"en-US-en-US", pluralSpecs["en"]},
{"th", pluralSpecs["th"]},
{"th-TH", pluralSpecs["th"]},
{"hr", pluralSpecs["hr"]},
{"bs", pluralSpecs["bs"]},
{"sr", pluralSpecs["sr"]},
{"ti", pluralSpecs["ti"]},
{"vi", pluralSpecs["vi"]},
{"vi-VN", pluralSpecs["vi"]},
{"mk", pluralSpecs["mk"]},
{"mk-MK", pluralSpecs["mk"]},
{"lv", pluralSpecs["lv"]},
{"lv-LV", pluralSpecs["lv"]},
{".en-US..en-US.", nil},
{"zh, en-gb;q=0.8, en;q=0.7", nil},
{"zh,en-gb;q=0.8,en;q=0.7", nil},
{"xx, en-gb;q=0.8, en;q=0.7", nil},
{"xx,en-gb;q=0.8,en;q=0.7", nil},
{"xx-YY,xx;q=0.8,en-US,en;q=0.8,de;q=0.6,nl;q=0.4", nil},
{"/foo/es/en.json", nil},
{"xx-Yyen-US", nil},
{"en US", nil},
{"", nil},
{"-", nil},
{"_", nil},
{".", nil},
{"-en", nil},
{"_en", nil},
{"-en-", nil},
{"_en_", nil},
{"xx", nil},
}
for _, test := range tests {
spec := getPluralSpec(test.src)
if spec != test.spec {
t.Errorf("getPluralSpec(%q) = %q expected %q", test.src, spec, test.spec)
}
}
}
type pluralTest struct {
num interface{}
plural Plural
}
func appendIntegerTests(tests []pluralTest, plural Plural, examples []string) []pluralTest {
for _, ex := range expandExamples(examples) {
i, err := strconv.ParseInt(ex, 10, 64)
if err != nil {
panic(err)
}
tests = append(tests, pluralTest{ex, plural}, pluralTest{i, plural})
}
return tests
}
func appendDecimalTests(tests []pluralTest, plural Plural, examples []string) []pluralTest {
for _, ex := range expandExamples(examples) {
tests = append(tests, pluralTest{ex, plural})
}
return tests
}
func expandExamples(examples []string) []string {
var expanded []string
for _, ex := range examples {
if parts := strings.Split(ex, "~"); len(parts) == 2 {
for ex := parts[0]; ; ex = increment(ex) {
expanded = append(expanded, ex)
if ex == parts[1] {
break
}
}
} else {
expanded = append(expanded, ex)
}
}
return expanded
}
func increment(dec string) string {
runes := []rune(dec)
carry := true
for i := len(runes) - 1; carry && i >= 0; i-- {
switch runes[i] {
case '.':
continue
case '9':
runes[i] = '0'
default:
runes[i]++
carry = false
}
}
if carry {
runes = append([]rune{'1'}, runes...)
}
return string(runes)
}
//
// Below here are tests that were manually written before tests were automatically generated.
// These are kept around as sanity checks for our code generation.
//
func TestArabic(t *testing.T) {
tests := []pluralTest{
{0, Zero},
{"0", Zero},
{"0.0", Zero},
{"0.00", Zero},
{1, One},
{"1", One},
{"1.0", One},
{"1.00", One},
{onePlusEpsilon, Other},
{2, Two},
{"2", Two},
{"2.0", Two},
{"2.00", Two},
{3, Few},
{"3", Few},
{"3.0", Few},
{"3.00", Few},
{10, Few},
{"10", Few},
{"10.0", Few},
{"10.00", Few},
{103, Few},
{"103", Few},
{"103.0", Few},
{"103.00", Few},
{110, Few},
{"110", Few},
{"110.0", Few},
{"110.00", Few},
{11, Many},
{"11", Many},
{"11.0", Many},
{"11.00", Many},
{99, Many},
{"99", Many},
{"99.0", Many},
{"99.00", Many},
{111, Many},
{"111", Many},
{"111.0", Many},
{"111.00", Many},
{199, Many},
{"199", Many},
{"199.0", Many},
{"199.00", Many},
{100, Other},
{"100", Other},
{"100.0", Other},
{"100.00", Other},
{102, Other},
{"102", Other},
{"102.0", Other},
{"102.00", Other},
{200, Other},
{"200", Other},
{"200.0", Other},
{"200.00", Other},
{202, Other},
{"202", Other},
{"202.0", Other},
{"202.00", Other},
}
tests = appendFloatTests(tests, 0.1, 0.9, Other)
tests = appendFloatTests(tests, 1.1, 1.9, Other)
tests = appendFloatTests(tests, 2.1, 2.9, Other)
tests = appendFloatTests(tests, 3.1, 3.9, Other)
tests = appendFloatTests(tests, 4.1, 4.9, Other)
runTests(t, "ar", tests)
}
func TestBelarusian(t *testing.T) {
tests := []pluralTest{
{0, Many},
{1, One},
{2, Few},
{3, Few},
{4, Few},
{5, Many},
{19, Many},
{20, Many},
{21, One},
{11, Many},
{52, Few},
{101, One},
{"0.1", Other},
{"0.7", Other},
{"1.5", Other},
{"1.0", One},
{onePlusEpsilon, Other},
{"2.0", Few},
{"10.0", Many},
}
runTests(t, "be", tests)
}
func TestBurmese(t *testing.T) {
tests := appendIntTests(nil, 0, 10, Other)
tests = appendFloatTests(tests, 0, 10, Other)
runTests(t, "my", tests)
}
func TestCatalan(t *testing.T) {
tests := []pluralTest{
{0, Other},
{"0", Other},
{1, One},
{"1", One},
{"1.0", Other},
{onePlusEpsilon, Other},
{2, Other},
{"2", Other},
}
tests = appendIntTests(tests, 2, 10, Other)
tests = appendFloatTests(tests, 0, 10, Other)
runTests(t, "ca", tests)
}
func TestChinese(t *testing.T) {
tests := appendIntTests(nil, 0, 10, Other)
tests = appendFloatTests(tests, 0, 10, Other)
runTests(t, "zh", tests)
}
func TestCzech(t *testing.T) {
tests := []pluralTest{
{0, Other},
{"0", Other},
{1, One},
{"1", One},
{onePlusEpsilon, Many},
{2, Few},
{"2", Few},
{3, Few},
{"3", Few},
{4, Few},
{"4", Few},
{5, Other},
{"5", Other},
}
tests = appendFloatTests(tests, 0, 10, Many)
runTests(t, "cs", tests)
}
func TestDanish(t *testing.T) {
tests := []pluralTest{
{0, Other},
{1, One},
{onePlusEpsilon, One},
{2, Other},
}
tests = appendFloatTests(tests, 0.1, 1.9, One)
tests = appendFloatTests(tests, 2.0, 10.0, Other)
runTests(t, "da", tests)
}
func TestDutch(t *testing.T) {
tests := []pluralTest{
{0, Other},
{1, One},
{onePlusEpsilon, Other},
{2, Other},
}
tests = appendFloatTests(tests, 0.0, 10.0, Other)
runTests(t, "nl", tests)
}
func TestEnglish(t *testing.T) {
tests := []pluralTest{
{0, Other},
{1, One},
{onePlusEpsilon, Other},
{2, Other},
}
tests = appendFloatTests(tests, 0.0, 10.0, Other)
runTests(t, "en", tests)
}
func TestFrench(t *testing.T) {
tests := []pluralTest{
{0, One},
{1, One},
{onePlusEpsilon, One},
{2, Other},
}
tests = appendFloatTests(tests, 0.0, 1.9, One)
tests = appendFloatTests(tests, 2.0, 10.0, Other)
runTests(t, "fr", tests)
}
func TestGerman(t *testing.T) {
tests := []pluralTest{
{0, Other},
{1, One},
{onePlusEpsilon, Other},
{2, Other},
}
tests = appendFloatTests(tests, 0.0, 10.0, Other)
runTests(t, "de", tests)
}
func TestIcelandic(t *testing.T) {
tests := []pluralTest{
{0, Other},
{1, One},
{2, Other},
{11, Other},
{21, One},
{111, Other},
{"0.0", Other},
{"0.1", One},
{"2.0", Other},
}
runTests(t, "is", tests)
}
func TestIndonesian(t *testing.T) {
tests := appendIntTests(nil, 0, 10, Other)
tests = appendFloatTests(tests, 0, 10, Other)
runTests(t, "id", tests)
}
func TestItalian(t *testing.T) {
tests := []pluralTest{
{0, Other},
{1, One},
{onePlusEpsilon, Other},
{2, Other},
}
tests = appendFloatTests(tests, 0.0, 10.0, Other)
runTests(t, "it", tests)
}
func TestKorean(t *testing.T) {
tests := appendIntTests(nil, 0, 10, Other)
tests = appendFloatTests(tests, 0, 10, Other)
runTests(t, "ko", tests)
}
func TestLatvian(t *testing.T) {
tests := []pluralTest{
{0, Zero},
{"0", Zero},
{"0.1", One},
{1, One},
{"1", One},
{onePlusEpsilon, One},
{"10.0", Zero},
{"10.1", One},
{"10.2", Other},
{21, One},
}
tests = appendFloatTests(tests, 0.2, 0.9, Other)
tests = appendFloatTests(tests, 1.2, 1.9, Other)
tests = appendIntTests(tests, 2, 9, Other)
tests = appendIntTests(tests, 10, 20, Zero)
tests = appendIntTests(tests, 22, 29, Other)
runTests(t, "lv", tests)
}
func TestJapanese(t *testing.T) {
tests := appendIntTests(nil, 0, 10, Other)
tests = appendFloatTests(tests, 0, 10, Other)
runTests(t, "ja", tests)
}
func TestLithuanian(t *testing.T) {
tests := []pluralTest{
{0, Other},
{1, One},
{2, Few},
{3, Few},
{9, Few},
{10, Other},
{11, Other},
{"0.1", Many},
{"0.7", Many},
{"1.0", One},
{onePlusEpsilon, Many},
{"2.0", Few},
{"10.0", Other},
}
runTests(t, "lt", tests)
}
func TestMalay(t *testing.T) {
tests := appendIntTests(nil, 0, 10, Other)
tests = appendFloatTests(tests, 0, 10, Other)
runTests(t, "ms", tests)
}
func TestPolish(t *testing.T) {
tests := []pluralTest{
{0, Many},
{1, One},
{2, Few},
{3, Few},
{4, Few},
{5, Many},
{19, Many},
{20, Many},
{10, Many},
{11, Many},
{52, Few},
{"0.1", Other},
{"0.7", Other},
{"1.5", Other},
{"1.0", Other},
{onePlusEpsilon, Other},
{"2.0", Other},
{"10.0", Other},
}
runTests(t, "pl", tests)
}
func TestPortuguese(t *testing.T) {
tests := []pluralTest{
{0, One},
{"0.0", One},
{1, One},
{"1.0", One},
{onePlusEpsilon, Other},
{2, Other},
}
tests = appendFloatTests(tests, 0.1, 0.9, Other)
tests = appendFloatTests(tests, 1.1, 10.0, Other)
runTests(t, "pt", tests)
}
func TestMacedonian(t *testing.T) {
tests := []pluralTest{
{0, Other},
{1, One},
{"1.1", One},
{"2.1", One},
{onePlusEpsilon, One},
{2, Other},
{"2.2", Other},
{11, One},
}
runTests(t, "mk", tests)
}
func TestPortugueseEuropean(t *testing.T) {
tests := []pluralTest{
{0, Other},
{"0.0", Other},
{"0.1", Other},
{"0.01", Other},
{1, One},
{"1", One},
{"1.1", Other},
{"1.01", Other},
{onePlusEpsilon, Other},
{2, Other},
}
tests = appendFloatTests(tests, 2.0, 10.0, Other)
runTests(t, "pt-pt", tests)
}
func TestRussian(t *testing.T) {
tests := []pluralTest{
{0, Many},
{1, One},
{2, Few},
{3, Few},
{4, Few},
{5, Many},
{19, Many},
{20, Many},
{21, One},
{11, Many},
{52, Few},
{101, One},
{"0.1", Other},
{"0.7", Other},
{"1.5", Other},
{"1.0", Other},
{onePlusEpsilon, Other},
{"2.0", Other},
{"10.0", Other},
}
runTests(t, "ru", tests)
}
func TestSpanish(t *testing.T) {
tests := []pluralTest{
{0, Other},
{1, One},
{"1", One},
{"1.0", One},
{"1.00", One},
{onePlusEpsilon, Other},
{2, Other},
}
tests = appendFloatTests(tests, 0.0, 0.9, Other)
tests = appendFloatTests(tests, 1.1, 10.0, Other)
runTests(t, "es", tests)
}
func TestNorweigan(t *testing.T) {
tests := []pluralTest{
{0, Other},
{1, One},
{"1", One},
{"1.0", One},
{"1.00", One},
{onePlusEpsilon, Other},
{2, Other},
}
tests = appendFloatTests(tests, 0.0, 0.9, Other)
tests = appendFloatTests(tests, 1.1, 10.0, Other)
runTests(t, "no", tests)
}
func TestBulgarian(t *testing.T) {
tests := []pluralTest{
{0, Other},
{1, One},
{2, Other},
{3, Other},
{9, Other},
{10, Other},
{11, Other},
{"0.1", Other},
{"0.7", Other},
{"1.0", One},
{"1.001", Other},
{onePlusEpsilon, Other},
{"1.1", Other},
{"2.0", Other},
{"10.0", Other},
}
runTests(t, "bg", tests)
}
func TestSwedish(t *testing.T) {
tests := []pluralTest{
{0, Other},
{1, One},
{onePlusEpsilon, Other},
{2, Other},
}
tests = appendFloatTests(tests, 0.0, 10.0, Other)
runTests(t, "sv", tests)
}
func TestThai(t *testing.T) {
tests := appendIntTests(nil, 0, 10, Other)
tests = appendFloatTests(tests, 0, 10, Other)
runTests(t, "th", tests)
}
func TestVietnamese(t *testing.T) {
tests := appendIntTests(nil, 0, 10, Other)
tests = appendFloatTests(tests, 0, 10, Other)
runTests(t, "vi", tests)
}
func TestTurkish(t *testing.T) {
tests := []pluralTest{
{0, Other},
{1, One},
{"1", One},
{"1.0", One},
{"1.00", One},
{"1.001", Other},
{"1.100", Other},
{"1.101", Other},
{onePlusEpsilon, Other},
{2, Other},
{"0.7", Other},
{"2.0", Other},
}
runTests(t, "tr", tests)
}
func TestUkrainian(t *testing.T) {
tests := []pluralTest{
{0, Many},
{1, One},
{2, Few},
{3, Few},
{4, Few},
{5, Many},
{19, Many},
{20, Many},
{21, One},
{11, Many},
{52, Few},
{101, One},
{"0.1", Other},
{"0.7", Other},
{"1.5", Other},
{"1.0", Other},
{onePlusEpsilon, Other},
{"2.0", Other},
{"10.0", Other},
}
runTests(t, "uk", tests)
}
func TestCroatian(t *testing.T) {
tests := makeCroatianBosnianSerbianTests()
runTests(t, "hr", tests)
}
func TestBosnian(t *testing.T) {
tests := makeCroatianBosnianSerbianTests()
runTests(t, "bs", tests)
}
func TestSerbian(t *testing.T) {
tests := makeCroatianBosnianSerbianTests()
runTests(t, "sr", tests)
}
func makeCroatianBosnianSerbianTests() []pluralTest {
return []pluralTest{
{1, One},
{"0.1", One},
{21, One},
{101, One},
{1001, One},
{51, One},
{"1.1", One},
{"5.1", One},
{"100.1", One},
{"1000.1", One},
{2, Few},
{"0.2", Few},
{22, Few},
{"1.2", Few},
{24, Few},
{"2.4", Few},
{102, Few},
{"100.2", Few},
{1002, Few},
{"1000.2", Few},
{5, Other},
{"0.5", Other},
{0, Other},
{100, Other},
{19, Other},
{"0.0", Other},
{"100.0", Other},
{"1000.0", Other},
}
}
func TestTigrinya(t *testing.T) {
tests := []pluralTest{
{0, One},
{1, One},
}
tests = appendIntTests(tests, 2, 10, Other)
tests = appendFloatTests(tests, 1.1, 10.0, Other)
runTests(t, "ti", tests)
}
func appendIntTests(tests []pluralTest, from, to int, p Plural) []pluralTest {
for i := from; i <= to; i++ {
tests = append(tests, pluralTest{i, p})
}
return tests
}
func appendFloatTests(tests []pluralTest, from, to float64, p Plural) []pluralTest {
stride := 0.1
format := "%.1f"
for f := from; f < to; f += stride {
tests = append(tests, pluralTest{fmt.Sprintf(format, f), p})
}
tests = append(tests, pluralTest{fmt.Sprintf(format, to), p})
return tests
}
func runTests(t *testing.T, pluralSpecID string, tests []pluralTest) {
pluralSpecID = normalizePluralSpecID(pluralSpecID)
if spec := pluralSpecs[pluralSpecID]; spec != nil {
for _, test := range tests {
if plural, err := spec.Plural(test.num); plural != test.plural {
t.Errorf("%s: PluralCategory(%#v) returned %s, %v; expected %s", pluralSpecID, test.num, plural, err, test.plural)
}
}
} else {
t.Errorf("could not find plural spec for locale %s", pluralSpecID)
}
}

View File

@ -0,0 +1,78 @@
package translation
import (
"github.com/nicksnyder/go-i18n/i18n/language"
)
type pluralTranslation struct {
id string
templates map[language.Plural]*template
}
func (pt *pluralTranslation) MarshalInterface() interface{} {
return map[string]interface{}{
"id": pt.id,
"translation": pt.templates,
}
}
func (pt *pluralTranslation) ID() string {
return pt.id
}
func (pt *pluralTranslation) Template(pc language.Plural) *template {
return pt.templates[pc]
}
func (pt *pluralTranslation) UntranslatedCopy() Translation {
return &pluralTranslation{pt.id, make(map[language.Plural]*template)}
}
func (pt *pluralTranslation) Normalize(l *language.Language) Translation {
// Delete plural categories that don't belong to this language.
for pc := range pt.templates {
if _, ok := l.Plurals[pc]; !ok {
delete(pt.templates, pc)
}
}
// Create map entries for missing valid categories.
for pc := range l.Plurals {
if _, ok := pt.templates[pc]; !ok {
pt.templates[pc] = mustNewTemplate("")
}
}
return pt
}
func (pt *pluralTranslation) Backfill(src Translation) Translation {
for pc, t := range pt.templates {
if t == nil || t.src == "" {
pt.templates[pc] = src.Template(language.Other)
}
}
return pt
}
func (pt *pluralTranslation) Merge(t Translation) Translation {
other, ok := t.(*pluralTranslation)
if !ok || pt.ID() != t.ID() {
return t
}
for pluralCategory, template := range other.templates {
if template != nil && template.src != "" {
pt.templates[pluralCategory] = template
}
}
return pt
}
func (pt *pluralTranslation) Incomplete(l *language.Language) bool {
for pc := range l.Plurals {
if t := pt.templates[pc]; t == nil || t.src == "" {
return true
}
}
return false
}
var _ = Translation(&pluralTranslation{})

View File

@ -0,0 +1,308 @@
package translation
import (
"reflect"
"testing"
"github.com/nicksnyder/go-i18n/i18n/language"
)
func mustTemplate(t *testing.T, src string) *template {
tmpl, err := newTemplate(src)
if err != nil {
t.Fatal(err)
}
return tmpl
}
func pluralTranslationFixture(t *testing.T, id string, pluralCategories ...language.Plural) *pluralTranslation {
templates := make(map[language.Plural]*template, len(pluralCategories))
for _, pc := range pluralCategories {
templates[pc] = mustTemplate(t, string(pc))
}
return &pluralTranslation{id, templates}
}
func verifyDeepEqual(t *testing.T, actual, expected interface{}) {
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("\n%#v\nnot equal to expected value\n%#v", actual, expected)
}
}
func TestPluralTranslationMerge(t *testing.T) {
pt := pluralTranslationFixture(t, "id", language.One, language.Other)
oneTemplate, otherTemplate := pt.templates[language.One], pt.templates[language.Other]
pt.Merge(pluralTranslationFixture(t, "id"))
verifyDeepEqual(t, pt.templates, map[language.Plural]*template{
language.One: oneTemplate,
language.Other: otherTemplate,
})
pt2 := pluralTranslationFixture(t, "id", language.One, language.Two)
pt.Merge(pt2)
verifyDeepEqual(t, pt.templates, map[language.Plural]*template{
language.One: pt2.templates[language.One],
language.Two: pt2.templates[language.Two],
language.Other: otherTemplate,
})
}
/* Test implementations from old idea
func TestCopy(t *testing.T) {
ls := &LocalizedString{
ID: "id",
Translation: testingTemplate(t, "translation {{.Hello}}"),
Translations: map[language.Plural]*template{
language.One: testingTemplate(t, "plural {{.One}}"),
language.Other: testingTemplate(t, "plural {{.Other}}"),
},
}
c := ls.Copy()
delete(c.Translations, language.One)
if _, ok := ls.Translations[language.One]; !ok {
t.Errorf("deleting plural translation from copy deleted it from the original")
}
c.Translations[language.Two] = testingTemplate(t, "plural {{.Two}}")
if _, ok := ls.Translations[language.Two]; ok {
t.Errorf("adding plural translation to copy added it to the original")
}
}
func TestNormalize(t *testing.T) {
oneTemplate := testingTemplate(t, "one {{.One}}")
ls := &LocalizedString{
Translation: testingTemplate(t, "single {{.Single}}"),
Translations: map[language.Plural]*template{
language.One: oneTemplate,
language.Two: testingTemplate(t, "two {{.Two}}"),
},
}
ls.Normalize(LanguageWithCode("en"))
if ls.Translation != nil {
t.Errorf("ls.Translation is %#v; expected nil", ls.Translation)
}
if actual := ls.Translations[language.Two]; actual != nil {
t.Errorf("ls.Translation[language.Two] is %#v; expected nil", actual)
}
if actual := ls.Translations[language.One]; actual != oneTemplate {
t.Errorf("ls.Translations[language.One] is %#v; expected %#v", actual, oneTemplate)
}
if _, ok := ls.Translations[language.Other]; !ok {
t.Errorf("ls.Translations[language.Other] shouldn't be empty")
}
}
func TestMergeTranslation(t *testing.T) {
ls := &LocalizedString{}
translation := testingTemplate(t, "one {{.Hello}}")
ls.Merge(&LocalizedString{
Translation: translation,
})
if ls.Translation != translation {
t.Errorf("expected %#v; got %#v", translation, ls.Translation)
}
ls.Merge(&LocalizedString{})
if ls.Translation != translation {
t.Errorf("expected %#v; got %#v", translation, ls.Translation)
}
translation = testingTemplate(t, "two {{.Hello}}")
ls.Merge(&LocalizedString{
Translation: translation,
})
if ls.Translation != translation {
t.Errorf("expected %#v; got %#v", translation, ls.Translation)
}
}
func TestMergeTranslations(t *testing.T) {
ls := &LocalizedString{}
oneTemplate := testingTemplate(t, "one {{.One}}")
otherTemplate := testingTemplate(t, "other {{.Other}}")
ls.Merge(&LocalizedString{
Translations: map[language.Plural]*template{
language.One: oneTemplate,
language.Other: otherTemplate,
},
})
if actual := ls.Translations[language.One]; actual != oneTemplate {
t.Errorf("ls.Translations[language.One] expected %#v; got %#v", oneTemplate, actual)
}
if actual := ls.Translations[language.Other]; actual != otherTemplate {
t.Errorf("ls.Translations[language.Other] expected %#v; got %#v", otherTemplate, actual)
}
ls.Merge(&LocalizedString{
Translations: map[language.Plural]*template{},
})
if actual := ls.Translations[language.One]; actual != oneTemplate {
t.Errorf("ls.Translations[language.One] expected %#v; got %#v", oneTemplate, actual)
}
if actual := ls.Translations[language.Other]; actual != otherTemplate {
t.Errorf("ls.Translations[language.Other] expected %#v; got %#v", otherTemplate, actual)
}
twoTemplate := testingTemplate(t, "two {{.Two}}")
otherTemplate = testingTemplate(t, "second other {{.Other}}")
ls.Merge(&LocalizedString{
Translations: map[language.Plural]*template{
language.Two: twoTemplate,
language.Other: otherTemplate,
},
})
if actual := ls.Translations[language.One]; actual != oneTemplate {
t.Errorf("ls.Translations[language.One] expected %#v; got %#v", oneTemplate, actual)
}
if actual := ls.Translations[language.Two]; actual != twoTemplate {
t.Errorf("ls.Translations[language.Two] expected %#v; got %#v", twoTemplate, actual)
}
if actual := ls.Translations[language.Other]; actual != otherTemplate {
t.Errorf("ls.Translations[language.Other] expected %#v; got %#v", otherTemplate, actual)
}
}
func TestMissingTranslations(t *testing.T) {
en := LanguageWithCode("en")
tests := []struct {
localizedString *LocalizedString
language *Language
expected bool
}{
{
&LocalizedString{},
en,
true,
},
{
&LocalizedString{Translation: testingTemplate(t, "single {{.Single}}")},
en,
false,
},
{
&LocalizedString{
Translation: testingTemplate(t, "single {{.Single}}"),
Translations: map[language.Plural]*template{
language.One: testingTemplate(t, "one {{.One}}"),
}},
en,
true,
},
{
&LocalizedString{Translations: map[language.Plural]*template{
language.One: testingTemplate(t, "one {{.One}}"),
}},
en,
true,
},
{
&LocalizedString{Translations: map[language.Plural]*template{
language.One: nil,
language.Other: nil,
}},
en,
true,
},
{
&LocalizedString{Translations: map[language.Plural]*template{
language.One: testingTemplate(t, ""),
language.Other: testingTemplate(t, ""),
}},
en,
true,
},
{
&LocalizedString{Translations: map[language.Plural]*template{
language.One: testingTemplate(t, "one {{.One}}"),
language.Other: testingTemplate(t, "other {{.Other}}"),
}},
en,
false,
},
}
for _, tt := range tests {
if actual := tt.localizedString.MissingTranslations(tt.language); actual != tt.expected {
t.Errorf("expected %t got %t for %s, %#v",
tt.expected, actual, tt.language.code, tt.localizedString)
}
}
}
func TestHasTranslations(t *testing.T) {
en := LanguageWithCode("en")
tests := []struct {
localizedString *LocalizedString
language *Language
expected bool
}{
{
&LocalizedString{},
en,
false,
},
{
&LocalizedString{Translation: testingTemplate(t, "single {{.Single}}")},
en,
true,
},
{
&LocalizedString{
Translation: testingTemplate(t, "single {{.Single}}"),
Translations: map[language.Plural]*template{}},
en,
false,
},
{
&LocalizedString{Translations: map[language.Plural]*template{
language.One: testingTemplate(t, "one {{.One}}"),
}},
en,
true,
},
{
&LocalizedString{Translations: map[language.Plural]*template{
language.Two: testingTemplate(t, "two {{.Two}}"),
}},
en,
false,
},
{
&LocalizedString{Translations: map[language.Plural]*template{
language.One: nil,
}},
en,
false,
},
{
&LocalizedString{Translations: map[language.Plural]*template{
language.One: testingTemplate(t, ""),
}},
en,
false,
},
}
for _, tt := range tests {
if actual := tt.localizedString.HasTranslations(tt.language); actual != tt.expected {
t.Errorf("expected %t got %t for %s, %#v",
tt.expected, actual, tt.language.code, tt.localizedString)
}
}
}
func testingTemplate(t *testing.T, src string) *template {
tmpl, err := newTemplate(src)
if err != nil {
t.Fatal(err)
}
return tmpl
}
*/

View File

@ -0,0 +1,57 @@
package translation
import (
"github.com/nicksnyder/go-i18n/i18n/language"
)
type singleTranslation struct {
id string
template *template
}
func (st *singleTranslation) MarshalInterface() interface{} {
return map[string]interface{}{
"id": st.id,
"translation": st.template,
}
}
func (st *singleTranslation) ID() string {
return st.id
}
func (st *singleTranslation) Template(pc language.Plural) *template {
return st.template
}
func (st *singleTranslation) UntranslatedCopy() Translation {
return &singleTranslation{st.id, mustNewTemplate("")}
}
func (st *singleTranslation) Normalize(language *language.Language) Translation {
return st
}
func (st *singleTranslation) Backfill(src Translation) Translation {
if st.template == nil || st.template.src == "" {
st.template = src.Template(language.Other)
}
return st
}
func (st *singleTranslation) Merge(t Translation) Translation {
other, ok := t.(*singleTranslation)
if !ok || st.ID() != t.ID() {
return t
}
if other.template != nil && other.template.src != "" {
st.template = other.template
}
return st
}
func (st *singleTranslation) Incomplete(l *language.Language) bool {
return st.template == nil || st.template.src == ""
}
var _ = Translation(&singleTranslation{})

View File

@ -0,0 +1,61 @@
package translation
import (
"bytes"
"encoding"
"strings"
gotemplate "text/template"
)
type template struct {
tmpl *gotemplate.Template
src string
}
func newTemplate(src string) (*template, error) {
var tmpl template
err := tmpl.parseTemplate(src)
return &tmpl, err
}
func mustNewTemplate(src string) *template {
t, err := newTemplate(src)
if err != nil {
panic(err)
}
return t
}
func (t *template) String() string {
return t.src
}
func (t *template) Execute(args interface{}) string {
if t.tmpl == nil {
return t.src
}
var buf bytes.Buffer
if err := t.tmpl.Execute(&buf, args); err != nil {
return err.Error()
}
return buf.String()
}
func (t *template) MarshalText() ([]byte, error) {
return []byte(t.src), nil
}
func (t *template) UnmarshalText(src []byte) error {
return t.parseTemplate(string(src))
}
func (t *template) parseTemplate(src string) (err error) {
t.src = src
if strings.Contains(src, "{{") {
t.tmpl, err = gotemplate.New(src).Parse(src)
}
return
}
var _ = encoding.TextMarshaler(&template{})
var _ = encoding.TextUnmarshaler(&template{})

View File

@ -0,0 +1,146 @@
package translation
import (
"bytes"
"fmt"
//"launchpad.net/goyaml"
"testing"
gotemplate "text/template"
)
func TestNilTemplate(t *testing.T) {
expected := "hello"
tmpl := &template{
tmpl: nil,
src: expected,
}
if actual := tmpl.Execute(nil); actual != expected {
t.Errorf("Execute(nil) returned %s; expected %s", actual, expected)
}
}
func TestMarshalText(t *testing.T) {
tmpl := &template{
tmpl: gotemplate.Must(gotemplate.New("id").Parse("this is a {{.foo}} template")),
src: "boom",
}
expectedBuf := []byte(tmpl.src)
if buf, err := tmpl.MarshalText(); !bytes.Equal(buf, expectedBuf) || err != nil {
t.Errorf("MarshalText() returned %#v, %#v; expected %#v, nil", buf, err, expectedBuf)
}
}
func TestUnmarshalText(t *testing.T) {
tmpl := &template{}
tmpl.UnmarshalText([]byte("hello {{.World}}"))
result := tmpl.Execute(map[string]string{
"World": "world!",
})
expected := "hello world!"
if result != expected {
t.Errorf("expected %#v; got %#v", expected, result)
}
}
/*
func TestYAMLMarshal(t *testing.T) {
src := "hello {{.World}}"
tmpl, err := newTemplate(src)
if err != nil {
t.Fatal(err)
}
buf, err := goyaml.Marshal(tmpl)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(buf, []byte(src)) {
t.Fatalf(`expected "%s"; got "%s"`, src, buf)
}
}
func TestYAMLUnmarshal(t *testing.T) {
buf := []byte(`Tmpl: "hello"`)
var out struct {
Tmpl *template
}
var foo map[string]string
if err := goyaml.Unmarshal(buf, &foo); err != nil {
t.Fatal(err)
}
if out.Tmpl == nil {
t.Fatalf("out.Tmpl was nil")
}
if out.Tmpl.tmpl == nil {
t.Fatalf("out.Tmpl.tmpl was nil")
}
if expected := "hello {{.World}}"; out.Tmpl.src != expected {
t.Fatalf("expected %s; got %s", expected, out.Tmpl.src)
}
}
func TestGetYAML(t *testing.T) {
src := "hello"
tmpl := &template{
tmpl: nil,
src: src,
}
if tag, value := tmpl.GetYAML(); tag != "" || value != src {
t.Errorf("GetYAML() returned (%#v, %#v); expected (%#v, %#v)", tag, value, "", src)
}
}
func TestSetYAML(t *testing.T) {
tmpl := &template{}
tmpl.SetYAML("tagDoesntMatter", "hello {{.World}}")
result := tmpl.Execute(map[string]string{
"World": "world!",
})
expected := "hello world!"
if result != expected {
t.Errorf("expected %#v; got %#v", expected, result)
}
}
*/
func BenchmarkExecuteNilTemplate(b *testing.B) {
template := &template{src: "hello world"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
template.Execute(nil)
}
}
func BenchmarkExecuteHelloWorldTemplate(b *testing.B) {
template, err := newTemplate("hello world")
if err != nil {
b.Fatal(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
template.Execute(nil)
}
}
// Executing a simple template like this is ~6x slower than Sprintf
// but it is still only a few microseconds which should be sufficiently fast.
// The benefit is that we have nice semantic tags in the translation.
func BenchmarkExecuteHelloNameTemplate(b *testing.B) {
template, err := newTemplate("hello {{.Name}}")
if err != nil {
b.Fatal(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
template.Execute(map[string]string{
"Name": "Nick",
})
}
}
func BenchmarkSprintf(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
fmt.Sprintf("hello %s", "nick")
}
}

View File

@ -0,0 +1,83 @@
// Package translation defines the interface for a translation.
package translation
import (
"fmt"
"github.com/nicksnyder/go-i18n/i18n/language"
)
// Translation is the interface that represents a translated string.
type Translation interface {
// MarshalInterface returns the object that should be used
// to serialize the translation.
MarshalInterface() interface{}
ID() string
Template(language.Plural) *template
UntranslatedCopy() Translation
Normalize(language *language.Language) Translation
Backfill(src Translation) Translation
Merge(Translation) Translation
Incomplete(l *language.Language) bool
}
// SortableByID implements sort.Interface for a slice of translations.
type SortableByID []Translation
func (a SortableByID) Len() int { return len(a) }
func (a SortableByID) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a SortableByID) Less(i, j int) bool { return a[i].ID() < a[j].ID() }
// NewTranslation reflects on data to create a new Translation.
//
// data["id"] must be a string and data["translation"] must be either a string
// for a non-plural translation or a map[string]interface{} for a plural translation.
func NewTranslation(data map[string]interface{}) (Translation, error) {
id, ok := data["id"].(string)
if !ok {
return nil, fmt.Errorf(`missing "id" key`)
}
var pluralObject map[string]interface{}
switch translation := data["translation"].(type) {
case string:
tmpl, err := newTemplate(translation)
if err != nil {
return nil, err
}
return &singleTranslation{id, tmpl}, nil
case map[interface{}]interface{}:
// The YAML parser uses interface{} keys so we first convert them to string keys.
pluralObject = make(map[string]interface{})
for k, v := range translation {
kStr, ok := k.(string)
if !ok {
return nil, fmt.Errorf(`invalid plural category type %T; expected string`, k)
}
pluralObject[kStr] = v
}
case map[string]interface{}:
pluralObject = translation
case nil:
return nil, fmt.Errorf(`missing "translation" key`)
default:
return nil, fmt.Errorf(`unsupported type for "translation" key %T`, translation)
}
templates := make(map[language.Plural]*template, len(pluralObject))
for k, v := range pluralObject {
pc, err := language.NewPlural(k)
if err != nil {
return nil, err
}
str, ok := v.(string)
if !ok {
return nil, fmt.Errorf(`plural category "%s" has value of type %T; expected string`, pc, v)
}
tmpl, err := newTemplate(str)
if err != nil {
return nil, err
}
templates[pc] = tmpl
}
return &pluralTranslation{id, templates}, nil
}

View File

@ -0,0 +1,17 @@
package translation
import (
"sort"
"testing"
)
// Check this here to avoid unnecessary import of sort package.
var _ = sort.Interface(make(SortableByID, 0, 0))
func TestNewSingleTranslation(t *testing.T) {
t.Skipf("not implemented")
}
func TestNewPluralTranslation(t *testing.T) {
t.Skipf("not implemented")
}

View File

@ -1,15 +1,13 @@
language: go
sudo: false
go:
- 1.1
- 1.2
- 1.3
- 1.4
- 1.5
- 1.6
- tip
script:
- go test -v ./...

View File

@ -7,7 +7,7 @@
"Deps": [
{
"ImportPath": "github.com/davecgh/go-spew/spew",
"Rev": "5215b55f46b2b919f50a1df0eaa5886afe4e3b3d"
"Rev": "2df174808ee097f90d259e432cc04442cf60be21"
},
{
"ImportPath": "github.com/pmezard/go-difflib/difflib",

View File

@ -1,7 +1,7 @@
Testify - Thou Shalt Write Tests
================================
[![Build Status](https://travis-ci.org/stretchr/testify.svg)](https://travis-ci.org/stretchr/testify) [![Go Report Card](https://goreportcard.com/badge/github.com/stretchr/testify)](https://goreportcard.com/report/github.com/stretchr/testify) [![GoDoc](https://godoc.org/github.com/stretchr/testify?status.svg)](https://godoc.org/github.com/stretchr/testify)
[![Build Status](https://travis-ci.org/stretchr/testify.svg)](https://travis-ci.org/stretchr/testify)
Go code (golang) set of packages that provide many tools for testifying that your code will behave as you intend.

View File

@ -832,11 +832,11 @@ func InEpsilonSlice(t TestingT, expected, actual interface{}, epsilon float64, m
//
// Returns whether the assertion was successful (true) or not (false).
func NoError(t TestingT, err error, msgAndArgs ...interface{}) bool {
if err != nil {
return Fail(t, fmt.Sprintf("Received unexpected error %q", err), msgAndArgs...)
if isNil(err) {
return true
}
return true
return Fail(t, fmt.Sprintf("Received unexpected error %q", err), msgAndArgs...)
}
// Error asserts that a function returned an error (i.e. not `nil`).
@ -850,11 +850,8 @@ func NoError(t TestingT, err error, msgAndArgs ...interface{}) bool {
func Error(t TestingT, err error, msgAndArgs ...interface{}) bool {
message := messageFromMsgAndArgs(msgAndArgs...)
if err == nil {
return Fail(t, "An error is expected but got nil. %s", message)
}
return NotNil(t, err, "An error is expected but got nil. %s", message)
return true
}
// EqualError asserts that a function returned an error (i.e. not `nil`)

View File

@ -523,26 +523,8 @@ func TestNoError(t *testing.T) {
False(t, NoError(mockT, err), "NoError with error should return False")
// returning an empty error interface
err = func() error {
var err *customError
if err != nil {
t.Fatal("err should be nil here")
}
return err
}()
if err == nil { // err is not nil here!
t.Errorf("Error should be nil due to empty interface", err)
}
False(t, NoError(mockT, err), "NoError should fail with empty error interface")
}
type customError struct{}
func (*customError) Error() string { return "fail" }
func TestError(t *testing.T) {
mockT := new(testing.T)
@ -557,20 +539,6 @@ func TestError(t *testing.T) {
True(t, Error(mockT, err), "Error with error should return True")
// returning an empty error interface
err = func() error {
var err *customError
if err != nil {
t.Fatal("err should be nil here")
}
return err
}()
if err == nil { // err is not nil here!
t.Errorf("Error should be nil due to empty interface", err)
}
True(t, Error(mockT, err), "Error should pass with empty error interface")
}
func TestEqualError(t *testing.T) {

View File

@ -43,9 +43,6 @@ type Call struct {
// expectations. 0 means to always return the value.
Repeatability int
// Amount of times this call has been called
totalCalls int
// Holds a channel that will be used to block the Return until it either
// recieves a message or is closed. nil means it returns immediately.
WaitFor <-chan time.Time
@ -135,7 +132,7 @@ func (c *Call) After(d time.Duration) *Call {
// mocking a method such as unmarshalers that takes a pointer to a struct and
// sets properties in such struct
//
// Mock.On("Unmarshal", AnythingOfType("*map[string]interface{}").Return().Run(func(args Arguments) {
// Mock.On("Unmarshal", AnythingOfType("*map[string]interface{}").Return().Run(function(args Arguments) {
// arg := args.Get(0).(*map[string]interface{})
// arg["foo"] = "bar"
// })
@ -265,7 +262,7 @@ func callString(method string, arguments Arguments, includeArgumentValues bool)
}
// Called tells the mock object that a method has been called, and gets an array
// of arguments to return. Panics if the call is unexpected (i.e. not preceded by
// of arguments to return. Panics if the call is unexpected (i.e. not preceeded by
// appropriate .On .Return() calls)
// If Call.WaitFor is set, blocks until the channel is closed or receives a message.
func (m *Mock) Called(arguments ...interface{}) Arguments {
@ -308,14 +305,9 @@ func (m *Mock) Called(arguments ...interface{}) Arguments {
switch {
case call.Repeatability == 1:
call.Repeatability = -1
call.totalCalls++
case call.Repeatability > 1:
call.Repeatability--
call.totalCalls++
case call.Repeatability == 0:
call.totalCalls++
}
m.mutex.Unlock()
}
@ -363,7 +355,7 @@ func (m *Mock) AssertExpectations(t TestingT) bool {
// iterate through each expectation
expectedCalls := m.expectedCalls()
for _, expectedCall := range expectedCalls {
if !m.methodWasCalled(expectedCall.Method, expectedCall.Arguments) && expectedCall.totalCalls == 0 {
if !m.methodWasCalled(expectedCall.Method, expectedCall.Arguments) {
somethingMissing = true
failedExpectations++
t.Logf("\u274C\t%s(%s)", expectedCall.Method, expectedCall.Arguments.String())
@ -398,7 +390,6 @@ func (m *Mock) AssertNumberOfCalls(t TestingT, methodName string, expectedCalls
}
// AssertCalled asserts that the method was called.
// It can produce a false result when an argument is a pointer type and the underlying value changed after calling the mocked method.
func (m *Mock) AssertCalled(t TestingT, methodName string, arguments ...interface{}) bool {
if !assert.True(t, m.methodWasCalled(methodName, arguments), fmt.Sprintf("The \"%s\" method should have been called with %d argument(s), but was not.", methodName, len(arguments))) {
t.Logf("%v", m.expectedCalls())
@ -408,7 +399,6 @@ func (m *Mock) AssertCalled(t TestingT, methodName string, arguments ...interfac
}
// AssertNotCalled asserts that the method was not called.
// It can produce a false result when an argument is a pointer type and the underlying value changed after calling the mocked method.
func (m *Mock) AssertNotCalled(t TestingT, methodName string, arguments ...interface{}) bool {
if !assert.False(t, m.methodWasCalled(methodName, arguments), fmt.Sprintf("The \"%s\" method was called with %d argument(s), but should NOT have been.", methodName, len(arguments))) {
t.Logf("%v", m.expectedCalls())
@ -499,7 +489,7 @@ func (f argumentMatcher) String() string {
// and false otherwise.
//
// Example:
// m.On("Do", MatchedBy(func(req *http.Request) bool { return req.Host == "example.com" }))
// m.On("Do", func(req *http.Request) bool { return req.Host == "example.com" })
//
// |fn|, must be a function accepting a single argument (of the expected type)
// which returns a bool. If |fn| doesn't match the required signature,

View File

@ -765,68 +765,6 @@ func Test_Mock_AssertExpectations(t *testing.T) {
}
func Test_Mock_AssertExpectations_Placeholder_NoArgs(t *testing.T) {
var mockedService = new(TestExampleImplementation)
mockedService.On("Test_Mock_AssertExpectations_Placeholder_NoArgs").Return(5, 6, 7).Once()
mockedService.On("Test_Mock_AssertExpectations_Placeholder_NoArgs").Return(7, 6, 5)
tt := new(testing.T)
assert.False(t, mockedService.AssertExpectations(tt))
// make the call now
mockedService.Called()
// now assert expectations
assert.True(t, mockedService.AssertExpectations(tt))
}
func Test_Mock_AssertExpectations_Placeholder(t *testing.T) {
var mockedService = new(TestExampleImplementation)
mockedService.On("Test_Mock_AssertExpectations_Placeholder", 1, 2, 3).Return(5, 6, 7).Once()
mockedService.On("Test_Mock_AssertExpectations_Placeholder", 3, 2, 1).Return(7, 6, 5)
tt := new(testing.T)
assert.False(t, mockedService.AssertExpectations(tt))
// make the call now
mockedService.Called(1, 2, 3)
// now assert expectations
assert.False(t, mockedService.AssertExpectations(tt))
// make call to the second expectation
mockedService.Called(3, 2, 1)
// now assert expectations again
assert.True(t, mockedService.AssertExpectations(tt))
}
func Test_Mock_AssertExpectations_With_Pointers(t *testing.T) {
var mockedService = new(TestExampleImplementation)
mockedService.On("Test_Mock_AssertExpectations_With_Pointers", &struct{ Foo int }{1}).Return(1)
mockedService.On("Test_Mock_AssertExpectations_With_Pointers", &struct{ Foo int }{2}).Return(2)
tt := new(testing.T)
assert.False(t, mockedService.AssertExpectations(tt))
s := struct{ Foo int }{1}
// make the calls now
mockedService.Called(&s)
s.Foo = 2
mockedService.Called(&s)
// now assert expectations
assert.True(t, mockedService.AssertExpectations(tt))
}
func Test_Mock_AssertExpectationsCustomType(t *testing.T) {
var mockedService = new(TestExampleImplementation)

View File

@ -91,21 +91,6 @@ func init() {
flagKindShift = 0
flagRO = 1 << 5
flagIndir = 1 << 6
// Commit adf9b30e5594 modified the flags to separate the
// flagRO flag into two bits which specifies whether or not the
// field is embedded. This causes flagIndir to move over a bit
// and means that flagRO is the combination of either of the
// original flagRO bit and the new bit.
//
// This code detects the change by extracting what used to be
// the indirect bit to ensure it's set. When it's not, the flag
// order has been changed to the newer format, so the flags are
// updated accordingly.
if upfv&flagIndir == 0 {
flagRO = 3 << 5
flagIndir = 1 << 7
}
}
}

View File

@ -181,6 +181,8 @@ func (d *dumpState) dumpSlice(v reflect.Value) {
// Try to use existing uint8 slices and fall back to converting
// and copying if that fails.
case vt.Kind() == reflect.Uint8:
// TODO(davec): Fix up the disableUnsafe bits...
// We need an addressable interface to convert the type
// to a byte slice. However, the reflect package won't
// give us an interface on certain things like

View File

@ -1,2 +0,0 @@
*.coverprofile
node_modules/

View File

@ -1,40 +1,23 @@
language: go
sudo: false
cache:
directories:
- node_modules
go:
- 1.1.2
- 1.2.2
- 1.3.3
- 1.4
- 1.5.4
- 1.6.2
- master
- tip
matrix:
allow_failures:
- go: master
include:
- go: 1.6.2
os: osx
- go: 1.1.2
install: go get -v .
before_script: echo skipping gfmxr on $TRAVIS_GO_VERSION
script:
- ./runtests vet
- ./runtests test
- go: tip
before_script:
- go get github.com/urfave/gfmxr/...
- if [ ! -f node_modules/.bin/markdown-toc ] ; then
npm install markdown-toc ;
fi
- go get github.com/meatballhat/gfmxr/...
script:
- ./runtests vet
- ./runtests test
- ./runtests gfmxr
- ./runtests toc
- go vet ./...
- go test -v ./...
- gfmxr -c $(grep -c 'package main' README.md) -s README.md

View File

@ -3,19 +3,6 @@
**ATTN**: This project uses [semantic versioning](http://semver.org/).
## [Unreleased]
### Added
- `./runtests` test runner with coverage tracking by default
- testing on OS X
- testing on Windows
- `UintFlag`, `Uint64Flag`, and `Int64Flag` types and supporting code
### Changed
- Use spaces for alignment in help/usage output instead of tabs, making the
output alignment consistent regardless of tab width
### Fixed
- Printing of command aliases in help text
- Printing of visible flags for both struct and struct pointer flags
## [1.17.0] - 2016-05-09
### Added
@ -36,7 +23,7 @@
makes it easier to script around apps built using `cli` since they can trust
that a 0 exit code indicated a successful execution.
- cleanups based on [Go Report Card
feedback](https://goreportcard.com/report/github.com/urfave/cli)
feedback](https://goreportcard.com/report/github.com/codegangsta/cli)
## [1.16.0] - 2016-05-02
### Added
@ -296,28 +283,28 @@ signature of `func(*cli.Context) error`, as defined by `cli.ActionFunc`.
### Added
- Initial implementation.
[Unreleased]: https://github.com/urfave/cli/compare/v1.17.0...HEAD
[1.17.0]: https://github.com/urfave/cli/compare/v1.16.0...v1.17.0
[1.16.0]: https://github.com/urfave/cli/compare/v1.15.0...v1.16.0
[1.15.0]: https://github.com/urfave/cli/compare/v1.14.0...v1.15.0
[1.14.0]: https://github.com/urfave/cli/compare/v1.13.0...v1.14.0
[1.13.0]: https://github.com/urfave/cli/compare/v1.12.0...v1.13.0
[1.12.0]: https://github.com/urfave/cli/compare/v1.11.1...v1.12.0
[1.11.1]: https://github.com/urfave/cli/compare/v1.11.0...v1.11.1
[1.11.0]: https://github.com/urfave/cli/compare/v1.10.2...v1.11.0
[1.10.2]: https://github.com/urfave/cli/compare/v1.10.1...v1.10.2
[1.10.1]: https://github.com/urfave/cli/compare/v1.10.0...v1.10.1
[1.10.0]: https://github.com/urfave/cli/compare/v1.9.0...v1.10.0
[1.9.0]: https://github.com/urfave/cli/compare/v1.8.0...v1.9.0
[1.8.0]: https://github.com/urfave/cli/compare/v1.7.1...v1.8.0
[1.7.1]: https://github.com/urfave/cli/compare/v1.7.0...v1.7.1
[1.7.0]: https://github.com/urfave/cli/compare/v1.6.0...v1.7.0
[1.6.0]: https://github.com/urfave/cli/compare/v1.5.0...v1.6.0
[1.5.0]: https://github.com/urfave/cli/compare/v1.4.1...v1.5.0
[1.4.1]: https://github.com/urfave/cli/compare/v1.4.0...v1.4.1
[1.4.0]: https://github.com/urfave/cli/compare/v1.3.1...v1.4.0
[1.3.1]: https://github.com/urfave/cli/compare/v1.3.0...v1.3.1
[1.3.0]: https://github.com/urfave/cli/compare/v1.2.0...v1.3.0
[1.2.0]: https://github.com/urfave/cli/compare/v1.1.0...v1.2.0
[1.1.0]: https://github.com/urfave/cli/compare/v1.0.0...v1.1.0
[1.0.0]: https://github.com/urfave/cli/compare/v0.1.0...v1.0.0
[Unreleased]: https://github.com/codegangsta/cli/compare/v1.17.0...HEAD
[1.17.0]: https://github.com/codegangsta/cli/compare/v1.16.0...v1.17.0
[1.16.0]: https://github.com/codegangsta/cli/compare/v1.15.0...v1.16.0
[1.15.0]: https://github.com/codegangsta/cli/compare/v1.14.0...v1.15.0
[1.14.0]: https://github.com/codegangsta/cli/compare/v1.13.0...v1.14.0
[1.13.0]: https://github.com/codegangsta/cli/compare/v1.12.0...v1.13.0
[1.12.0]: https://github.com/codegangsta/cli/compare/v1.11.1...v1.12.0
[1.11.1]: https://github.com/codegangsta/cli/compare/v1.11.0...v1.11.1
[1.11.0]: https://github.com/codegangsta/cli/compare/v1.10.2...v1.11.0
[1.10.2]: https://github.com/codegangsta/cli/compare/v1.10.1...v1.10.2
[1.10.1]: https://github.com/codegangsta/cli/compare/v1.10.0...v1.10.1
[1.10.0]: https://github.com/codegangsta/cli/compare/v1.9.0...v1.10.0
[1.9.0]: https://github.com/codegangsta/cli/compare/v1.8.0...v1.9.0
[1.8.0]: https://github.com/codegangsta/cli/compare/v1.7.1...v1.8.0
[1.7.1]: https://github.com/codegangsta/cli/compare/v1.7.0...v1.7.1
[1.7.0]: https://github.com/codegangsta/cli/compare/v1.6.0...v1.7.0
[1.6.0]: https://github.com/codegangsta/cli/compare/v1.5.0...v1.6.0
[1.5.0]: https://github.com/codegangsta/cli/compare/v1.4.1...v1.5.0
[1.4.1]: https://github.com/codegangsta/cli/compare/v1.4.0...v1.4.1
[1.4.0]: https://github.com/codegangsta/cli/compare/v1.3.1...v1.4.0
[1.3.1]: https://github.com/codegangsta/cli/compare/v1.3.0...v1.3.1
[1.3.0]: https://github.com/codegangsta/cli/compare/v1.2.0...v1.3.0
[1.2.0]: https://github.com/codegangsta/cli/compare/v1.1.0...v1.2.0
[1.1.0]: https://github.com/codegangsta/cli/compare/v1.0.0...v1.1.0
[1.0.0]: https://github.com/codegangsta/cli/compare/v0.1.0...v1.0.0

1122
vendor/github.com/urfave/cli/README.md generated vendored

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@ import (
"strconv"
"strings"
"github.com/urfave/cli"
"github.com/codegangsta/cli"
)
// FlagInputSourceExtension is an extension interface of cli.Flag that

View File

@ -8,7 +8,7 @@ import (
"testing"
"time"
"github.com/urfave/cli"
"github.com/codegangsta/cli"
)
type testApplyInputSource struct {

View File

@ -3,7 +3,7 @@ package altsrc
import (
"time"
"github.com/urfave/cli"
"github.com/codegangsta/cli"
)
// InputSourceContext is an interface used to allow

View File

@ -6,7 +6,7 @@ import (
"strings"
"time"
"github.com/urfave/cli"
"github.com/codegangsta/cli"
)
// MapInputSource implements InputSourceContext to return

View File

@ -11,7 +11,7 @@ import (
"os"
"testing"
"github.com/urfave/cli"
"github.com/codegangsta/cli"
)
func TestCommandYamlFileTest(t *testing.T) {

View File

@ -12,7 +12,7 @@ import (
"net/url"
"os"
"github.com/urfave/cli"
"github.com/codegangsta/cli"
"gopkg.in/yaml.v2"
)

15
vendor/github.com/urfave/cli/app.go generated vendored
View File

@ -8,12 +8,11 @@ import (
"path/filepath"
"reflect"
"sort"
"strings"
"time"
)
var (
changeLogURL = "https://github.com/urfave/cli/blob/master/CHANGELOG.md"
changeLogURL = "https://github.com/codegangsta/cli/blob/master/CHANGELOG.md"
appActionDeprecationURL = fmt.Sprintf("%s#deprecated-cli-app-action-signature", changeLogURL)
runAndExitOnErrorDeprecationURL = fmt.Sprintf("%s#deprecated-cli-app-runandexitonerror", changeLogURL)
@ -465,13 +464,11 @@ func (a Author) String() string {
func HandleAction(action interface{}, context *Context) (err error) {
defer func() {
if r := recover(); r != nil {
// Try to detect a known reflection error from *this scope*, rather than
// swallowing all panics that may happen when calling an Action func.
s := fmt.Sprintf("%v", r)
if strings.HasPrefix(s, "reflect: ") && strings.Contains(s, "too many input arguments") {
err = NewExitError(fmt.Sprintf("ERROR unknown Action error: %v. See %s", r, appActionDeprecationURL), 2)
} else {
panic(r)
switch r.(type) {
case error:
err = r.(error)
default:
err = NewExitError(fmt.Sprintf("ERROR unknown Action error: %v. See %s", r, appActionDeprecationURL), 2)
}
}
}()

View File

@ -1452,16 +1452,3 @@ func TestHandleAction_WithInvalidFuncReturnSignature(t *testing.T) {
t.Fatalf("expected error exit code to be 2, but got: %v", exitErr.ExitCode())
}
}
func TestHandleAction_WithUnknownPanic(t *testing.T) {
defer func() { refute(t, recover(), nil) }()
var fn ActionFunc
app := NewApp()
app.Action = func(ctx *Context) error {
fn(ctx)
return nil
}
HandleAction(app.Action, NewContext(app, flagSet(app.Name, app.Flags), nil))
}

View File

@ -2,24 +2,15 @@ version: "{build}"
os: Windows Server 2012 R2
clone_folder: c:\gopath\src\github.com\urfave\cli
environment:
GOPATH: C:\gopath
GOVERSION: 1.6
PYTHON: C:\Python27-x64
PYTHON_VERSION: 2.7.x
PYTHON_ARCH: 64
GFMXR_DEBUG: 1
install:
- set PATH=%GOPATH%\bin;C:\go\bin;%PATH%
- go version
- go env
- go get github.com/urfave/gfmxr/...
- go get -v -t ./...
- go version
- go env
build_script:
- python runtests vet
- python runtests test
- python runtests gfmxr
- cd %APPVEYOR_BUILD_FOLDER%
- go vet ./...
- go test -v ./...
test: off
deploy: off

View File

@ -31,21 +31,6 @@ func (c *Context) Int(name string) int {
return lookupInt(name, c.flagSet)
}
// Int64 looks up the value of a local int flag, returns 0 if no int flag exists
func (c *Context) Int64(name string) int64 {
return lookupInt64(name, c.flagSet)
}
// Uint looks up the value of a local int flag, returns 0 if no int flag exists
func (c *Context) Uint(name string) uint {
return lookupUint(name, c.flagSet)
}
// Uint64 looks up the value of a local int flag, returns 0 if no int flag exists
func (c *Context) Uint64(name string) uint64 {
return lookupUint64(name, c.flagSet)
}
// Duration looks up the value of a local time.Duration flag, returns 0 if no
// time.Duration flag exists
func (c *Context) Duration(name string) time.Duration {
@ -85,12 +70,6 @@ func (c *Context) IntSlice(name string) []int {
return lookupIntSlice(name, c.flagSet)
}
// Int64Slice looks up the value of a local int slice flag, returns nil if no int
// slice flag exists
func (c *Context) Int64Slice(name string) []int64 {
return lookupInt64Slice(name, c.flagSet)
}
// Generic looks up the value of a local generic flag, returns nil if no generic
// flag exists
func (c *Context) Generic(name string) interface{} {
@ -105,30 +84,6 @@ func (c *Context) GlobalInt(name string) int {
return 0
}
// GlobalInt64 looks up the value of a global int flag, returns 0 if no int flag exists
func (c *Context) GlobalInt64(name string) int64 {
if fs := lookupGlobalFlagSet(name, c); fs != nil {
return lookupInt64(name, fs)
}
return 0
}
// GlobalUint looks up the value of a global int flag, returns 0 if no int flag exists
func (c *Context) GlobalUint(name string) uint {
if fs := lookupGlobalFlagSet(name, c); fs != nil {
return lookupUint(name, fs)
}
return 0
}
// GlobalUint64 looks up the value of a global int flag, returns 0 if no int flag exists
func (c *Context) GlobalUint64(name string) uint64 {
if fs := lookupGlobalFlagSet(name, c); fs != nil {
return lookupUint64(name, fs)
}
return 0
}
// GlobalFloat64 looks up the value of a global float64 flag, returns float64(0)
// if no float64 flag exists
func (c *Context) GlobalFloat64(name string) float64 {
@ -192,15 +147,6 @@ func (c *Context) GlobalIntSlice(name string) []int {
return nil
}
// GlobalInt64Slice looks up the value of a global int slice flag, returns nil if
// no int slice flag exists
func (c *Context) GlobalInt64Slice(name string) []int64 {
if fs := lookupGlobalFlagSet(name, c); fs != nil {
return lookupInt64Slice(name, fs)
}
return nil
}
// GlobalGeneric looks up the value of a global generic flag, returns nil if no
// generic flag exists
func (c *Context) GlobalGeneric(name string) interface{} {
@ -360,46 +306,7 @@ func lookupGlobalFlagSet(name string, ctx *Context) *flag.FlagSet {
func lookupInt(name string, set *flag.FlagSet) int {
f := set.Lookup(name)
if f != nil {
val, err := strconv.ParseInt(f.Value.String(), 0, 64)
if err != nil {
return 0
}
return int(val)
}
return 0
}
func lookupInt64(name string, set *flag.FlagSet) int64 {
f := set.Lookup(name)
if f != nil {
val, err := strconv.ParseInt(f.Value.String(), 0, 64)
if err != nil {
return 0
}
return val
}
return 0
}
func lookupUint(name string, set *flag.FlagSet) uint {
f := set.Lookup(name)
if f != nil {
val, err := strconv.ParseUint(f.Value.String(), 0, 64)
if err != nil {
return 0
}
return uint(val)
}
return 0
}
func lookupUint64(name string, set *flag.FlagSet) uint64 {
f := set.Lookup(name)
if f != nil {
val, err := strconv.ParseUint(f.Value.String(), 0, 64)
val, err := strconv.Atoi(f.Value.String())
if err != nil {
return 0
}
@ -463,16 +370,6 @@ func lookupIntSlice(name string, set *flag.FlagSet) []int {
return nil
}
func lookupInt64Slice(name string, set *flag.FlagSet) []int64 {
f := set.Lookup(name)
if f != nil {
return (f.Value.(*Int64Slice)).Value()
}
return nil
}
func lookupGeneric(name string, set *flag.FlagSet) interface{} {
f := set.Lookup(name)
if f != nil {

View File

@ -9,29 +9,17 @@ import (
func TestNewContext(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Int("myflag", 12, "doc")
set.Int64("myflagInt64", int64(12), "doc")
set.Uint("myflagUint", uint(93), "doc")
set.Uint64("myflagUint64", uint64(93), "doc")
set.Float64("myflag64", float64(17), "doc")
globalSet := flag.NewFlagSet("test", 0)
globalSet.Int("myflag", 42, "doc")
globalSet.Int64("myflagInt64", int64(42), "doc")
globalSet.Uint("myflagUint", uint(33), "doc")
globalSet.Uint64("myflagUint64", uint64(33), "doc")
globalSet.Float64("myflag64", float64(47), "doc")
globalCtx := NewContext(nil, globalSet, nil)
command := Command{Name: "mycommand"}
c := NewContext(nil, set, globalCtx)
c.Command = command
expect(t, c.Int("myflag"), 12)
expect(t, c.Int64("myflagInt64"), int64(12))
expect(t, c.Uint("myflagUint"), uint(93))
expect(t, c.Uint64("myflagUint64"), uint64(93))
expect(t, c.Float64("myflag64"), float64(17))
expect(t, c.GlobalInt("myflag"), 42)
expect(t, c.GlobalInt64("myflagInt64"), int64(42))
expect(t, c.GlobalUint("myflagUint"), uint(33))
expect(t, c.GlobalUint64("myflagUint64"), uint64(33))
expect(t, c.GlobalFloat64("myflag64"), float64(47))
expect(t, c.Command.Name, "mycommand")
}
@ -43,27 +31,6 @@ func TestContext_Int(t *testing.T) {
expect(t, c.Int("myflag"), 12)
}
func TestContext_Int64(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Int64("myflagInt64", 12, "doc")
c := NewContext(nil, set, nil)
expect(t, c.Int64("myflagInt64"), int64(12))
}
func TestContext_Uint(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Uint("myflagUint", uint(13), "doc")
c := NewContext(nil, set, nil)
expect(t, c.Uint("myflagUint"), uint(13))
}
func TestContext_Uint64(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Uint64("myflagUint64", uint64(9), "doc")
c := NewContext(nil, set, nil)
expect(t, c.Uint64("myflagUint64"), uint64(9))
}
func TestContext_GlobalInt(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Int("myflag", 12, "doc")
@ -72,14 +39,6 @@ func TestContext_GlobalInt(t *testing.T) {
expect(t, c.GlobalInt("nope"), 0)
}
func TestContext_GlobalInt64(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Int64("myflagInt64", 12, "doc")
c := NewContext(nil, set, nil)
expect(t, c.GlobalInt64("myflagInt64"), int64(12))
expect(t, c.GlobalInt64("nope"), int64(0))
}
func TestContext_Float64(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Float64("myflag", float64(17), "doc")

233
vendor/github.com/urfave/cli/flag.go generated vendored
View File

@ -189,7 +189,7 @@ func (f *IntSlice) Set(value string) error {
// String returns a readable representation of this value (for usage defaults)
func (f *IntSlice) String() string {
return fmt.Sprintf("%#v", *f)
return fmt.Sprintf("%d", *f)
}
// Value returns the slice of ints set by this flag
@ -245,77 +245,6 @@ func (f IntSliceFlag) GetName() string {
return f.Name
}
// Int64Slice is an opaque type for []int to satisfy flag.Value
type Int64Slice []int64
// Set parses the value into an integer and appends it to the list of values
func (f *Int64Slice) Set(value string) error {
tmp, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return err
}
*f = append(*f, tmp)
return nil
}
// String returns a readable representation of this value (for usage defaults)
func (f *Int64Slice) String() string {
return fmt.Sprintf("%#v", *f)
}
// Value returns the slice of ints set by this flag
func (f *Int64Slice) Value() []int64 {
return *f
}
// Int64SliceFlag is an int flag that can be specified multiple times on the
// command-line
type Int64SliceFlag struct {
Name string
Value *Int64Slice
Usage string
EnvVar string
Hidden bool
}
// String returns the usage
func (f Int64SliceFlag) String() string {
return FlagStringer(f)
}
// Apply populates the flag given the flag set and environment
func (f Int64SliceFlag) Apply(set *flag.FlagSet) {
if f.EnvVar != "" {
for _, envVar := range strings.Split(f.EnvVar, ",") {
envVar = strings.TrimSpace(envVar)
if envVal := os.Getenv(envVar); envVal != "" {
newVal := &Int64Slice{}
for _, s := range strings.Split(envVal, ",") {
s = strings.TrimSpace(s)
err := newVal.Set(s)
if err != nil {
fmt.Fprintf(ErrWriter, err.Error())
}
}
f.Value = newVal
break
}
}
}
eachName(f.Name, func(name string) {
if f.Value == nil {
f.Value = &Int64Slice{}
}
set.Var(f.Value, name, f.Usage)
})
}
// GetName returns the name of the flag.
func (f Int64SliceFlag) GetName() string {
return f.Name
}
// BoolFlag is a switch that defaults to false
type BoolFlag struct {
Name string
@ -447,6 +376,7 @@ func (f StringFlag) GetName() string {
}
// IntFlag is a flag that takes an integer
// Errors if the value provided cannot be parsed
type IntFlag struct {
Name string
Value int
@ -490,138 +420,6 @@ func (f IntFlag) GetName() string {
return f.Name
}
// Int64Flag is a flag that takes a 64-bit integer
type Int64Flag struct {
Name string
Value int64
Usage string
EnvVar string
Destination *int64
Hidden bool
}
// String returns the usage
func (f Int64Flag) String() string {
return FlagStringer(f)
}
// Apply populates the flag given the flag set and environment
func (f Int64Flag) Apply(set *flag.FlagSet) {
if f.EnvVar != "" {
for _, envVar := range strings.Split(f.EnvVar, ",") {
envVar = strings.TrimSpace(envVar)
if envVal := os.Getenv(envVar); envVal != "" {
envValInt, err := strconv.ParseInt(envVal, 0, 64)
if err == nil {
f.Value = envValInt
break
}
}
}
}
eachName(f.Name, func(name string) {
if f.Destination != nil {
set.Int64Var(f.Destination, name, f.Value, f.Usage)
return
}
set.Int64(name, f.Value, f.Usage)
})
}
// GetName returns the name of the flag.
func (f Int64Flag) GetName() string {
return f.Name
}
// UintFlag is a flag that takes an unsigned integer
type UintFlag struct {
Name string
Value uint
Usage string
EnvVar string
Destination *uint
Hidden bool
}
// String returns the usage
func (f UintFlag) String() string {
return FlagStringer(f)
}
// Apply populates the flag given the flag set and environment
func (f UintFlag) Apply(set *flag.FlagSet) {
if f.EnvVar != "" {
for _, envVar := range strings.Split(f.EnvVar, ",") {
envVar = strings.TrimSpace(envVar)
if envVal := os.Getenv(envVar); envVal != "" {
envValInt, err := strconv.ParseUint(envVal, 0, 64)
if err == nil {
f.Value = uint(envValInt)
break
}
}
}
}
eachName(f.Name, func(name string) {
if f.Destination != nil {
set.UintVar(f.Destination, name, f.Value, f.Usage)
return
}
set.Uint(name, f.Value, f.Usage)
})
}
// GetName returns the name of the flag.
func (f UintFlag) GetName() string {
return f.Name
}
// Uint64Flag is a flag that takes an unsigned 64-bit integer
type Uint64Flag struct {
Name string
Value uint64
Usage string
EnvVar string
Destination *uint64
Hidden bool
}
// String returns the usage
func (f Uint64Flag) String() string {
return FlagStringer(f)
}
// Apply populates the flag given the flag set and environment
func (f Uint64Flag) Apply(set *flag.FlagSet) {
if f.EnvVar != "" {
for _, envVar := range strings.Split(f.EnvVar, ",") {
envVar = strings.TrimSpace(envVar)
if envVal := os.Getenv(envVar); envVal != "" {
envValInt, err := strconv.ParseUint(envVal, 0, 64)
if err == nil {
f.Value = uint64(envValInt)
break
}
}
}
}
eachName(f.Name, func(name string) {
if f.Destination != nil {
set.Uint64Var(f.Destination, name, f.Value, f.Usage)
return
}
set.Uint64(name, f.Value, f.Usage)
})
}
// GetName returns the name of the flag.
func (f Uint64Flag) GetName() string {
return f.Name
}
// DurationFlag is a flag that takes a duration specified in Go's duration
// format: https://golang.org/pkg/time/#ParseDuration
type DurationFlag struct {
@ -668,6 +466,7 @@ func (f DurationFlag) GetName() string {
}
// Float64Flag is a flag that takes an float value
// Errors if the value provided cannot be parsed
type Float64Flag struct {
Name string
Value float64
@ -713,7 +512,7 @@ func (f Float64Flag) GetName() string {
func visibleFlags(fl []Flag) []Flag {
visible := []Flag{}
for _, flag := range fl {
if !flagValue(flag).FieldByName("Hidden").Bool() {
if !reflect.ValueOf(flag).FieldByName("Hidden").Bool() {
visible = append(visible, flag)
}
}
@ -779,24 +578,13 @@ func withEnvHint(envVar, str string) string {
return str + envText
}
func flagValue(f Flag) reflect.Value {
fv := reflect.ValueOf(f)
for fv.Kind() == reflect.Ptr {
fv = reflect.Indirect(fv)
}
return fv
}
func stringifyFlag(f Flag) string {
fv := flagValue(f)
fv := reflect.ValueOf(f)
switch f.(type) {
case IntSliceFlag:
return withEnvHint(fv.FieldByName("EnvVar").String(),
stringifyIntSliceFlag(f.(IntSliceFlag)))
case Int64SliceFlag:
return withEnvHint(fv.FieldByName("EnvVar").String(),
stringifyInt64SliceFlag(f.(Int64SliceFlag)))
case StringSliceFlag:
return withEnvHint(fv.FieldByName("EnvVar").String(),
stringifyStringSliceFlag(f.(StringSliceFlag)))
@ -842,17 +630,6 @@ func stringifyIntSliceFlag(f IntSliceFlag) string {
return stringifySliceFlag(f.Usage, f.Name, defaultVals)
}
func stringifyInt64SliceFlag(f Int64SliceFlag) string {
defaultVals := []string{}
if f.Value != nil && len(f.Value.Value()) > 0 {
for _, i := range f.Value.Value() {
defaultVals = append(defaultVals, fmt.Sprintf("%d", i))
}
}
return stringifySliceFlag(f.Usage, f.Name, defaultVals)
}
func stringifyStringSliceFlag(f StringSliceFlag) string {
defaultVals := []string{}
if f.Value != nil && len(f.Value.Value()) > 0 {

View File

@ -162,114 +162,6 @@ func TestIntFlagWithEnvVarHelpOutput(t *testing.T) {
}
}
var int64FlagTests = []struct {
name string
expected string
}{
{"hats", "--hats value\t(default: 8589934592)"},
{"H", "-H value\t(default: 8589934592)"},
}
func TestInt64FlagHelpOutput(t *testing.T) {
for _, test := range int64FlagTests {
flag := Int64Flag{Name: test.name, Value: 8589934592}
output := flag.String()
if output != test.expected {
t.Errorf("%s does not match %s", output, test.expected)
}
}
}
func TestInt64FlagWithEnvVarHelpOutput(t *testing.T) {
os.Clearenv()
os.Setenv("APP_BAR", "2")
for _, test := range int64FlagTests {
flag := IntFlag{Name: test.name, EnvVar: "APP_BAR"}
output := flag.String()
expectedSuffix := " [$APP_BAR]"
if runtime.GOOS == "windows" {
expectedSuffix = " [%APP_BAR%]"
}
if !strings.HasSuffix(output, expectedSuffix) {
t.Errorf("%s does not end with"+expectedSuffix, output)
}
}
}
var uintFlagTests = []struct {
name string
expected string
}{
{"nerfs", "--nerfs value\t(default: 41)"},
{"N", "-N value\t(default: 41)"},
}
func TestUintFlagHelpOutput(t *testing.T) {
for _, test := range uintFlagTests {
flag := UintFlag{Name: test.name, Value: 41}
output := flag.String()
if output != test.expected {
t.Errorf("%s does not match %s", output, test.expected)
}
}
}
func TestUintFlagWithEnvVarHelpOutput(t *testing.T) {
os.Clearenv()
os.Setenv("APP_BAR", "2")
for _, test := range uintFlagTests {
flag := UintFlag{Name: test.name, EnvVar: "APP_BAR"}
output := flag.String()
expectedSuffix := " [$APP_BAR]"
if runtime.GOOS == "windows" {
expectedSuffix = " [%APP_BAR%]"
}
if !strings.HasSuffix(output, expectedSuffix) {
t.Errorf("%s does not end with"+expectedSuffix, output)
}
}
}
var uint64FlagTests = []struct {
name string
expected string
}{
{"gerfs", "--gerfs value\t(default: 8589934582)"},
{"G", "-G value\t(default: 8589934582)"},
}
func TestUint64FlagHelpOutput(t *testing.T) {
for _, test := range uint64FlagTests {
flag := Uint64Flag{Name: test.name, Value: 8589934582}
output := flag.String()
if output != test.expected {
t.Errorf("%s does not match %s", output, test.expected)
}
}
}
func TestUint64FlagWithEnvVarHelpOutput(t *testing.T) {
os.Clearenv()
os.Setenv("APP_BAR", "2")
for _, test := range uint64FlagTests {
flag := UintFlag{Name: test.name, EnvVar: "APP_BAR"}
output := flag.String()
expectedSuffix := " [$APP_BAR]"
if runtime.GOOS == "windows" {
expectedSuffix = " [%APP_BAR%]"
}
if !strings.HasSuffix(output, expectedSuffix) {
t.Errorf("%s does not end with"+expectedSuffix, output)
}
}
}
var durationFlagTests = []struct {
name string
expected string
@ -349,49 +241,6 @@ func TestIntSliceFlagWithEnvVarHelpOutput(t *testing.T) {
}
}
var int64SliceFlagTests = []struct {
name string
value *Int64Slice
expected string
}{
{"heads", &Int64Slice{}, "--heads value\t"},
{"H", &Int64Slice{}, "-H value\t"},
{"H, heads", func() *Int64Slice {
i := &Int64Slice{}
i.Set("2")
i.Set("17179869184")
return i
}(), "-H value, --heads value\t(default: 2, 17179869184)"},
}
func TestInt64SliceFlagHelpOutput(t *testing.T) {
for _, test := range int64SliceFlagTests {
flag := Int64SliceFlag{Name: test.name, Value: test.value}
output := flag.String()
if output != test.expected {
t.Errorf("%q does not match %q", output, test.expected)
}
}
}
func TestInt64SliceFlagWithEnvVarHelpOutput(t *testing.T) {
os.Clearenv()
os.Setenv("APP_SMURF", "42,17179869184")
for _, test := range int64SliceFlagTests {
flag := Int64SliceFlag{Name: test.name, Value: test.value, EnvVar: "APP_SMURF"}
output := flag.String()
expectedSuffix := " [$APP_SMURF]"
if runtime.GOOS == "windows" {
expectedSuffix = " [%APP_SMURF%]"
}
if !strings.HasSuffix(output, expectedSuffix) {
t.Errorf("%q does not end with"+expectedSuffix, output)
}
}
}
var float64FlagTests = []struct {
name string
expected string
@ -730,63 +579,6 @@ func TestParseMultiIntSliceFromEnvCascade(t *testing.T) {
}).Run([]string{"run"})
}
func TestParseMultiInt64Slice(t *testing.T) {
(&App{
Flags: []Flag{
Int64SliceFlag{Name: "serve, s", Value: &Int64Slice{}},
},
Action: func(ctx *Context) error {
if !reflect.DeepEqual(ctx.Int64Slice("serve"), []int64{10, 17179869184}) {
t.Errorf("main name not set")
}
if !reflect.DeepEqual(ctx.Int64Slice("s"), []int64{10, 17179869184}) {
t.Errorf("short name not set")
}
return nil
},
}).Run([]string{"run", "-s", "10", "-s", "17179869184"})
}
func TestParseMultiInt64SliceFromEnv(t *testing.T) {
os.Clearenv()
os.Setenv("APP_INTERVALS", "20,30,17179869184")
(&App{
Flags: []Flag{
Int64SliceFlag{Name: "intervals, i", Value: &Int64Slice{}, EnvVar: "APP_INTERVALS"},
},
Action: func(ctx *Context) error {
if !reflect.DeepEqual(ctx.Int64Slice("intervals"), []int64{20, 30, 17179869184}) {
t.Errorf("main name not set from env")
}
if !reflect.DeepEqual(ctx.Int64Slice("i"), []int64{20, 30, 17179869184}) {
t.Errorf("short name not set from env")
}
return nil
},
}).Run([]string{"run"})
}
func TestParseMultiInt64SliceFromEnvCascade(t *testing.T) {
os.Clearenv()
os.Setenv("APP_INTERVALS", "20,30,17179869184")
(&App{
Flags: []Flag{
Int64SliceFlag{Name: "intervals, i", Value: &Int64Slice{}, EnvVar: "COMPAT_INTERVALS,APP_INTERVALS"},
},
Action: func(ctx *Context) error {
if !reflect.DeepEqual(ctx.Int64Slice("intervals"), []int64{20, 30, 17179869184}) {
t.Errorf("main name not set from env")
}
if !reflect.DeepEqual(ctx.Int64Slice("i"), []int64{20, 30, 17179869184}) {
t.Errorf("short name not set from env")
}
return nil
},
}).Run([]string{"run"})
}
func TestParseMultiFloat64(t *testing.T) {
a := App{
Flags: []Flag{

View File

@ -26,7 +26,7 @@ AUTHOR(S):
{{end}}{{if .VisibleCommands}}
COMMANDS:{{range .VisibleCategories}}{{if .Name}}
{{.Name}}:{{end}}{{range .VisibleCommands}}
{{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}
{{.Name}}{{with .ShortName}}, {{.}}{{end}}{{"\t"}}{{.Usage}}{{end}}
{{end}}{{end}}{{if .VisibleFlags}}
GLOBAL OPTIONS:
{{range .VisibleFlags}}{{.}}
@ -67,7 +67,7 @@ USAGE:
COMMANDS:{{range .VisibleCategories}}{{if .Name}}
{{.Name}}:{{end}}{{range .VisibleCommands}}
{{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}
{{.Name}}{{with .ShortName}}, {{.}}{{end}}{{"\t"}}{{.Usage}}{{end}}
{{end}}{{if .VisibleFlags}}
OPTIONS:
{{range .VisibleFlags}}{{.}}
@ -117,9 +117,8 @@ var HelpPrinter helpPrinter = printHelp
var VersionPrinter = printVersion
// ShowAppHelp is an action that displays the help.
func ShowAppHelp(c *Context) error {
func ShowAppHelp(c *Context) {
HelpPrinter(c.App.Writer, AppHelpTemplate, c.App)
return nil
}
// DefaultAppComplete prints the list of subcommands as the default app completion method
@ -192,7 +191,7 @@ func printHelp(out io.Writer, templ string, data interface{}) {
"join": strings.Join,
}
w := tabwriter.NewWriter(out, 1, 8, 2, ' ', 0)
w := tabwriter.NewWriter(out, 0, 8, 1, '\t', 0)
t := template.Must(template.New("help").Funcs(funcMap).Parse(templ))
err := t.Execute(w, data)
if err != nil {

View File

@ -169,76 +169,6 @@ func Test_helpSubcommand_Action_ErrorIfNoTopic(t *testing.T) {
}
}
func TestShowAppHelp_CommandAliases(t *testing.T) {
app := &App{
Commands: []Command{
{
Name: "frobbly",
Aliases: []string{"fr", "frob"},
Action: func(ctx *Context) error {
return nil
},
},
},
}
output := &bytes.Buffer{}
app.Writer = output
app.Run([]string{"foo", "--help"})
if !strings.Contains(output.String(), "frobbly, fr, frob") {
t.Errorf("expected output to include all command aliases; got: %q", output.String())
}
}
func TestShowCommandHelp_CommandAliases(t *testing.T) {
app := &App{
Commands: []Command{
{
Name: "frobbly",
Aliases: []string{"fr", "frob", "bork"},
Action: func(ctx *Context) error {
return nil
},
},
},
}
output := &bytes.Buffer{}
app.Writer = output
app.Run([]string{"foo", "help", "fr"})
if !strings.Contains(output.String(), "frobbly") {
t.Errorf("expected output to include command name; got: %q", output.String())
}
if strings.Contains(output.String(), "bork") {
t.Errorf("expected output to exclude command aliases; got: %q", output.String())
}
}
func TestShowSubcommandHelp_CommandAliases(t *testing.T) {
app := &App{
Commands: []Command{
{
Name: "frobbly",
Aliases: []string{"fr", "frob", "bork"},
Action: func(ctx *Context) error {
return nil
},
},
},
}
output := &bytes.Buffer{}
app.Writer = output
app.Run([]string{"foo", "help"})
if !strings.Contains(output.String(), "frobbly, fr, frob, bork") {
t.Errorf("expected output to include all command aliases; got: %q", output.String())
}
}
func TestShowAppHelp_HiddenCommand(t *testing.T) {
app := &App{
Commands: []Command{

Some files were not shown because too many files have changed in this diff Show More