Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add lang.HasTranslation #10539

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
93 changes: 66 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 @@ -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 != "" {
Expand All @@ -102,24 +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
}
}

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