hugo

Unnamed repository; edit this file 'description' to name the repository.

git clone git://git.shimmy1996.com/hugo.git
commit eebde0c2ac4964e91d26d8b0cf0ac43afcfd207f
parent e4dc9a82b557a417b1552c533b0df605c6ff1cc0
Author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Date:   Sat, 24 Apr 2021 12:26:51 +0200

langs/i18n: Improve plural handling of floats

The go-i18n library expects plural counts with floats to be represented as strings.

Fixes #8464

Diffstat:
Mcommon/types/types.go | 6++++++
Mlangs/i18n/i18n.go | 42+++++++++++++++++++++++++++++++++---------
Mlangs/i18n/i18n_test.go | 101++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
3 files changed, 127 insertions(+), 22 deletions(-)
diff --git a/common/types/types.go b/common/types/types.go
@@ -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
diff --git a/langs/i18n/i18n.go b/langs/i18n/i18n.go
@@ -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
+	}
 }
diff --git a/langs/i18n/i18n_test.go b/langs/i18n/i18n_test.go
@@ -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)
 }