summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2021-04-22 09:57:24 +0200
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2021-04-23 07:39:59 +0200
commit537c905ec103dc5adaf8a1b2ccdef5da7cc660fd (patch)
tree735d5bf08ccbb1dca15ae4206d85a286690962b4
parent243951ebe9715d3da3968e96e6f60dcd53e25d92 (diff)
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
-rw-r--r--langs/i18n/i18n.go62
-rw-r--r--langs/i18n/i18n_test.go110
2 files changed, 164 insertions, 8 deletions
diff --git a/langs/i18n/i18n.go b/langs/i18n/i18n.go
index dab620be6..17462bc56 100644
--- 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
index 7b5a10d67..8a2335c92 100644
--- 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)