langs/i18n: Upgrade to go-i18n v2
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sun, 2 Jun 2019 09:11:46 +0000 (11:11 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Tue, 29 Sep 2020 15:48:07 +0000 (17:48 +0200)
Fixes #5242

common/hreflect/helpers.go
deps/deps.go
go.mod
go.sum
langs/i18n/i18n.go
langs/i18n/translationProvider.go
tpl/lang/lang.go

index db7b208b5b658c5da280899aa118ca38df34c2ac..d936da19cf743c3b640cb5e626742185536e3f21 100644 (file)
@@ -22,6 +22,42 @@ import (
        "github.com/gohugoio/hugo/common/types"
 )
 
+// TODO(bep) replace the private versions in /tpl with these.
+// IsInt returns whether the given kind is a number.
+func IsNumber(kind reflect.Kind) bool {
+       return IsInt(kind) || IsUint(kind) || IsFloat(kind)
+}
+
+// IsInt returns whether the given kind is an int.
+func IsInt(kind reflect.Kind) bool {
+       switch kind {
+       case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+               return true
+       default:
+               return false
+       }
+}
+
+// IsUint returns whether the given kind is an uint.
+func IsUint(kind reflect.Kind) bool {
+       switch kind {
+       case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+               return true
+       default:
+               return false
+       }
+}
+
+// IsFloat returns whether the given kind is a float.
+func IsFloat(kind reflect.Kind) bool {
+       switch kind {
+       case reflect.Float32, reflect.Float64:
+               return true
+       default:
+               return false
+       }
+}
+
 // IsTruthful returns whether in represents a truthful value.
 // See IsTruthfulValue
 func IsTruthful(in interface{}) bool {
index 07fe2fc7d4cb69cb2f8b168bf230a28ace3e0662..cfd39bf7d8ebbd2000ebfd1665b0d19e816367a3 100644 (file)
@@ -66,7 +66,7 @@ type Deps struct {
        FileCaches filecache.Caches
 
        // The translation func to use
-       Translate func(translationID string, args ...interface{}) string `json:"-"`
+       Translate func(translationID string, templateData interface{}) string `json:"-"`
 
        // The language in use. TODO(bep) consolidate with site
        Language *langs.Language
diff --git a/go.mod b/go.mod
index 330ef07ced6b5899b67b16ce4eaf8ba584c5d51b..33427beeb78663c40e26cef65f0ac415e91bd164 100644 (file)
--- a/go.mod
+++ b/go.mod
@@ -35,7 +35,7 @@ require (
        github.com/mitchellh/mapstructure v1.3.3
        github.com/muesli/smartcrop v0.3.0
        github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
-       github.com/nicksnyder/go-i18n v1.10.1
+       github.com/nicksnyder/go-i18n/v2 v2.1.1
        github.com/niklasfasching/go-org v1.3.2
        github.com/olekukonko/tablewriter v0.0.4
        github.com/pelletier/go-toml v1.6.0 // indirect
diff --git a/go.sum b/go.sum
index e8f08b6569572a72d574466f24fee7b1be158ee7..38cf6423787edb27e6cc7d7c7857ceb3a12ea826 100644 (file)
--- a/go.sum
+++ b/go.sum
@@ -352,6 +352,8 @@ github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6
 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
 github.com/nicksnyder/go-i18n v1.10.1 h1:isfg77E/aCD7+0lD/D00ebR2MV5vgeQ276WYyDaCRQc=
 github.com/nicksnyder/go-i18n v1.10.1/go.mod h1:e4Di5xjP9oTVrC6y3C7C0HoSYXjSbhh/dU0eUV32nB4=
+github.com/nicksnyder/go-i18n/v2 v2.1.1 h1:ATCOanRDlrfKVB4WHAdJnLEqZtDmKYsweqsOUYflnBU=
+github.com/nicksnyder/go-i18n/v2 v2.1.1/go.mod h1:d++QJC9ZVf7pa48qrsRWhMJ5pSHIPmS3OLqK1niyLxs=
 github.com/niklasfasching/go-org v1.3.2 h1:ZKTSd+GdJYkoZl1pBXLR/k7DRiRXnmB96TRiHmHdzwI=
 github.com/niklasfasching/go-org v1.3.2/go.mod h1:AsLD6X7djzRIz4/RFZu8vwRL0VGjUvGZCCH1Nz0VdrU=
 github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
index 5beef8683127656ed2281638dec7a0a60dc0992e..922b06367e0e37fd8750d4993c25ed26f63b50d5 100644 (file)
 package i18n
 
 import (
+       "reflect"
+       "strings"
+
+       "github.com/gohugoio/hugo/common/hreflect"
        "github.com/gohugoio/hugo/common/loggers"
        "github.com/gohugoio/hugo/config"
        "github.com/gohugoio/hugo/helpers"
 
-       "github.com/nicksnyder/go-i18n/i18n/bundle"
-       "github.com/nicksnyder/go-i18n/i18n/translation"
+       "github.com/nicksnyder/go-i18n/v2/i18n"
 )
 
+type translateFunc func(translationID string, templateData interface{}) string
+
 var (
        i18nWarningLogger = helpers.NewDistinctFeedbackLogger()
 )
 
 // Translator handles i18n translations.
 type Translator struct {
-       translateFuncs map[string]bundle.TranslateFunc
+       translateFuncs map[string]translateFunc
        cfg            config.Provider
        logger         *loggers.Logger
 }
 
 // NewTranslator creates a new Translator for the given language bundle and configuration.
-func NewTranslator(b *bundle.Bundle, cfg config.Provider, logger *loggers.Logger) Translator {
-       t := Translator{cfg: cfg, logger: logger, translateFuncs: make(map[string]bundle.TranslateFunc)}
+func NewTranslator(b *i18n.Bundle, cfg config.Provider, logger *loggers.Logger) Translator {
+       t := Translator{cfg: cfg, logger: logger, translateFuncs: make(map[string]translateFunc)}
        t.initFuncs(b)
        return t
 }
 
 // Func gets the translate func for the given language, or for the default
 // configured language if not found.
-func (t Translator) Func(lang string) bundle.TranslateFunc {
+func (t Translator) Func(lang string) translateFunc {
        if f, ok := t.translateFuncs[lang]; ok {
                return f
        }
@@ -50,68 +55,57 @@ func (t Translator) Func(lang string) bundle.TranslateFunc {
        if f, ok := t.translateFuncs[t.cfg.GetString("defaultContentLanguage")]; ok {
                return f
        }
+
        t.logger.INFO.Println("i18n not initialized; if you need string translations, check that you have a bundle in /i18n that matches the site language or the default language.")
-       return func(translationID string, args ...interface{}) string {
+       return func(translationID string, args interface{}) string {
                return ""
        }
 
 }
 
-func (t Translator) initFuncs(bndl *bundle.Bundle) {
-       defaultContentLanguage := t.cfg.GetString("defaultContentLanguage")
-
-       defaultT, err := bndl.Tfunc(defaultContentLanguage)
-       if err != nil {
-               t.logger.INFO.Printf("No translation bundle found for default language %q", defaultContentLanguage)
-       }
-
-       translations := bndl.Translations()
-
+func (t Translator) initFuncs(bndl *i18n.Bundle) {
        enableMissingTranslationPlaceholders := t.cfg.GetBool("enableMissingTranslationPlaceholders")
        for _, lang := range bndl.LanguageTags() {
-               currentLang := lang
 
-               t.translateFuncs[currentLang] = func(translationID string, args ...interface{}) string {
-                       tFunc, err := bndl.Tfunc(currentLang)
-                       if err != nil {
-                               t.logger.WARN.Printf("could not load translations for language %q (%s), will use default content language.\n", lang, err)
+               currentLang := lang
+               currentLangStr := currentLang.String()
+               currentLangKey := strings.TrimPrefix(currentLangStr, artificialLangTagPrefix)
+               localizer := i18n.NewLocalizer(bndl, currentLangStr)
+
+               t.translateFuncs[currentLangKey] = func(translationID string, templateData interface{}) string {
+
+                       if templateData != nil {
+                               tp := reflect.TypeOf(templateData)
+                               if hreflect.IsNumber(tp.Kind()) {
+                                       // This was how go-i18n worked in v1.
+                                       templateData = map[string]interface{}{
+                                               "Count": templateData,
+                                       }
+                               }
                        }
 
-                       translated := tFunc(translationID, args...)
-                       if translated != translationID {
+                       translated, translatedLang, err := localizer.LocalizeWithTag(&i18n.LocalizeConfig{
+                               MessageID:    translationID,
+                               TemplateData: templateData,
+                       })
+
+                       if err == nil && currentLang == translatedLang {
                                return translated
                        }
-                       // If there is no translation for translationID,
-                       // then Tfunc returns translationID itself.
-                       // But if user set same translationID and translation, we should check
-                       // if it really untranslated:
-                       if isIDTranslated(translations, currentLang, translationID) {
-                               return translated
+
+                       if _, ok := err.(*i18n.MessageNotFoundErr); !ok {
+                               t.logger.WARN.Printf("Failed to get translated string for language %q and ID %q: %s", currentLangStr, translationID, err)
                        }
 
                        if t.cfg.GetBool("logI18nWarnings") {
-                               i18nWarningLogger.Printf("i18n|MISSING_TRANSLATION|%s|%s", currentLang, translationID)
+                               i18nWarningLogger.Printf("i18n|MISSING_TRANSLATION|%s|%s", currentLangStr, translationID)
                        }
+
                        if enableMissingTranslationPlaceholders {
                                return "[i18n] " + translationID
                        }
-                       if defaultT != nil {
-                               translated := defaultT(translationID, args...)
-                               if translated != translationID {
-                                       return translated
-                               }
-                               if isIDTranslated(translations, defaultContentLanguage, translationID) {
-                                       return translated
-                               }
-                       }
-                       return ""
+
+                       return translated
                }
        }
 }
-
-// If the translation map contains translationID for specified currentLang,
-// then the translationID is actually translated.
-func isIDTranslated(translations map[string]map[string]translation.Translation, lang, id string) bool {
-       _, contains := translations[lang][id]
-       return contains
-}
index 4ce9b59fe3454ac945ec09094b65ede62e9458ac..d191c0077609844e06cd03ec6aa52c59e1bea706 100644 (file)
 package i18n
 
 import (
-       "errors"
+       "encoding/json"
 
        "github.com/gohugoio/hugo/common/herrors"
+       "golang.org/x/text/language"
+       yaml "gopkg.in/yaml.v2"
 
-       "github.com/gohugoio/hugo/deps"
+       "github.com/BurntSushi/toml"
        "github.com/gohugoio/hugo/helpers"
+       "github.com/nicksnyder/go-i18n/v2/i18n"
+
+       "github.com/gohugoio/hugo/deps"
        "github.com/gohugoio/hugo/hugofs"
        "github.com/gohugoio/hugo/source"
-       "github.com/nicksnyder/go-i18n/i18n/bundle"
-       "github.com/nicksnyder/go-i18n/i18n/language"
        _errors "github.com/pkg/errors"
 )
 
@@ -42,13 +45,10 @@ func NewTranslationProvider() *TranslationProvider {
 func (tp *TranslationProvider) Update(d *deps.Deps) error {
        spec := source.NewSourceSpec(d.PathSpec, nil)
 
-       i18nBundle := bundle.New()
-
-       en := language.GetPluralSpec("en")
-       if en == nil {
-               return errors.New("the English language has vanished like an old oak table")
-       }
-       var newLangs []string
+       bundle := i18n.NewBundle(language.English)
+       bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
+       bundle.RegisterUnmarshalFunc("yaml", yaml.Unmarshal)
+       bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
 
        // The source dirs are ordered so the most important comes first. Since this is a
        // last key win situation, we have to reverse the iteration order.
@@ -56,33 +56,18 @@ func (tp *TranslationProvider) Update(d *deps.Deps) error {
        for i := len(dirs) - 1; i >= 0; i-- {
                dir := dirs[i]
                src := spec.NewFilesystemFromFileMetaInfo(dir)
-
                files, err := src.Files()
                if err != nil {
                        return err
                }
-
-               for _, r := range files {
-                       currentSpec := language.GetPluralSpec(r.BaseFileName())
-                       if currentSpec == nil {
-                               // This may is a language code not supported by go-i18n, it may be
-                               // Klingon or ... not even a fake language. Make sure it works.
-                               newLangs = append(newLangs, r.BaseFileName())
-                       }
-               }
-
-               if len(newLangs) > 0 {
-                       language.RegisterPluralSpec(newLangs, en)
-               }
-
                for _, file := range files {
-                       if err := addTranslationFile(i18nBundle, file); err != nil {
+                       if err := addTranslationFile(bundle, file); err != nil {
                                return err
                        }
                }
        }
 
-       tp.t = NewTranslator(i18nBundle, d.Cfg, d.Log)
+       tp.t = NewTranslator(bundle, d.Cfg, d.Log)
 
        d.Translate = tp.t.Func(d.Language.Lang)
 
@@ -90,16 +75,29 @@ func (tp *TranslationProvider) Update(d *deps.Deps) error {
 
 }
 
-func addTranslationFile(bundle *bundle.Bundle, r source.File) error {
+const artificialLangTagPrefix = "art-x-"
+
+func addTranslationFile(bundle *i18n.Bundle, r source.File) error {
        f, err := r.FileInfo().Meta().Open()
        if err != nil {
                return _errors.Wrapf(err, "failed to open translations file %q:", r.LogicalName())
        }
-       err = bundle.ParseTranslationFileBytes(r.LogicalName(), helpers.ReaderToBytes(f))
+
+       b := helpers.ReaderToBytes(f)
        f.Close()
+
+       name := r.LogicalName()
+       lang := helpers.Filename(name)
+       tag := language.Make(lang)
+       if tag == language.Und {
+               name = artificialLangTagPrefix + name
+       }
+
+       _, err = bundle.ParseMessageFileBytes(b, name)
        if err != nil {
                return errWithFileContext(_errors.Wrapf(err, "failed to load translations"), r)
        }
+
        return nil
 }
 
index 491e2492e001dce04e0d8e3f1d3ba684992bc97f..4e6c9c70a9109846c10cce17886039c7524ffa83 100644 (file)
 package lang
 
 import (
-       "errors"
        "fmt"
        "math"
        "strconv"
        "strings"
 
+       "github.com/pkg/errors"
+
        "github.com/gohugoio/hugo/deps"
        "github.com/spf13/cast"
 )
@@ -39,12 +40,21 @@ type Namespace struct {
 
 // Translate returns a translated string for id.
 func (ns *Namespace) Translate(id interface{}, args ...interface{}) (string, error) {
+       var templateData interface{}
+
+       if len(args) > 0 {
+               if len(args) > 1 {
+                       return "", errors.Errorf("wrong number of arguments, expecting at most 2, got %d", len(args)+1)
+               }
+               templateData = args[0]
+       }
+
        sid, err := cast.ToStringE(id)
        if err != nil {
                return "", nil
        }
 
-       return ns.deps.Translate(sid, args...), nil
+       return ns.deps.Translate(sid, templateData), nil
 }
 
 // NumFmt formats a number with the given precision using the