hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

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 }