langs/i18n: Improve plural handling of floats
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sat, 24 Apr 2021 10:26:51 +0000 (12:26 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sun, 25 Apr 2021 09:12:30 +0000 (11:12 +0200)
The go-i18n library expects plural counts with floats to be represented as strings.

Fixes #8464

common/types/types.go
langs/i18n/i18n.go
langs/i18n/i18n_test.go

index 04a27766e9455a262c24160b89beeb7c8ac1a2bb..4f9f02c8d7d63a213fe8171bf33c9fd2cb6e92be 100644 (file)
@@ -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
index 17462bc560cb1eb6cd15a4dcaa235955f29f37a6..75f0bdaaa68f4c17bc0b3c0c62fb5c90e5cc0ec9 100644 (file)
@@ -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:
@@ -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
+       }
 }
index 8a2335c92be341eb6aa63533d3d007088eec4d75..278ab44467960670edf64d6136aae32cd110651c 100644 (file)
@@ -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"
@@ -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,
@@ -310,6 +311,78 @@ 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)
@@ -317,7 +390,7 @@ func doTestI18nTranslate(t testing.TB, test i18nTest, cfg config.Provider) strin
 }
 
 type countField struct {
-       Count int
+       Count interface{}
 }
 
 type noCountField struct {
@@ -327,8 +400,8 @@ 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) {
@@ -336,23 +409,25 @@ func TestGetPluralCount(t *testing.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)
 }