github.com/kovansky/hugo@v0.92.3-0.20220224232819-63076e4ff19f/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  	"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 interface{}) 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 interface{}) 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 interface{}) 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 interface{}) interface{} {
   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]interface{}:
   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 := vv.MethodByName(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  
   175  // go-i18n expects floats to be represented by string.
   176  func toPluralCountValue(in interface{}) interface{} {
   177  	k := reflect.TypeOf(in).Kind()
   178  	switch {
   179  	case hreflect.IsFloat(k):
   180  		f := cast.ToString(in)
   181  		if !strings.Contains(f, ".") {
   182  			f += ".0"
   183  		}
   184  		return f
   185  	case k == reflect.String:
   186  		if _, err := cast.ToFloat64E(in); err == nil {
   187  			return in
   188  		}
   189  		// A non-numeric value.
   190  		return nil
   191  	default:
   192  		if i, err := cast.ToIntE(in); err == nil {
   193  			return i
   194  		}
   195  		return nil
   196  	}
   197  }