github.com/linchen2chris/hugo@v0.0.0-20230307053224-cec209389705/langs/i18n/i18n.go (about) 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 "context" 18 "fmt" 19 "reflect" 20 "strings" 21 22 "github.com/spf13/cast" 23 24 "github.com/gohugoio/hugo/common/hreflect" 25 "github.com/gohugoio/hugo/common/loggers" 26 "github.com/gohugoio/hugo/config" 27 "github.com/gohugoio/hugo/helpers" 28 "github.com/gohugoio/hugo/resources/page" 29 30 "github.com/gohugoio/go-i18n/v2/i18n" 31 ) 32 33 type translateFunc func(ctx context.Context, translationID string, templateData any) string 34 35 var i18nWarningLogger = helpers.NewDistinctErrorLogger() 36 37 // Translator handles i18n translations. 38 type Translator struct { 39 translateFuncs map[string]translateFunc 40 cfg config.Provider 41 logger loggers.Logger 42 } 43 44 // NewTranslator creates a new Translator for the given language bundle and configuration. 45 func NewTranslator(b *i18n.Bundle, cfg config.Provider, logger loggers.Logger) Translator { 46 t := Translator{cfg: cfg, logger: logger, translateFuncs: make(map[string]translateFunc)} 47 t.initFuncs(b) 48 return t 49 } 50 51 // Func gets the translate func for the given language, or for the default 52 // configured language if not found. 53 func (t Translator) Func(lang string) translateFunc { 54 if f, ok := t.translateFuncs[lang]; ok { 55 return f 56 } 57 t.logger.Infof("Translation func for language %v not found, use default.", lang) 58 if f, ok := t.translateFuncs[t.cfg.GetString("defaultContentLanguage")]; ok { 59 return f 60 } 61 62 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.") 63 return func(ctx context.Context, translationID string, args any) string { 64 return "" 65 } 66 } 67 68 func (t Translator) initFuncs(bndl *i18n.Bundle) { 69 enableMissingTranslationPlaceholders := t.cfg.GetBool("enableMissingTranslationPlaceholders") 70 for _, lang := range bndl.LanguageTags() { 71 currentLang := lang 72 currentLangStr := currentLang.String() 73 // This may be pt-BR; make it case insensitive. 74 currentLangKey := strings.ToLower(strings.TrimPrefix(currentLangStr, artificialLangTagPrefix)) 75 localizer := i18n.NewLocalizer(bndl, currentLangStr) 76 t.translateFuncs[currentLangKey] = func(ctx context.Context, translationID string, templateData any) string { 77 pluralCount := getPluralCount(templateData) 78 79 if templateData != nil { 80 tp := reflect.TypeOf(templateData) 81 if hreflect.IsInt(tp.Kind()) { 82 // This was how go-i18n worked in v1, 83 // and we keep it like this to avoid breaking 84 // lots of sites in the wild. 85 templateData = intCount(cast.ToInt(templateData)) 86 } else { 87 if p, ok := templateData.(page.Page); ok { 88 // See issue 10782. 89 // The i18n has its own template handling and does not know about 90 // the context.Context. 91 // A common pattern is to pass Page to i18n, and use .ReadingTime etc. 92 // We need to improve this, but that requires some upstream changes. 93 // For now, just creata a wrepper. 94 templateData = page.PageWithContext{Page: p, Ctx: ctx} 95 } 96 } 97 } 98 99 translated, translatedLang, err := localizer.LocalizeWithTag(&i18n.LocalizeConfig{ 100 MessageID: translationID, 101 TemplateData: templateData, 102 PluralCount: pluralCount, 103 }) 104 105 sameLang := currentLang == translatedLang 106 107 if err == nil && sameLang { 108 return translated 109 } 110 111 if err != nil && sameLang && translated != "" { 112 // See #8492 113 // TODO(bep) this needs to be improved/fixed upstream, 114 // but currently we get an error even if the fallback to 115 // "other" succeeds. 116 if fmt.Sprintf("%T", err) == "i18n.pluralFormNotFoundError" { 117 return translated 118 } 119 } 120 121 if _, ok := err.(*i18n.MessageNotFoundErr); !ok { 122 t.logger.Warnf("Failed to get translated string for language %q and ID %q: %s", currentLangStr, translationID, err) 123 } 124 125 if t.cfg.GetBool("logI18nWarnings") { 126 i18nWarningLogger.Printf("i18n|MISSING_TRANSLATION|%s|%s", currentLangStr, translationID) 127 } 128 129 if enableMissingTranslationPlaceholders { 130 return "[i18n] " + translationID 131 } 132 133 return translated 134 } 135 } 136 } 137 138 // intCount wraps the Count method. 139 type intCount int 140 141 func (c intCount) Count() int { 142 return int(c) 143 } 144 145 const countFieldName = "Count" 146 147 // getPluralCount gets the plural count as a string (floats) or an integer. 148 // If v is nil, nil is returned. 149 func getPluralCount(v any) any { 150 if v == nil { 151 // i18n called without any argument, make sure it does not 152 // get any plural count. 153 return nil 154 } 155 156 switch v := v.(type) { 157 case map[string]any: 158 for k, vv := range v { 159 if strings.EqualFold(k, countFieldName) { 160 return toPluralCountValue(vv) 161 } 162 } 163 default: 164 vv := reflect.Indirect(reflect.ValueOf(v)) 165 if vv.Kind() == reflect.Interface && !vv.IsNil() { 166 vv = vv.Elem() 167 } 168 tp := vv.Type() 169 170 if tp.Kind() == reflect.Struct { 171 f := vv.FieldByName(countFieldName) 172 if f.IsValid() { 173 return toPluralCountValue(f.Interface()) 174 } 175 m := hreflect.GetMethodByName(vv, countFieldName) 176 if m.IsValid() && m.Type().NumIn() == 0 && m.Type().NumOut() == 1 { 177 c := m.Call(nil) 178 return toPluralCountValue(c[0].Interface()) 179 } 180 } 181 } 182 183 return toPluralCountValue(v) 184 } 185 186 // go-i18n expects floats to be represented by string. 187 func toPluralCountValue(in any) any { 188 k := reflect.TypeOf(in).Kind() 189 switch { 190 case hreflect.IsFloat(k): 191 f := cast.ToString(in) 192 if !strings.Contains(f, ".") { 193 f += ".0" 194 } 195 return f 196 case k == reflect.String: 197 if _, err := cast.ToFloat64E(in); err == nil { 198 return in 199 } 200 // A non-numeric value. 201 return nil 202 default: 203 if i, err := cast.ToIntE(in); err == nil { 204 return i 205 } 206 return nil 207 } 208 }