Skip to content

Commit

Permalink
langs/i18n: Revise the plural implementation
Browse files Browse the repository at this point in the history
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 gohugoio#8454
Closes gohugoio#7822
See gohugoio/hugoDocs#1410
  • Loading branch information
bep committed Apr 22, 2021
1 parent bca40cf commit 83beb65
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 5 deletions.
51 changes: 46 additions & 5 deletions langs/i18n/i18n.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
}

}
}

Expand Down Expand Up @@ -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
}
94 changes: 94 additions & 0 deletions langs/i18n/i18n_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 83beb65

Please sign in to comment.