diff options
author | Bjørn Erik Pedersen <[email protected]> | 2021-04-22 09:57:24 +0200 |
---|---|---|
committer | Bjørn Erik Pedersen <[email protected]> | 2021-04-23 07:39:59 +0200 |
commit | 537c905ec103dc5adaf8a1b2ccdef5da7cc660fd (patch) | |
tree | 735d5bf08ccbb1dca15ae4206d85a286690962b4 /langs/i18n | |
parent | 243951ebe9715d3da3968e96e6f60dcd53e25d92 (diff) | |
download | hugo-537c905ec103dc5adaf8a1b2ccdef5da7cc660fd.tar.gz hugo-537c905ec103dc5adaf8a1b2ccdef5da7cc660fd.zip |
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 (limited to 'langs/i18n')
-rw-r--r-- | langs/i18n/i18n.go | 62 | ||||
-rw-r--r-- | langs/i18n/i18n_test.go | 110 |
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) |