Skip to content

Commit

Permalink
langs/i18n: Improve plural handling of floats
Browse files Browse the repository at this point in the history
The go-i18n library expects plural counts with floats to be represented as strings.

Fixes #8464
  • Loading branch information
bep committed Apr 25, 2021
1 parent e4dc9a8 commit eebde0c
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 22 deletions.
6 changes: 6 additions & 0 deletions common/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ type RLocker interface {
RUnlock()
}

// KeyValue is a interface{} tuple.
type KeyValue struct {
Key interface{}
Value interface{}
}

// KeyValueStr is a string tuple.
type KeyValueStr struct {
Key string
Expand Down
42 changes: 33 additions & 9 deletions langs/i18n/i18n.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,16 +119,17 @@ func (c intCount) Count() int {

const countFieldName = "Count"

func getPluralCount(o interface{}) int {
if o == nil {
// getPluralCount gets the plural count as a string (floats) or an integer.
func getPluralCount(v interface{}) interface{} {
if v == nil {
return 0
}

switch v := o.(type) {
switch v := v.(type) {
case map[string]interface{}:
for k, vv := range v {
if strings.EqualFold(k, countFieldName) {
return cast.ToInt(vv)
return toPluralCountValue(vv)
}
}
default:
Expand All @@ -141,17 +142,40 @@ func getPluralCount(o interface{}) int {
if tp.Kind() == reflect.Struct {
f := vv.FieldByName(countFieldName)
if f.IsValid() {
return cast.ToInt(f.Interface())
return toPluralCountValue(f.Interface())
}
m := vv.MethodByName(countFieldName)
if m.IsValid() && m.Type().NumIn() == 0 && m.Type().NumOut() == 1 {
c := m.Call(nil)
return cast.ToInt(c[0].Interface())
return toPluralCountValue(c[0].Interface())
}
}

return cast.ToInt(o)
}

return 0
return toPluralCountValue(v)

}

// go-i18n expects floats to be represented by string.
func toPluralCountValue(in interface{}) interface{} {
k := reflect.TypeOf(in).Kind()
switch {
case hreflect.IsFloat(k):
f := cast.ToString(in)
if !strings.Contains(f, ".") {
f += ".0"
}
return f
case k == reflect.String:
if _, err := cast.ToFloat64E(in); err == nil {
return in
}
// A non-numeric value.
return 0
default:
if i, err := cast.ToIntE(in); err == nil {
return i
}
return 0
}
}
101 changes: 88 additions & 13 deletions langs/i18n/i18n_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (
"path/filepath"
"testing"

"github.com/gohugoio/hugo/common/types"

"github.com/gohugoio/hugo/modules"

"github.com/gohugoio/hugo/tpl/tplimpl"
Expand Down Expand Up @@ -287,7 +289,6 @@ one = "abc"`),
name: "dotted-bare-key",
data: map[string][]byte{
"en.toml": []byte(`"shop_nextPage.one" = "Show Me The Money"
`),
},
args: nil,
Expand All @@ -310,14 +311,86 @@ one = "abc"`),
},
}

func TestPlural(t *testing.T) {
c := qt.New(t)

for _, test := range []struct {
name string
lang string
id string
templ string
variants []types.KeyValue
}{
{
name: "English",
lang: "en",
id: "hour",
templ: `
[hour]
one = "{{ . }} hour"
other = "{{ . }} hours"`,
variants: []types.KeyValue{
{Key: 1, Value: "1 hour"},
{Key: "1", Value: "1 hour"},
{Key: 1.5, Value: "1.5 hours"},
{Key: "1.5", Value: "1.5 hours"},
{Key: 2, Value: "2 hours"},
{Key: "2", Value: "2 hours"},
},
},
{
name: "Polish",
lang: "pl",
id: "day",
templ: `
[day]
one = "{{ . }} miesiąc"
few = "{{ . }} miesiące"
many = "{{ . }} miesięcy"
other = "{{ . }} miesiąca"
`,
variants: []types.KeyValue{
{Key: 1, Value: "1 miesiąc"},
{Key: 2, Value: "2 miesiące"},
{Key: 100, Value: "100 miesięcy"},
{Key: "100.0", Value: "100.0 miesiąca"},
{Key: 100.0, Value: "100 miesiąca"},
},
},
} {

c.Run(test.name, func(c *qt.C) {
cfg := getConfig()
fs := hugofs.NewMem(cfg)

err := afero.WriteFile(fs.Source, filepath.Join("i18n", test.lang+".toml"), []byte(test.templ), 0755)
c.Assert(err, qt.IsNil)

tp := NewTranslationProvider()
depsCfg := newDepsConfig(tp, cfg, fs)
d, err := deps.New(depsCfg)
c.Assert(err, qt.IsNil)
c.Assert(d.LoadResources(), qt.IsNil)

f := tp.t.Func(test.lang)

for _, variant := range test.variants {
c.Assert(f(test.id, variant.Key), qt.Equals, variant.Value, qt.Commentf("input: %v", variant.Key))
}

})

}
}

func doTestI18nTranslate(t testing.TB, test i18nTest, cfg config.Provider) string {
tp := prepareTranslationProvider(t, test, cfg)
f := tp.t.Func(test.lang)
return f(test.id, test.args)
}

type countField struct {
Count int
Count interface{}
}

type noCountField struct {
Expand All @@ -327,32 +400,34 @@ type noCountField struct {
type countMethod struct {
}

func (c countMethod) Count() int {
return 32
func (c countMethod) Count() interface{} {
return 32.5
}

func TestGetPluralCount(t *testing.T) {
c := qt.New(t)

c.Assert(getPluralCount(map[string]interface{}{"Count": 32}), qt.Equals, 32)
c.Assert(getPluralCount(map[string]interface{}{"Count": 1}), qt.Equals, 1)
c.Assert(getPluralCount(map[string]interface{}{"Count": "32"}), qt.Equals, 32)
c.Assert(getPluralCount(map[string]interface{}{"Count": 1.5}), qt.Equals, "1.5")
c.Assert(getPluralCount(map[string]interface{}{"Count": "32"}), qt.Equals, "32")
c.Assert(getPluralCount(map[string]interface{}{"Count": "32.5"}), qt.Equals, "32.5")
c.Assert(getPluralCount(map[string]interface{}{"count": 32}), qt.Equals, 32)
c.Assert(getPluralCount(map[string]interface{}{"Count": "32"}), qt.Equals, 32)
c.Assert(getPluralCount(map[string]interface{}{"Count": "32"}), qt.Equals, "32")
c.Assert(getPluralCount(map[string]interface{}{"Counts": 32}), qt.Equals, 0)
c.Assert(getPluralCount("foo"), qt.Equals, 0)
c.Assert(getPluralCount(countField{Count: 22}), qt.Equals, 22)
c.Assert(getPluralCount(countField{Count: 1.5}), qt.Equals, "1.5")
c.Assert(getPluralCount(&countField{Count: 22}), qt.Equals, 22)
c.Assert(getPluralCount(noCountField{Counts: 23}), qt.Equals, 0)
c.Assert(getPluralCount(countMethod{}), qt.Equals, 32)
c.Assert(getPluralCount(&countMethod{}), qt.Equals, 32)
c.Assert(getPluralCount(countMethod{}), qt.Equals, "32.5")
c.Assert(getPluralCount(&countMethod{}), qt.Equals, "32.5")

c.Assert(getPluralCount(1234), qt.Equals, 1234)
c.Assert(getPluralCount(1234.4), qt.Equals, 1234)
c.Assert(getPluralCount(1234.6), qt.Equals, 1234)
c.Assert(getPluralCount(0.6), qt.Equals, 0)
c.Assert(getPluralCount(1.0), qt.Equals, 1)
c.Assert(getPluralCount("1234"), qt.Equals, 1234)
c.Assert(getPluralCount(1234.4), qt.Equals, "1234.4")
c.Assert(getPluralCount(1234.0), qt.Equals, "1234.0")
c.Assert(getPluralCount("1234"), qt.Equals, "1234")
c.Assert(getPluralCount("0.5"), qt.Equals, "0.5")
c.Assert(getPluralCount(nil), qt.Equals, 0)
}

Expand Down

0 comments on commit eebde0c

Please # to comment.