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  }