From eebde0c2ac4964e91d26d8b0cf0ac43afcfd207f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Sat, 24 Apr 2021 12:26:51 +0200 Subject: [PATCH] langs/i18n: Improve plural handling of floats The go-i18n library expects plural counts with floats to be represented as strings. Fixes #8464 --- common/types/types.go | 6 +++ langs/i18n/i18n.go | 42 +++++++++++++---- langs/i18n/i18n_test.go | 101 ++++++++++++++++++++++++++++++++++------ 3 files changed, 127 insertions(+), 22 deletions(-) diff --git a/common/types/types.go b/common/types/types.go index 04a27766e..4f9f02c8d 100644 --- a/common/types/types.go +++ b/common/types/types.go @@ -27,6 +27,12 @@ type RLocker interface { RUnlock() } +// KeyValue is a interface{} tuple. +type KeyValue struct { + Key interface{} + Value interface{} +} + // KeyValueStr is a string tuple. type KeyValueStr struct { Key string diff --git a/langs/i18n/i18n.go b/langs/i18n/i18n.go index 17462bc56..75f0bdaaa 100644 --- a/langs/i18n/i18n.go +++ b/langs/i18n/i18n.go @@ -119,16 +119,17 @@ func (c intCount) Count() int { const countFieldName = "Count" -func getPluralCount(o interface{}) int { - if o == nil { +// getPluralCount gets the plural count as a string (floats) or an integer. +func getPluralCount(v interface{}) interface{} { + if v == nil { return 0 } - switch v := o.(type) { + switch v := v.(type) { case map[string]interface{}: for k, vv := range v { if strings.EqualFold(k, countFieldName) { - return cast.ToInt(vv) + return toPluralCountValue(vv) } } default: @@ -141,17 +142,40 @@ func getPluralCount(o interface{}) int { if tp.Kind() == reflect.Struct { f := vv.FieldByName(countFieldName) if f.IsValid() { - return cast.ToInt(f.Interface()) + return toPluralCountValue(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 toPluralCountValue(c[0].Interface()) } } - - return cast.ToInt(o) } - return 0 + return toPluralCountValue(v) + +} + +// go-i18n expects floats to be represented by string. +func toPluralCountValue(in interface{}) interface{} { + k := reflect.TypeOf(in).Kind() + switch { + case hreflect.IsFloat(k): + f := cast.ToString(in) + if !strings.Contains(f, ".") { + f += ".0" + } + return f + case k == reflect.String: + if _, err := cast.ToFloat64E(in); err == nil { + return in + } + // A non-numeric value. + return 0 + default: + if i, err := cast.ToIntE(in); err == nil { + return i + } + return 0 + } } diff --git a/langs/i18n/i18n_test.go b/langs/i18n/i18n_test.go index 8a2335c92..278ab4446 100644 --- a/langs/i18n/i18n_test.go +++ b/langs/i18n/i18n_test.go @@ -18,6 +18,8 @@ import ( "path/filepath" "testing" + "github.com/gohugoio/hugo/common/types" + "github.com/gohugoio/hugo/modules" "github.com/gohugoio/hugo/tpl/tplimpl" @@ -287,7 +289,6 @@ one = "abc"`), name: "dotted-bare-key", data: map[string][]byte{ "en.toml": []byte(`"shop_nextPage.one" = "Show Me The Money" - `), }, args: nil, @@ -310,6 +311,78 @@ one = "abc"`), }, } +func TestPlural(t *testing.T) { + c := qt.New(t) + + for _, test := range []struct { + name string + lang string + id string + templ string + variants []types.KeyValue + }{ + { + name: "English", + lang: "en", + id: "hour", + templ: ` +[hour] +one = "{{ . }} hour" +other = "{{ . }} hours"`, + variants: []types.KeyValue{ + {Key: 1, Value: "1 hour"}, + {Key: "1", Value: "1 hour"}, + {Key: 1.5, Value: "1.5 hours"}, + {Key: "1.5", Value: "1.5 hours"}, + {Key: 2, Value: "2 hours"}, + {Key: "2", Value: "2 hours"}, + }, + }, + { + name: "Polish", + lang: "pl", + id: "day", + templ: ` +[day] +one = "{{ . }} miesiąc" +few = "{{ . }} miesiące" +many = "{{ . }} miesięcy" +other = "{{ . }} miesiąca" +`, + variants: []types.KeyValue{ + {Key: 1, Value: "1 miesiąc"}, + {Key: 2, Value: "2 miesiące"}, + {Key: 100, Value: "100 miesięcy"}, + {Key: "100.0", Value: "100.0 miesiąca"}, + {Key: 100.0, Value: "100 miesiąca"}, + }, + }, + } { + + c.Run(test.name, func(c *qt.C) { + cfg := getConfig() + fs := hugofs.NewMem(cfg) + + err := afero.WriteFile(fs.Source, filepath.Join("i18n", test.lang+".toml"), []byte(test.templ), 0755) + c.Assert(err, qt.IsNil) + + tp := NewTranslationProvider() + depsCfg := newDepsConfig(tp, cfg, fs) + d, err := deps.New(depsCfg) + c.Assert(err, qt.IsNil) + c.Assert(d.LoadResources(), qt.IsNil) + + f := tp.t.Func(test.lang) + + for _, variant := range test.variants { + c.Assert(f(test.id, variant.Key), qt.Equals, variant.Value, qt.Commentf("input: %v", variant.Key)) + } + + }) + + } +} + func doTestI18nTranslate(t testing.TB, test i18nTest, cfg config.Provider) string { tp := prepareTranslationProvider(t, test, cfg) f := tp.t.Func(test.lang) @@ -317,7 +390,7 @@ func doTestI18nTranslate(t testing.TB, test i18nTest, cfg config.Provider) strin } type countField struct { - Count int + Count interface{} } type noCountField struct { @@ -327,8 +400,8 @@ type noCountField struct { type countMethod struct { } -func (c countMethod) Count() int { - return 32 +func (c countMethod) Count() interface{} { + return 32.5 } func TestGetPluralCount(t *testing.T) { @@ -336,23 +409,25 @@ func TestGetPluralCount(t *testing.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": 1.5}), qt.Equals, "1.5") + c.Assert(getPluralCount(map[string]interface{}{"Count": "32"}), qt.Equals, "32") + c.Assert(getPluralCount(map[string]interface{}{"Count": "32.5"}), qt.Equals, "32.5") 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: 1.5}), qt.Equals, "1.5") 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(countMethod{}), qt.Equals, "32.5") + c.Assert(getPluralCount(&countMethod{}), qt.Equals, "32.5") 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(1234.4), qt.Equals, "1234.4") + c.Assert(getPluralCount(1234.0), qt.Equals, "1234.0") + c.Assert(getPluralCount("1234"), qt.Equals, "1234") + c.Assert(getPluralCount("0.5"), qt.Equals, "0.5") c.Assert(getPluralCount(nil), qt.Equals, 0) }