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