Skip to content

Commit 13a5e59

Browse files
committed
Add lang.HasTranslation
Fixes #10538
1 parent 2a81a49 commit 13a5e59

File tree

7 files changed

+148
-42
lines changed

7 files changed

+148
-42
lines changed

deps/deps.go

+15-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
// Copyright 2022 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+
114
package deps
215

316
import (
@@ -69,8 +82,8 @@ type Deps struct {
6982
// The file cache to use.
7083
FileCaches filecache.Caches
7184

72-
// The translation func to use
73-
Translate func(translationID string, templateData any) string `json:"-"`
85+
// The translator interface to use
86+
Translator langs.Translator `json:"-"`
7487

7588
// The language in use. TODO(bep) consolidate with site
7689
Language *langs.Language

langs/i18n/i18n.go

+68-27
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2017 The Hugo Authors. All rights reserved.
1+
// Copyright 2022 The Hugo Authors. All rights reserved.
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -24,54 +24,76 @@ import (
2424
"github.com/gohugoio/hugo/common/loggers"
2525
"github.com/gohugoio/hugo/config"
2626
"github.com/gohugoio/hugo/helpers"
27+
"github.com/gohugoio/hugo/langs"
2728

2829
"github.com/gohugoio/go-i18n/v2/i18n"
2930
)
3031

32+
type translator struct {
33+
translate func(translationID string, templateData any) string
34+
hasTranslation func(translationID string) bool
35+
}
36+
37+
var nopTranslator = translator{}
38+
39+
func (t translator) Translate(translationID string, templateData any) string {
40+
if t.translate == nil {
41+
return ""
42+
}
43+
return t.translate(translationID, templateData)
44+
}
45+
46+
func (t translator) HasTranslation(translationID string) bool {
47+
if t.hasTranslation == nil {
48+
return false
49+
}
50+
return t.hasTranslation(translationID)
51+
}
52+
3153
type translateFunc func(translationID string, templateData any) string
3254

3355
var i18nWarningLogger = helpers.NewDistinctErrorLogger()
3456

35-
// Translator handles i18n translations.
36-
type Translator struct {
37-
translateFuncs map[string]translateFunc
38-
cfg config.Provider
39-
logger loggers.Logger
57+
// Translators handles i18n translations.
58+
type Translators struct {
59+
translators map[string]langs.Translator
60+
cfg config.Provider
61+
logger loggers.Logger
4062
}
4163

4264
// NewTranslator creates a new Translator for the given language bundle and configuration.
43-
func NewTranslator(b *i18n.Bundle, cfg config.Provider, logger loggers.Logger) Translator {
44-
t := Translator{cfg: cfg, logger: logger, translateFuncs: make(map[string]translateFunc)}
65+
func NewTranslator(b *i18n.Bundle, cfg config.Provider, logger loggers.Logger) Translators {
66+
t := Translators{cfg: cfg, logger: logger, translators: make(map[string]langs.Translator)}
4567
t.initFuncs(b)
4668
return t
4769
}
4870

49-
// Func gets the translate func for the given language, or for the default
71+
// Get gets the Translator for the given language, or for the default
5072
// configured language if not found.
51-
func (t Translator) Func(lang string) translateFunc {
52-
if f, ok := t.translateFuncs[lang]; ok {
53-
return f
73+
func (ts Translators) Get(lang string) langs.Translator {
74+
if t, ok := ts.translators[lang]; ok {
75+
return t
5476
}
55-
t.logger.Infof("Translation func for language %v not found, use default.", lang)
56-
if f, ok := t.translateFuncs[t.cfg.GetString("defaultContentLanguage")]; ok {
57-
return f
77+
ts.logger.Infof("Translation func for language %v not found, use default.", lang)
78+
if tt, ok := ts.translators[ts.cfg.GetString("defaultContentLanguage")]; ok {
79+
return tt
5880
}
5981

60-
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.")
61-
return func(translationID string, args any) string {
62-
return ""
63-
}
82+
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.")
83+
84+
return nopTranslator
6485
}
6586

66-
func (t Translator) initFuncs(bndl *i18n.Bundle) {
67-
enableMissingTranslationPlaceholders := t.cfg.GetBool("enableMissingTranslationPlaceholders")
87+
func (ts Translators) initFuncs(bndl *i18n.Bundle) {
88+
enableMissingTranslationPlaceholders := ts.cfg.GetBool("enableMissingTranslationPlaceholders")
6889
for _, lang := range bndl.LanguageTags() {
6990
currentLang := lang
7091
currentLangStr := currentLang.String()
7192
// This may be pt-BR; make it case insensitive.
7293
currentLangKey := strings.ToLower(strings.TrimPrefix(currentLangStr, artificialLangTagPrefix))
7394
localizer := i18n.NewLocalizer(bndl, currentLangStr)
74-
t.translateFuncs[currentLangKey] = func(translationID string, templateData any) string {
95+
96+
translate := func(translationID string, templateData any) (string, error) {
7597
pluralCount := getPluralCount(templateData)
7698

7799
if templateData != nil {
@@ -90,10 +112,12 @@ func (t Translator) initFuncs(bndl *i18n.Bundle) {
90112
PluralCount: pluralCount,
91113
})
92114

115+
fmt.Println("translatedLang", translatedLang, "currentLang", currentLang, "err", err, "translated", translated)
116+
93117
sameLang := currentLang == translatedLang
94118

95119
if err == nil && sameLang {
96-
return translated
120+
return translated, nil
97121
}
98122

99123
if err != nil && sameLang && translated != "" {
@@ -102,24 +126,41 @@ func (t Translator) initFuncs(bndl *i18n.Bundle) {
102126
// but currently we get an error even if the fallback to
103127
// "other" succeeds.
104128
if fmt.Sprintf("%T", err) == "i18n.pluralFormNotFoundError" {
105-
return translated
129+
return translated, nil
106130
}
107131
}
108132

133+
return translated, err
134+
135+
}
136+
137+
translateAndLogIfNeeded := func(translationID string, templateData any) string {
138+
translated, err := translate(translationID, templateData)
139+
if err == nil {
140+
return translated
141+
}
142+
109143
if _, ok := err.(*i18n.MessageNotFoundErr); !ok {
110-
t.logger.Warnf("Failed to get translated string for language %q and ID %q: %s", currentLangStr, translationID, err)
144+
ts.logger.Warnf("Failed to get translated string for language %q and ID %q: %s", currentLangStr, translationID, err)
111145
}
112146

113-
if t.cfg.GetBool("logI18nWarnings") {
147+
if ts.cfg.GetBool("logI18nWarnings") {
114148
i18nWarningLogger.Printf("i18n|MISSING_TRANSLATION|%s|%s", currentLangStr, translationID)
115149
}
116150

117151
if enableMissingTranslationPlaceholders {
118152
return "[i18n] " + translationID
119153
}
120-
121154
return translated
122155
}
156+
157+
ts.translators[currentLangKey] = translator{
158+
translate: translateAndLogIfNeeded,
159+
hasTranslation: func(translationID string) bool {
160+
_, err := translate(translationID, nil)
161+
return err == nil
162+
},
163+
}
123164
}
124165
}
125166

langs/i18n/i18n_test.go

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2017 The Hugo Authors. All rights reserved.
1+
// Copyright 2022 The Hugo Authors. All rights reserved.
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -407,10 +407,10 @@ other = "{{ . }} miesiąca"
407407
c.Assert(err, qt.IsNil)
408408
c.Assert(d.LoadResources(), qt.IsNil)
409409

410-
f := tp.t.Func(test.lang)
410+
f := tp.t.Get(test.lang)
411411

412412
for _, variant := range test.variants {
413-
c.Assert(f(test.id, variant.Key), qt.Equals, variant.Value, qt.Commentf("input: %v", variant.Key))
413+
c.Assert(f.Translate(test.id, variant.Key), qt.Equals, variant.Value, qt.Commentf("input: %v", variant.Key))
414414
c.Assert(int(depsCfg.Logger.LogCounters().WarnCounter.Count()), qt.Equals, 0)
415415
}
416416

@@ -421,8 +421,8 @@ other = "{{ . }} miesiąca"
421421

422422
func doTestI18nTranslate(t testing.TB, test i18nTest, cfg config.Provider) string {
423423
tp := prepareTranslationProvider(t, test, cfg)
424-
f := tp.t.Func(test.lang)
425-
return f(test.id, test.args)
424+
f := tp.t.Get(test.lang)
425+
return f.Translate(test.id, test.args)
426426
}
427427

428428
type countField struct {
@@ -541,8 +541,8 @@ func BenchmarkI18nTranslate(b *testing.B) {
541541
tp := prepareTranslationProvider(b, test, v)
542542
b.ResetTimer()
543543
for i := 0; i < b.N; i++ {
544-
f := tp.t.Func(test.lang)
545-
actual := f(test.id, test.args)
544+
f := tp.t.Get(test.lang)
545+
actual := f.Translate(test.id, test.args)
546546
if actual != test.expected {
547547
b.Fatalf("expected %v got %v", test.expected, actual)
548548
}

langs/i18n/integration_test.go

+38
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,41 @@ l1: {{ i18n "l1" }}|l2: {{ i18n "l2" }}|l3: {{ i18n "l3" }}
5555
l1: l1main|l2: l2main|l3: l3theme
5656
`)
5757
}
58+
59+
func TestHasLanguage(t *testing.T) {
60+
t.Parallel()
61+
62+
files := `
63+
-- config.toml --
64+
baseURL = "https://example.org"
65+
defaultContentLanguage = "en"
66+
defaultContentLanguageInSubDir = true
67+
[languages]
68+
[languages.en]
69+
weight=10
70+
[languages.nn]
71+
weight=20
72+
-- i18n/en.toml --
73+
key1.other = "en key1"
74+
key2.other = "en key2"
75+
76+
-- i18n/nn.toml --
77+
key1.other = "nn key1"
78+
key3.other = "nn key2"
79+
-- layouts/index.html --
80+
key1: {{ lang.HasTranslation "key1" }}|
81+
key2: {{ lang.HasTranslation "key2" }}|
82+
key3: {{ lang.HasTranslation "key3" }}|
83+
84+
`
85+
86+
b := hugolib.NewIntegrationTestBuilder(
87+
hugolib.IntegrationTestConfig{
88+
T: t,
89+
TxtarString: files,
90+
},
91+
).Build()
92+
93+
b.AssertFileContent("public/en/index.html", "key1: true|\nkey2: true|\nkey3: false|")
94+
b.AssertFileContent("public/nn/index.html", "key1: true|\nkey2: false|\nkey3: true|")
95+
}

langs/i18n/translationProvider.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2017 The Hugo Authors. All rights reserved.
1+
// Copyright 2022 The Hugo Authors. All rights reserved.
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@ import (
1919
"strings"
2020

2121
"github.com/gohugoio/hugo/common/paths"
22+
"github.com/gohugoio/hugo/deps"
2223

2324
"github.com/gohugoio/hugo/common/herrors"
2425
"golang.org/x/text/language"
@@ -28,15 +29,14 @@ import (
2829
"github.com/gohugoio/hugo/helpers"
2930
toml "github.com/pelletier/go-toml/v2"
3031

31-
"github.com/gohugoio/hugo/deps"
3232
"github.com/gohugoio/hugo/hugofs"
3333
"github.com/gohugoio/hugo/source"
3434
)
3535

3636
// TranslationProvider provides translation handling, i.e. loading
3737
// of bundles etc.
3838
type TranslationProvider struct {
39-
t Translator
39+
t Translators
4040
}
4141

4242
// NewTranslationProvider creates a new translation provider.
@@ -73,7 +73,7 @@ func (tp *TranslationProvider) Update(d *deps.Deps) error {
7373

7474
tp.t = NewTranslator(bundle, d.Cfg, d.Log)
7575

76-
d.Translate = tp.t.Func(d.Language.Lang)
76+
d.Translator = tp.t.Get(d.Language.Lang)
7777

7878
return nil
7979
}
@@ -119,7 +119,7 @@ func addTranslationFile(bundle *i18n.Bundle, r source.File) error {
119119

120120
// Clone sets the language func for the new language.
121121
func (tp *TranslationProvider) Clone(d *deps.Deps) error {
122-
d.Translate = tp.t.Func(d.Language.Lang)
122+
d.Translator = tp.t.Get(d.Language.Lang)
123123

124124
return nil
125125
}

langs/language.go

+5
Original file line numberDiff line numberDiff line change
@@ -329,3 +329,8 @@ type Collator struct {
329329
func (c *Collator) CompareStrings(a, b string) int {
330330
return c.c.CompareString(a, b)
331331
}
332+
333+
type Translator interface {
334+
Translate(translationID string, templateData any) string
335+
HasTranslation(translationID string) bool
336+
}

tpl/lang/lang.go

+10-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,16 @@ func (ns *Namespace) Translate(id any, args ...any) (string, error) {
6060
return "", nil
6161
}
6262

63-
return ns.deps.Translate(sid, templateData), nil
63+
return ns.deps.Translator.Translate(sid, templateData), nil
64+
}
65+
66+
// HasTranslation returns true if the translation key is translated in the current language.
67+
func (ns *Namespace) HasTranslation(key any) bool {
68+
keys, err := cast.ToStringE(key)
69+
if err != nil {
70+
return false
71+
}
72+
return ns.deps.Translator.HasTranslation(keys)
6473
}
6574

6675
// FormatNumber formats number with the given precision for the current language.

0 commit comments

Comments
 (0)