From 1b80e61e4ce74509ff4ed4022a847ee3cb71c0cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Wed, 14 Dec 2022 18:23:05 +0100 Subject: [PATCH] Add lang.HasTranslation Fixes #10538 --- deps/deps.go | 4 +- langs/i18n/i18n.go | 92 ++++++++++++++++++++++--------- langs/i18n/i18n_test.go | 12 ++-- langs/i18n/integration_test.go | 38 +++++++++++++ langs/i18n/translationProvider.go | 8 +-- langs/language.go | 5 ++ tpl/lang/lang.go | 11 +++- 7 files changed, 131 insertions(+), 39 deletions(-) diff --git a/deps/deps.go b/deps/deps.go index 02730e825a0..c2e636fb7bf 100644 --- a/deps/deps.go +++ b/deps/deps.go @@ -69,8 +69,8 @@ type Deps struct { // The file cache to use. FileCaches filecache.Caches - // The translation func to use - Translate func(translationID string, templateData any) string `json:"-"` + // The translator interface to use + Translator langs.Translator `json:"-"` // The language in use. TODO(bep) consolidate with site Language *langs.Language diff --git a/langs/i18n/i18n.go b/langs/i18n/i18n.go index 5594c84cb5a..06aef34483c 100644 --- a/langs/i18n/i18n.go +++ b/langs/i18n/i18n.go @@ -24,54 +24,76 @@ import ( "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/langs" "github.com/gohugoio/go-i18n/v2/i18n" ) +type translator struct { + translate func(translationID string, templateData any) string + hasTranslation func(translationID string) bool +} + +var nopTranslator = translator{} + +func (t translator) Translate(translationID string, templateData any) string { + if t.translate == nil { + return "" + } + return t.translate(translationID, templateData) +} + +func (t translator) HasTranslation(translationID string) bool { + if t.hasTranslation == nil { + return false + } + return t.hasTranslation(translationID) +} + type translateFunc func(translationID string, templateData any) string var i18nWarningLogger = helpers.NewDistinctErrorLogger() -// Translator handles i18n translations. -type Translator struct { - translateFuncs map[string]translateFunc - cfg config.Provider - logger loggers.Logger +// Translators handles i18n translations. +type Translators struct { + translators map[string]langs.Translator + cfg config.Provider + logger loggers.Logger } // NewTranslator creates a new Translator for the given language bundle and configuration. -func NewTranslator(b *i18n.Bundle, cfg config.Provider, logger loggers.Logger) Translator { - t := Translator{cfg: cfg, logger: logger, translateFuncs: make(map[string]translateFunc)} +func NewTranslator(b *i18n.Bundle, cfg config.Provider, logger loggers.Logger) Translators { + t := Translators{cfg: cfg, logger: logger, translators: make(map[string]langs.Translator)} t.initFuncs(b) return t } -// Func gets the translate func for the given language, or for the default +// Get gets the Translator for the given language, or for the default // configured language if not found. -func (t Translator) Func(lang string) translateFunc { - if f, ok := t.translateFuncs[lang]; ok { - return f +func (ts Translators) Get(lang string) langs.Translator { + if t, ok := ts.translators[lang]; ok { + return t } - t.logger.Infof("Translation func for language %v not found, use default.", lang) - if f, ok := t.translateFuncs[t.cfg.GetString("defaultContentLanguage")]; ok { - return f + ts.logger.Infof("Translation func for language %v not found, use default.", lang) + if tt, ok := ts.translators[ts.cfg.GetString("defaultContentLanguage")]; ok { + return tt } - 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.") - return func(translationID string, args any) string { - return "" - } + ts.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.") + + return nopTranslator } -func (t Translator) initFuncs(bndl *i18n.Bundle) { - enableMissingTranslationPlaceholders := t.cfg.GetBool("enableMissingTranslationPlaceholders") +func (ts Translators) initFuncs(bndl *i18n.Bundle) { + enableMissingTranslationPlaceholders := ts.cfg.GetBool("enableMissingTranslationPlaceholders") for _, lang := range bndl.LanguageTags() { currentLang := lang currentLangStr := currentLang.String() // This may be pt-BR; make it case insensitive. currentLangKey := strings.ToLower(strings.TrimPrefix(currentLangStr, artificialLangTagPrefix)) localizer := i18n.NewLocalizer(bndl, currentLangStr) - t.translateFuncs[currentLangKey] = func(translationID string, templateData any) string { + + translate := func(translationID string, templateData any) (string, error) { pluralCount := getPluralCount(templateData) if templateData != nil { @@ -93,7 +115,7 @@ func (t Translator) initFuncs(bndl *i18n.Bundle) { sameLang := currentLang == translatedLang if err == nil && sameLang { - return translated + return translated, nil } if err != nil && sameLang && translated != "" { @@ -102,23 +124,41 @@ func (t Translator) initFuncs(bndl *i18n.Bundle) { // but currently we get an error even if the fallback to // "other" succeeds. if fmt.Sprintf("%T", err) == "i18n.pluralFormNotFoundError" { - return translated + return translated, nil } } + // Not found + return "", err + + } + + translateAndLogIfNeeded := func(translationID string, templateData any) string { + translated, err := translate(translationID, templateData) + if err == nil && translated != "" { + return translated + } + if _, ok := err.(*i18n.MessageNotFoundErr); !ok { - t.logger.Warnf("Failed to get translated string for language %q and ID %q: %s", currentLangStr, translationID, err) + ts.logger.Warnf("Failed to get translated string for language %q and ID %q: %s", currentLangStr, translationID, err) } - if t.cfg.GetBool("logI18nWarnings") { + if ts.cfg.GetBool("logI18nWarnings") { i18nWarningLogger.Printf("i18n|MISSING_TRANSLATION|%s|%s", currentLangStr, translationID) } if enableMissingTranslationPlaceholders { return "[i18n] " + translationID } + return "" + } - return translated + ts.translators[currentLangKey] = translator{ + translate: translateAndLogIfNeeded, + hasTranslation: func(translationID string) bool { + s, _ := translate(translationID, nil) + return s != "" + }, } } } diff --git a/langs/i18n/i18n_test.go b/langs/i18n/i18n_test.go index 0048d4b1b72..f123c76e59b 100644 --- a/langs/i18n/i18n_test.go +++ b/langs/i18n/i18n_test.go @@ -407,10 +407,10 @@ other = "{{ . }} miesiąca" c.Assert(err, qt.IsNil) c.Assert(d.LoadResources(), qt.IsNil) - f := tp.t.Func(test.lang) + f := tp.t.Get(test.lang) for _, variant := range test.variants { - c.Assert(f(test.id, variant.Key), qt.Equals, variant.Value, qt.Commentf("input: %v", variant.Key)) + c.Assert(f.Translate(test.id, variant.Key), qt.Equals, variant.Value, qt.Commentf("input: %v", variant.Key)) c.Assert(int(depsCfg.Logger.LogCounters().WarnCounter.Count()), qt.Equals, 0) } @@ -421,8 +421,8 @@ other = "{{ . }} miesiąca" func doTestI18nTranslate(t testing.TB, test i18nTest, cfg config.Provider) string { tp := prepareTranslationProvider(t, test, cfg) - f := tp.t.Func(test.lang) - return f(test.id, test.args) + f := tp.t.Get(test.lang) + return f.Translate(test.id, test.args) } type countField struct { @@ -541,8 +541,8 @@ func BenchmarkI18nTranslate(b *testing.B) { tp := prepareTranslationProvider(b, test, v) b.ResetTimer() for i := 0; i < b.N; i++ { - f := tp.t.Func(test.lang) - actual := f(test.id, test.args) + f := tp.t.Get(test.lang) + actual := f.Translate(test.id, test.args) if actual != test.expected { b.Fatalf("expected %v got %v", test.expected, actual) } diff --git a/langs/i18n/integration_test.go b/langs/i18n/integration_test.go index 5599859ee69..36f7bbe66a0 100644 --- a/langs/i18n/integration_test.go +++ b/langs/i18n/integration_test.go @@ -55,3 +55,41 @@ l1: {{ i18n "l1" }}|l2: {{ i18n "l2" }}|l3: {{ i18n "l3" }} l1: l1main|l2: l2main|l3: l3theme `) } + +func TestHasLanguage(t *testing.T) { + t.Parallel() + + files := ` +-- config.toml -- +baseURL = "https://example.org" +defaultContentLanguage = "en" +defaultContentLanguageInSubDir = true +[languages] +[languages.en] +weight=10 +[languages.nn] +weight=20 +-- i18n/en.toml -- +key1.other = "en key1" +key2.other = "en key2" + +-- i18n/nn.toml -- +key1.other = "nn key1" +key3.other = "nn key2" +-- layouts/index.html -- +key1: {{ lang.HasTranslation "key1" }}| +key2: {{ lang.HasTranslation "key2" }}| +key3: {{ lang.HasTranslation "key3" }}| + + ` + + b := hugolib.NewIntegrationTestBuilder( + hugolib.IntegrationTestConfig{ + T: t, + TxtarString: files, + }, + ).Build() + + b.AssertFileContent("public/en/index.html", "key1: true|\nkey2: true|\nkey3: false|") + b.AssertFileContent("public/nn/index.html", "key1: true|\nkey2: false|\nkey3: true|") +} diff --git a/langs/i18n/translationProvider.go b/langs/i18n/translationProvider.go index d9d334567f9..abf3b6d2110 100644 --- a/langs/i18n/translationProvider.go +++ b/langs/i18n/translationProvider.go @@ -19,6 +19,7 @@ import ( "strings" "github.com/gohugoio/hugo/common/paths" + "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/common/herrors" "golang.org/x/text/language" @@ -28,7 +29,6 @@ import ( "github.com/gohugoio/hugo/helpers" toml "github.com/pelletier/go-toml/v2" - "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/source" ) @@ -36,7 +36,7 @@ import ( // TranslationProvider provides translation handling, i.e. loading // of bundles etc. type TranslationProvider struct { - t Translator + t Translators } // NewTranslationProvider creates a new translation provider. @@ -73,7 +73,7 @@ func (tp *TranslationProvider) Update(d *deps.Deps) error { tp.t = NewTranslator(bundle, d.Cfg, d.Log) - d.Translate = tp.t.Func(d.Language.Lang) + d.Translator = tp.t.Get(d.Language.Lang) return nil } @@ -119,7 +119,7 @@ func addTranslationFile(bundle *i18n.Bundle, r source.File) error { // Clone sets the language func for the new language. func (tp *TranslationProvider) Clone(d *deps.Deps) error { - d.Translate = tp.t.Func(d.Language.Lang) + d.Translator = tp.t.Get(d.Language.Lang) return nil } diff --git a/langs/language.go b/langs/language.go index d6b30ec1005..0a87b8b5054 100644 --- a/langs/language.go +++ b/langs/language.go @@ -329,3 +329,8 @@ type Collator struct { func (c *Collator) CompareStrings(a, b string) int { return c.c.CompareString(a, b) } + +type Translator interface { + Translate(translationID string, templateData any) string + HasTranslation(translationID string) bool +} diff --git a/tpl/lang/lang.go b/tpl/lang/lang.go index 17d37faa495..170fd24668e 100644 --- a/tpl/lang/lang.go +++ b/tpl/lang/lang.go @@ -60,7 +60,16 @@ func (ns *Namespace) Translate(id any, args ...any) (string, error) { return "", nil } - return ns.deps.Translate(sid, templateData), nil + return ns.deps.Translator.Translate(sid, templateData), nil +} + +// HasTranslation returns true if the translation key is translated in the current language. +func (ns *Namespace) HasTranslation(key any) bool { + keys, err := cast.ToStringE(key) + if err != nil { + return false + } + return ns.deps.Translator.HasTranslation(keys) } // FormatNumber formats number with the given precision for the current language.