From 633cc772e0e8a79205b1b268bd9bfc2b0f5b1473 Mon Sep 17 00:00:00 2001 From: Joe Mooring Date: Thu, 23 Apr 2026 10:41:27 -0700 Subject: [PATCH] langs/i18n: Improve default content language fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fallback order for translations is now: 1. Current language's locale (e.g., pt-BR → pt-br.toml) 2. Current language's key (e.g., pt → pt.toml) 3. Default language's locale (e.g., es-AR → es-ar.toml) ← new 4. Default language's key (e.g., es → es.toml) Closes #14243 Co-authored-by: Claude Sonnet 4.6 --- hugolib/menu_test.go | 2 +- langs/i18n/translationProvider.go | 31 ++++++++++++----- langs/languages_integration_test.go | 52 +++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 10 deletions(-) diff --git a/hugolib/menu_test.go b/hugolib/menu_test.go index 20d862caa..beb1faf6f 100644 --- a/hugolib/menu_test.go +++ b/hugolib/menu_test.go @@ -567,7 +567,7 @@ menu: main b.AssertFileContent("public/en/index.html", `p1p2`) b.AssertFileContent("public/fr/index.html", `p1`) - b.AssertLogContains("! WARN") + b.AssertLogContains("! duplicate menu entry") } func TestSectionPagesIssue12399(t *testing.T) { diff --git a/langs/i18n/translationProvider.go b/langs/i18n/translationProvider.go index 11aeaa781..5fcaeb3fd 100644 --- a/langs/i18n/translationProvider.go +++ b/langs/i18n/translationProvider.go @@ -21,6 +21,7 @@ import ( "strings" "github.com/gohugoio/hugo/common/paths" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/langs" "github.com/gohugoio/hugo/parser/metadecoders" @@ -150,17 +151,29 @@ func (tp *TranslationProvider) CloneResource(dst, src *deps.Deps) error { return nil } -// getTranslateFunc returns the translation function for the language in Deps. -// We first try the locale (e.g. "en-US"), then the language key (e.g. "en"). -func (tp *TranslationProvider) getTranslateFunc(dst *deps.Deps) func(ctx context.Context, translationID string, templateData any) string { - l := dst.Conf.Language().(*langs.Language) - if locale := l.Locale(); locale != "" { - if fn, ok := tp.t.Lookup(strings.ToLower(locale)); ok { - return fn +func defaultLanguage(conf config.AllProvider) *langs.Language { + key := conf.DefaultContentLanguage() + for _, l := range conf.Languages().(langs.Languages) { + if l.Lang == key { + return l } } - // Func will fall back to the default language if not found. - return tp.t.Func(l.Lang) + return conf.Language().(*langs.Language) +} + +// getTranslateFunc returns the translation function for the language in Deps. +// The lookup order is: current locale, current key, default locale, default key. +func (tp *TranslationProvider) getTranslateFunc(dst *deps.Deps) func(ctx context.Context, translationID string, templateData any) string { + current := dst.Conf.Language().(*langs.Language) + defaultLang := defaultLanguage(dst.Conf) + for _, l := range []*langs.Language{current, defaultLang} { + for _, key := range []string{strings.ToLower(l.Locale()), l.Lang} { + if fn, ok := tp.t.Lookup(key); ok { + return fn + } + } + } + return tp.t.Func("en") } func errWithFileContext(inerr error, r *source.File) error { diff --git a/langs/languages_integration_test.go b/langs/languages_integration_test.go index fbf04d874..3bb4bf171 100644 --- a/langs/languages_integration_test.go +++ b/langs/languages_integration_test.go @@ -326,3 +326,55 @@ language: {{ site.Language.Name }} file: {{ T "file" }}| b.AssertFileContent("public/de/index.html", "language: de file: de|") b.AssertFileContent("public/de-de/index.html", "language: de-de file: de-de|") } + +func TestDefaultContentLanguageFallback14243(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +disableKinds = ['page','rss','section','sitemap','taxonomy','term'] +defaultContentLanguage = 'es' +defaultContentLanguageInSubdir = true + +[languages.es] +locale = 'es-AR' +weight = 1 + +[languages.pt] +locale = 'pt-BR' +weight = 2 +-- layouts/home.html -- +{{ T "foo"}}| +-- i18n/es-ar.toml -- +foo = 'foo es-ar' +-- i18n/es.toml -- +foo = 'foo es' +-- i18n/pt-br.toml -- +foo = 'foo pt-br' +-- i18n/pt.toml -- +foo = 'foo pt' +` + + b := hugolib.Test(t, files) + b.AssertFileContent("public/pt/index.html", "foo pt-br|") + + files = strings.ReplaceAll(files, "i18n/pt-br.toml", "unused-a.toml") + + b = hugolib.Test(t, files) + b.AssertFileContent("public/pt/index.html", "foo pt|") + + files = strings.ReplaceAll(files, "i18n/pt.toml", "unused-b.toml") + + b = hugolib.Test(t, files) + b.AssertFileContent("public/pt/index.html", "foo es-ar|") + + files = strings.ReplaceAll(files, "i18n/es-ar.toml", "unused-c.toml") + + b = hugolib.Test(t, files) + b.AssertFileContent("public/pt/index.html", "foo es|") + + files = strings.ReplaceAll(files, "i18n/es.toml", "unused-d.toml") + + b = hugolib.Test(t, files) + b.AssertFileContent("public/pt/index.html", "|") +}