Skip to content

Commit

Permalink
Add lang.HasTranslation
Browse files Browse the repository at this point in the history
Fixes #10538
  • Loading branch information
bep committed Dec 14, 2022
1 parent 2a81a49 commit 13a5e59
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 42 deletions.
17 changes: 15 additions & 2 deletions deps/deps.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
// Copyright 2022 The Hugo Authors. All rights reserved.
//
// 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.

package deps

import (
Expand Down Expand Up @@ -69,8 +82,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
Expand Down
95 changes: 68 additions & 27 deletions langs/i18n/i18n.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2017 The Hugo Authors. All rights reserved.
// Copyright 2022 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -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 {
Expand All @@ -90,10 +112,12 @@ func (t Translator) initFuncs(bndl *i18n.Bundle) {
PluralCount: pluralCount,
})

fmt.Println("translatedLang", translatedLang, "currentLang", currentLang, "err", err, "translated", translated)

sameLang := currentLang == translatedLang

if err == nil && sameLang {
return translated
return translated, nil
}

if err != nil && sameLang && translated != "" {
Expand All @@ -102,24 +126,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
}
}

return translated, err

}

translateAndLogIfNeeded := func(translationID string, templateData any) string {
translated, err := translate(translationID, templateData)
if err == nil {
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 translated
}

ts.translators[currentLangKey] = translator{
translate: translateAndLogIfNeeded,
hasTranslation: func(translationID string) bool {
_, err := translate(translationID, nil)
return err == nil
},
}
}
}

Expand Down
14 changes: 7 additions & 7 deletions langs/i18n/i18n_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2017 The Hugo Authors. All rights reserved.
// Copyright 2022 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -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)
}

Expand All @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down
38 changes: 38 additions & 0 deletions langs/i18n/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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|")
}
10 changes: 5 additions & 5 deletions langs/i18n/translationProvider.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2017 The Hugo Authors. All rights reserved.
// Copyright 2022 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -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"
Expand All @@ -28,15 +29,14 @@ 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"
)

// TranslationProvider provides translation handling, i.e. loading
// of bundles etc.
type TranslationProvider struct {
t Translator
t Translators
}

// NewTranslationProvider creates a new translation provider.
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down
5 changes: 5 additions & 0 deletions langs/language.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
11 changes: 10 additions & 1 deletion tpl/lang/lang.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 13a5e59

Please sign in to comment.