mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-14 20:37:55 -05:00
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
This commit is contained in:
parent
243951ebe9
commit
537c905ec1
2 changed files with 164 additions and 8 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -141,6 +141,20 @@ other = "{{ .Count }} minutes to read"
|
|||
expected: "One minute 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{
|
||||
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue