2017-02-04 22:20:06 -05:00
// Copyright 2017 The Hugo Authors. All rights reserved.
2016-05-14 00:35:16 -04:00
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
2017-02-04 22:20:06 -05:00
package i18n
2016-05-14 00:35:16 -04:00
import (
2023-03-04 08:43:23 -05:00
"context"
2021-05-02 08:06:16 -04:00
"fmt"
2019-06-02 05:11:46 -04:00
"reflect"
"strings"
2021-04-22 03:57:24 -04:00
"github.com/spf13/cast"
2019-06-02 05:11:46 -04:00
"github.com/gohugoio/hugo/common/hreflect"
2018-10-03 08:58:09 -04:00
"github.com/gohugoio/hugo/common/loggers"
2017-06-13 12:42:45 -04:00
"github.com/gohugoio/hugo/config"
2023-03-04 08:43:23 -05:00
"github.com/gohugoio/hugo/resources/page"
2018-10-03 08:58:09 -04:00
2021-04-23 01:55:52 -04:00
"github.com/gohugoio/go-i18n/v2/i18n"
2016-08-09 05:41:56 -04:00
)
2023-03-04 08:43:23 -05:00
type translateFunc func ( ctx context . Context , translationID string , templateData any ) string
2019-06-02 05:11:46 -04:00
2017-02-04 22:20:06 -05:00
// Translator handles i18n translations.
type Translator struct {
2019-06-02 05:11:46 -04:00
translateFuncs map [ string ] translateFunc
2023-01-04 12:24:36 -05:00
cfg config . AllProvider
2020-10-21 05:17:48 -04:00
logger loggers . Logger
2016-07-26 08:44:37 -04:00
}
2017-02-04 22:20:06 -05:00
// NewTranslator creates a new Translator for the given language bundle and configuration.
2023-01-04 12:24:36 -05:00
func NewTranslator ( b * i18n . Bundle , cfg config . AllProvider , logger loggers . Logger ) Translator {
2019-06-02 05:11:46 -04:00
t := Translator { cfg : cfg , logger : logger , translateFuncs : make ( map [ string ] translateFunc ) }
2017-02-04 22:20:06 -05:00
t . initFuncs ( b )
return t
}
2016-07-26 08:44:37 -04:00
2017-02-04 22:20:06 -05:00
// Func gets the translate func for the given language, or for the default
// configured language if not found.
2019-06-02 05:11:46 -04:00
func ( t Translator ) Func ( lang string ) translateFunc {
2017-02-04 22:20:06 -05:00
if f , ok := t . translateFuncs [ lang ] ; ok {
return f
}
2020-10-21 05:17:48 -04:00
t . logger . Infof ( "Translation func for language %v not found, use default." , lang )
2023-01-04 12:24:36 -05:00
if f , ok := t . translateFuncs [ t . cfg . DefaultContentLanguage ( ) ] ; ok {
2017-02-04 22:20:06 -05:00
return f
}
2019-06-02 05:11:46 -04:00
2020-10-21 05:17:48 -04:00
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." )
2023-03-04 08:43:23 -05:00
return func ( ctx context . Context , translationID string , args any ) string {
2017-02-04 22:20:06 -05:00
return ""
2016-07-26 08:44:37 -04:00
}
}
2019-06-02 05:11:46 -04:00
func ( t Translator ) initFuncs ( bndl * i18n . Bundle ) {
2023-01-04 12:24:36 -05:00
enableMissingTranslationPlaceholders := t . cfg . EnableMissingTranslationPlaceholders ( )
2016-07-26 08:44:37 -04:00
for _ , lang := range bndl . LanguageTags ( ) {
2019-06-02 05:11:46 -04:00
currentLang := lang
currentLangStr := currentLang . String ( )
2020-10-09 04:00:50 -04:00
// This may be pt-BR; make it case insensitive.
currentLangKey := strings . ToLower ( strings . TrimPrefix ( currentLangStr , artificialLangTagPrefix ) )
2019-06-02 05:11:46 -04:00
localizer := i18n . NewLocalizer ( bndl , currentLangStr )
2023-03-04 08:43:23 -05:00
t . translateFuncs [ currentLangKey ] = func ( ctx context . Context , translationID string , templateData any ) string {
2021-04-22 03:57:24 -04:00
pluralCount := getPluralCount ( templateData )
2020-10-06 14:32:52 -04:00
2019-06-02 05:11:46 -04:00
if templateData != nil {
tp := reflect . TypeOf ( templateData )
2021-04-22 03:57:24 -04:00
if hreflect . IsInt ( tp . Kind ( ) ) {
// This was how go-i18n worked in v1,
// and we keep it like this to avoid breaking
// lots of sites in the wild.
templateData = intCount ( cast . ToInt ( templateData ) )
2023-03-04 08:43:23 -05:00
} else {
if p , ok := templateData . ( page . Page ) ; ok {
// See issue 10782.
// The i18n has its own template handling and does not know about
// the context.Context.
// A common pattern is to pass Page to i18n, and use .ReadingTime etc.
// We need to improve this, but that requires some upstream changes.
// For now, just creata a wrepper.
templateData = page . PageWithContext { Page : p , Ctx : ctx }
}
2019-06-02 05:11:46 -04:00
}
2017-05-02 13:01:05 -04:00
}
2019-06-02 05:11:46 -04:00
translated , translatedLang , err := localizer . LocalizeWithTag ( & i18n . LocalizeConfig {
MessageID : translationID ,
TemplateData : templateData ,
2020-10-06 14:32:52 -04:00
PluralCount : pluralCount ,
2019-06-02 05:11:46 -04:00
} )
2021-05-02 08:06:16 -04:00
sameLang := currentLang == translatedLang
if err == nil && sameLang {
2017-05-03 03:11:14 -04:00
return translated
}
2019-06-02 05:11:46 -04:00
2021-05-02 08:06:16 -04:00
if err != nil && sameLang && translated != "" {
// See #8492
// TODO(bep) this needs to be improved/fixed upstream,
// but currently we get an error even if the fallback to
// "other" succeeds.
if fmt . Sprintf ( "%T" , err ) == "i18n.pluralFormNotFoundError" {
return translated
}
}
2019-06-02 05:11:46 -04:00
if _ , ok := err . ( * i18n . MessageNotFoundErr ) ; ! ok {
2020-10-21 05:17:48 -04:00
t . logger . Warnf ( "Failed to get translated string for language %q and ID %q: %s" , currentLangStr , translationID , err )
2016-08-09 05:41:56 -04:00
}
2017-05-03 03:11:14 -04:00
2023-06-30 02:47:11 -04:00
if t . cfg . PrintI18nWarnings ( ) {
2023-06-16 02:17:42 -04:00
t . logger . Warnf ( "i18n|MISSING_TRANSLATION|%s|%s" , currentLangStr , translationID )
2016-08-09 05:41:56 -04:00
}
2019-06-02 05:11:46 -04:00
2016-11-06 18:10:32 -05:00
if enableMissingTranslationPlaceholders {
2016-11-18 16:38:41 -05:00
return "[i18n] " + translationID
2016-09-18 16:18:13 -04:00
}
2019-06-02 05:11:46 -04:00
return translated
2016-08-09 05:41:56 -04:00
}
2016-05-14 00:35:16 -04:00
}
}
2021-04-22 03:57:24 -04:00
// intCount wraps the Count method.
type intCount int
func ( c intCount ) Count ( ) int {
return int ( c )
}
const countFieldName = "Count"
2021-04-24 06:26:51 -04:00
// getPluralCount gets the plural count as a string (floats) or an integer.
2021-05-02 08:06:16 -04:00
// If v is nil, nil is returned.
2022-03-17 17:03:27 -04:00
func getPluralCount ( v any ) any {
2021-04-24 06:26:51 -04:00
if v == nil {
2021-05-02 08:06:16 -04:00
// i18n called without any argument, make sure it does not
// get any plural count.
return nil
2021-04-22 03:57:24 -04:00
}
2021-04-24 06:26:51 -04:00
switch v := v . ( type ) {
2022-03-17 17:03:27 -04:00
case map [ string ] any :
2021-04-22 03:57:24 -04:00
for k , vv := range v {
if strings . EqualFold ( k , countFieldName ) {
2021-04-24 06:26:51 -04:00
return toPluralCountValue ( vv )
2021-04-22 03:57:24 -04:00
}
}
default :
vv := reflect . Indirect ( reflect . ValueOf ( v ) )
if vv . Kind ( ) == reflect . Interface && ! vv . IsNil ( ) {
vv = vv . Elem ( )
}
tp := vv . Type ( )
if tp . Kind ( ) == reflect . Struct {
f := vv . FieldByName ( countFieldName )
if f . IsValid ( ) {
2021-04-24 06:26:51 -04:00
return toPluralCountValue ( f . Interface ( ) )
2021-04-22 03:57:24 -04:00
}
2022-03-08 04:06:12 -05:00
m := hreflect . GetMethodByName ( vv , countFieldName )
2021-04-22 03:57:24 -04:00
if m . IsValid ( ) && m . Type ( ) . NumIn ( ) == 0 && m . Type ( ) . NumOut ( ) == 1 {
c := m . Call ( nil )
2021-04-24 06:26:51 -04:00
return toPluralCountValue ( c [ 0 ] . Interface ( ) )
2021-04-22 03:57:24 -04:00
}
}
}
2021-04-24 06:26:51 -04:00
return toPluralCountValue ( v )
}
// go-i18n expects floats to be represented by string.
2022-03-17 17:03:27 -04:00
func toPluralCountValue ( in any ) any {
2021-04-24 06:26:51 -04:00
k := reflect . TypeOf ( in ) . Kind ( )
switch {
case hreflect . IsFloat ( k ) :
f := cast . ToString ( in )
if ! strings . Contains ( f , "." ) {
f += ".0"
}
return f
case k == reflect . String :
if _ , err := cast . ToFloat64E ( in ) ; err == nil {
return in
}
// A non-numeric value.
2021-05-02 08:06:16 -04:00
return nil
2021-04-24 06:26:51 -04:00
default :
if i , err := cast . ToIntE ( in ) ; err == nil {
return i
}
2021-05-02 08:06:16 -04:00
return nil
2021-04-24 06:26:51 -04:00
}
2021-04-22 03:57:24 -04:00
}