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 }