hugo

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

git clone git://git.shimmy1996.com/hugo.git
commit 537c905ec103dc5adaf8a1b2ccdef5da7cc660fd
parent 243951ebe9715d3da3968e96e6f60dcd53e25d92
Author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Date:   Thu, 22 Apr 2021 09:57:24 +0200

langs/i18n: Revise the plural implementation

There were some issues introduced with the plural counting when we upgraded from v1 to v2 of go-i18n.

This commit improves that situation given the following rules:

* A single integer argument is used as plural count and passed to the i18n template as a int type with a `.Count` method. The latter is to preserve compability with v1.
* Else the plural count is either fetched from the `Count`/`count` field/method/map or from the value itself.
* Any data type is accepted, if it can be converted to an integer, that value is used.

The above means that you can now do pass a single integer and both of the below will work:

```
{{ . }} minutes to read
{{ .Count }} minutes to read
```

Fixes #8454
Closes #7822
See https://github.com/gohugoio/hugoDocs/issues/1410

Diffstat:
Mlangs/i18n/i18n.go | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mlangs/i18n/i18n_test.go | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 164 insertions(+), 8 deletions(-)
diff --git a/langs/i18n/i18n.go b/langs/i18n/i18n.go
@@ -17,6 +17,8 @@ import (
 	"reflect"
 	"strings"
 
+	"github.com/spf13/cast"
+
 	"github.com/gohugoio/hugo/common/hreflect"
 	"github.com/gohugoio/hugo/common/loggers"
 	"github.com/gohugoio/hugo/config"
@@ -69,17 +71,15 @@ func (t Translator) initFuncs(bndl *i18n.Bundle) {
 		currentLangKey := strings.ToLower(strings.TrimPrefix(currentLangStr, artificialLangTagPrefix))
 		localizer := i18n.NewLocalizer(bndl, currentLangStr)
 		t.translateFuncs[currentLangKey] = func(translationID string, templateData interface{}) string {
-			var pluralCount interface{}
+			pluralCount := getPluralCount(templateData)
 
 			if templateData != nil {
 				tp := reflect.TypeOf(templateData)
-				if hreflect.IsNumber(tp.Kind()) {
-					pluralCount = templateData
-					// This was how go-i18n worked in v1.
-					templateData = map[string]interface{}{
-						"Count": templateData,
-					}
-
+				if hreflect.IsInt(tp.Kind()) {
+					// This was how go-i18n worked in v1,
+					// and we keep it like this to avoid breaking
+					// lots of sites in the wild.
+					templateData = intCount(cast.ToInt(templateData))
 				}
 			}
 
@@ -109,3 +109,49 @@ func (t Translator) initFuncs(bndl *i18n.Bundle) {
 		}
 	}
 }
+
+// intCount wraps the Count method.
+type intCount int
+
+func (c intCount) Count() int {
+	return int(c)
+}
+
+const countFieldName = "Count"
+
+func getPluralCount(o interface{}) int {
+	if o == nil {
+		return 0
+	}
+
+	switch v := o.(type) {
+	case map[string]interface{}:
+		for k, vv := range v {
+			if strings.EqualFold(k, countFieldName) {
+				return cast.ToInt(vv)
+			}
+		}
+	default:
+		vv := reflect.Indirect(reflect.ValueOf(v))
+		if vv.Kind() == reflect.Interface && !vv.IsNil() {
+			vv = vv.Elem()
+		}
+		tp := vv.Type()
+
+		if tp.Kind() == reflect.Struct {
+			f := vv.FieldByName(countFieldName)
+			if f.IsValid() {
+				return cast.ToInt(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 cast.ToInt(o)
+	}
+
+	return 0
+}
diff --git a/langs/i18n/i18n_test.go b/langs/i18n/i18n_test.go
@@ -142,6 +142,20 @@ other = "{{ .Count }} minutes to read"
 		expectedFlag: "One minute to read",
 	},
 	{
+		name: "readingTime-many-dot",
+		data: map[string][]byte{
+			"en.toml": []byte(`[readingTime]
+one = "One minute to read"
+other = "{{ . }} minutes to read"
+`),
+		},
+		args:         21,
+		lang:         "en",
+		id:           "readingTime",
+		expected:     "21 minutes to read",
+		expectedFlag: "21 minutes to read",
+	},
+	{
 		name: "readingTime-many",
 		data: map[string][]byte{
 			"en.toml": []byte(`[readingTime]
@@ -155,6 +169,62 @@ other = "{{ .Count }} minutes to read"
 		expected:     "21 minutes to read",
 		expectedFlag: "21 minutes to read",
 	},
+	// Issue #8454
+	{
+		name: "readingTime-map-one",
+		data: map[string][]byte{
+			"en.toml": []byte(`[readingTime]
+one = "One minute to read"
+other = "{{ .Count }} minutes to read"
+`),
+		},
+		args:         map[string]interface{}{"Count": 1},
+		lang:         "en",
+		id:           "readingTime",
+		expected:     "One minute to read",
+		expectedFlag: "One minute to read",
+	},
+	{
+		name: "readingTime-string-one",
+		data: map[string][]byte{
+			"en.toml": []byte(`[readingTime]
+one = "One minute to read"
+other = "{{ . }} minutes to read"
+`),
+		},
+		args:         "1",
+		lang:         "en",
+		id:           "readingTime",
+		expected:     "One minute to read",
+		expectedFlag: "One minute to read",
+	},
+	{
+		name: "readingTime-map-many",
+		data: map[string][]byte{
+			"en.toml": []byte(`[readingTime]
+one = "One minute to read"
+other = "{{ .Count }} minutes to read"
+`),
+		},
+		args:         map[string]interface{}{"Count": 21},
+		lang:         "en",
+		id:           "readingTime",
+		expected:     "21 minutes to read",
+		expectedFlag: "21 minutes to read",
+	},
+	{
+		name: "argument-float",
+		data: map[string][]byte{
+			"en.toml": []byte(`[float]
+other = "Number is {{ . }}"
+`),
+		},
+		args:         22.5,
+		lang:         "en",
+		id:           "float",
+		expected:     "Number is 22.5",
+		expectedFlag: "Number is 22.5",
+	},
 	// Same id and translation in current language
 	// https://github.com/gohugoio/hugo/issues/2607
 	{
@@ -246,6 +316,46 @@ func doTestI18nTranslate(t testing.TB, test i18nTest, cfg config.Provider) strin
 	return f(test.id, test.args)
 }
 
+type countField struct {
+	Count int
+}
+
+type noCountField struct {
+	Counts int
+}
+
+type countMethod struct {
+}
+
+func (c countMethod) Count() int {
+	return 32
+}
+
+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": 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: 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(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(nil), qt.Equals, 0)
+}
+
 func prepareTranslationProvider(t testing.TB, test i18nTest, cfg config.Provider) *TranslationProvider {
 	c := qt.New(t)
 	fs := hugofs.NewMem(cfg)