i18n.go (5632B)
1 // Copyright 2017 The Hugo Authors. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // http://www.apache.org/licenses/LICENSE-2.0 7 // 8 // Unless required by applicable law or agreed to in writing, software 9 // distributed under the License is distributed on an "AS IS" BASIS, 10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package i18n 15 16 import ( 17 "fmt" 18 "reflect" 19 "strings" 20 21 "github.com/spf13/cast" 22 23 "github.com/gohugoio/hugo/common/hreflect" 24 "github.com/gohugoio/hugo/common/loggers" 25 "github.com/gohugoio/hugo/config" 26 "github.com/gohugoio/hugo/helpers" 27 28 "github.com/gohugoio/go-i18n/v2/i18n" 29 ) 30 31 type translateFunc func(translationID string, templateData any) string 32 33 var i18nWarningLogger = helpers.NewDistinctErrorLogger() 34 35 // Translator handles i18n translations. 36 type Translator struct { 37 translateFuncs map[string]translateFunc 38 cfg config.Provider 39 logger loggers.Logger 40 } 41 42 // NewTranslator creates a new Translator for the given language bundle and configuration. 43 func NewTranslator(b *i18n.Bundle, cfg config.Provider, logger loggers.Logger) Translator { 44 t := Translator{cfg: cfg, logger: logger, translateFuncs: make(map[string]translateFunc)} 45 t.initFuncs(b) 46 return t 47 } 48 49 // Func gets the translate func for the given language, or for the default 50 // configured language if not found. 51 func (t Translator) Func(lang string) translateFunc { 52 if f, ok := t.translateFuncs[lang]; ok { 53 return f 54 } 55 t.logger.Infof("Translation func for language %v not found, use default.", lang) 56 if f, ok := t.translateFuncs[t.cfg.GetString("defaultContentLanguage")]; ok { 57 return f 58 } 59 60 t.logger.Infoln("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.") 61 return func(translationID string, args any) string { 62 return "" 63 } 64 } 65 66 func (t Translator) initFuncs(bndl *i18n.Bundle) { 67 enableMissingTranslationPlaceholders := t.cfg.GetBool("enableMissingTranslationPlaceholders") 68 for _, lang := range bndl.LanguageTags() { 69 currentLang := lang 70 currentLangStr := currentLang.String() 71 // This may be pt-BR; make it case insensitive. 72 currentLangKey := strings.ToLower(strings.TrimPrefix(currentLangStr, artificialLangTagPrefix)) 73 localizer := i18n.NewLocalizer(bndl, currentLangStr) 74 t.translateFuncs[currentLangKey] = func(translationID string, templateData any) string { 75 pluralCount := getPluralCount(templateData) 76 77 if templateData != nil { 78 tp := reflect.TypeOf(templateData) 79 if hreflect.IsInt(tp.Kind()) { 80 // This was how go-i18n worked in v1, 81 // and we keep it like this to avoid breaking 82 // lots of sites in the wild. 83 templateData = intCount(cast.ToInt(templateData)) 84 } 85 } 86 87 translated, translatedLang, err := localizer.LocalizeWithTag(&i18n.LocalizeConfig{ 88 MessageID: translationID, 89 TemplateData: templateData, 90 PluralCount: pluralCount, 91 }) 92 93 sameLang := currentLang == translatedLang 94 95 if err == nil && sameLang { 96 return translated 97 } 98 99 if err != nil && sameLang && translated != "" { 100 // See #8492 101 // TODO(bep) this needs to be improved/fixed upstream, 102 // but currently we get an error even if the fallback to 103 // "other" succeeds. 104 if fmt.Sprintf("%T", err) == "i18n.pluralFormNotFoundError" { 105 return translated 106 } 107 } 108 109 if _, ok := err.(*i18n.MessageNotFoundErr); !ok { 110 t.logger.Warnf("Failed to get translated string for language %q and ID %q: %s", currentLangStr, translationID, err) 111 } 112 113 if t.cfg.GetBool("logI18nWarnings") { 114 i18nWarningLogger.Printf("i18n|MISSING_TRANSLATION|%s|%s", currentLangStr, translationID) 115 } 116 117 if enableMissingTranslationPlaceholders { 118 return "[i18n] " + translationID 119 } 120 121 return translated 122 } 123 } 124 } 125 126 // intCount wraps the Count method. 127 type intCount int 128 129 func (c intCount) Count() int { 130 return int(c) 131 } 132 133 const countFieldName = "Count" 134 135 // getPluralCount gets the plural count as a string (floats) or an integer. 136 // If v is nil, nil is returned. 137 func getPluralCount(v any) any { 138 if v == nil { 139 // i18n called without any argument, make sure it does not 140 // get any plural count. 141 return nil 142 } 143 144 switch v := v.(type) { 145 case map[string]any: 146 for k, vv := range v { 147 if strings.EqualFold(k, countFieldName) { 148 return toPluralCountValue(vv) 149 } 150 } 151 default: 152 vv := reflect.Indirect(reflect.ValueOf(v)) 153 if vv.Kind() == reflect.Interface && !vv.IsNil() { 154 vv = vv.Elem() 155 } 156 tp := vv.Type() 157 158 if tp.Kind() == reflect.Struct { 159 f := vv.FieldByName(countFieldName) 160 if f.IsValid() { 161 return toPluralCountValue(f.Interface()) 162 } 163 m := hreflect.GetMethodByName(vv, countFieldName) 164 if m.IsValid() && m.Type().NumIn() == 0 && m.Type().NumOut() == 1 { 165 c := m.Call(nil) 166 return toPluralCountValue(c[0].Interface()) 167 } 168 } 169 } 170 171 return toPluralCountValue(v) 172 } 173 174 // go-i18n expects floats to be represented by string. 175 func toPluralCountValue(in any) any { 176 k := reflect.TypeOf(in).Kind() 177 switch { 178 case hreflect.IsFloat(k): 179 f := cast.ToString(in) 180 if !strings.Contains(f, ".") { 181 f += ".0" 182 } 183 return f 184 case k == reflect.String: 185 if _, err := cast.ToFloat64E(in); err == nil { 186 return in 187 } 188 // A non-numeric value. 189 return nil 190 default: 191 if i, err := cast.ToIntE(in); err == nil { 192 return i 193 } 194 return nil 195 } 196 }