From 83beb657e0e1e10814f08ede90aaf1e29581200a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Thu, 22 Apr 2021 09:57:24 +0200 Subject: [PATCH] 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 `.Count`. 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. Fixes #8454 Closes #7822 See https://github.com/gohugoio/hugoDocs/issues/1410 --- langs/i18n/i18n.go | 51 +++++++++++++++++++--- langs/i18n/i18n_test.go | 94 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 5 deletions(-) diff --git a/langs/i18n/i18n.go b/langs/i18n/i18n.go index dab620be60a..51baa5a7c04 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,17 @@ 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. + 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 = map[string]interface{}{ "Count": templateData, } - } } @@ -109,3 +111,42 @@ func (t Translator) initFuncs(bndl *i18n.Bundle) { } } } + +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 7b5a10d675d..642926a70f0 100644 --- a/langs/i18n/i18n_test.go +++ b/langs/i18n/i18n_test.go @@ -155,6 +155,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 +302,44 @@ 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": "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("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)