in := map[string]any{
"baseURL": baseURL,
"title": title,
- "languageCode": "en-us",
+ "locale": "en-us",
"disablePathToLower": true,
}
return err
}
+ // Legacy language values.
+ if c.LanguageCode != "" {
+ if !c.isLanguageClone {
+ hugo.DeprecateWithLogger("project config key languageCode", "Use locale instead.", "v0.158.0", logger.Logger())
+ }
+ if c.Locale == "" {
+ c.Locale = c.LanguageCode
+ }
+ c.LanguageCode = ""
+ }
+ for k, v := range c.Languages.Config.LanguageConfigs {
+ //lint:ignore SA1019 Keep as adapter for now.
+ if v.LanguageCode != "" {
+ if !c.isLanguageClone {
+ hugo.DeprecateWithLogger(fmt.Sprintf("project config key languages.%s.languageCode", k), fmt.Sprintf("Use languages.%s.locale instead.", k), "v0.158.0", logger.Logger())
+ }
+ if v.Locale == "" {
+ //lint:ignore SA1019 Keep as adapter for now.
+ v.Locale = v.LanguageCode
+ }
+ //lint:ignore SA1019 Keep as adapter for now.
+ v.LanguageCode = ""
+ }
+ //lint:ignore SA1019 Keep as adapter for now.
+ if v.LanguageName != "" {
+ if !c.isLanguageClone {
+ hugo.DeprecateWithLogger(fmt.Sprintf("project config key languages.%s.languageName", k), fmt.Sprintf("Use languages.%s.label instead.", k), "v0.158.0", logger.Logger())
+ }
+ if v.Label == "" {
+ //lint:ignore SA1019 Keep as adapter for now.
+ v.Label = v.LanguageName
+ }
+ //lint:ignore SA1019 Keep as adapter for now.
+ v.LanguageName = ""
+ }
+ //lint:ignore SA1019 Keep as adapter for now.
+ if v.LanguageDirection != "" {
+ if !c.isLanguageClone {
+ hugo.DeprecateWithLogger(fmt.Sprintf("project config key languages.%s.languageDirection", k), fmt.Sprintf("Use languages.%s.direction instead.", k), "v0.158.0", logger.Logger())
+ }
+ if v.Direction == "" {
+ //lint:ignore SA1019 Keep as adapter for now.
+ v.Direction = v.LanguageDirection
+ }
+ //lint:ignore SA1019 Keep as adapter for now.
+ v.LanguageDirection = ""
+ }
+
+ c.Languages.Config.LanguageConfigs[k] = v
+ // Sorted is a snapshot of LanguageConfigs taken at decode time; keep it
+ // in sync so Configs.Init, which reads from Sorted, sees the migrated values.
+ for i, s := range c.Languages.Config.Sorted {
+ if s.Name == k {
+ c.Languages.Config.Sorted[i].LanguageConfig = v
+ break
+ }
+ }
+ }
+
// Legacy privacy values.
if c.Privacy.Twitter.Disable {
hugo.DeprecateWithLogger("project config key privacy.twitter.disable", "Use privacy.x.disable instead.", "v0.141.0", logger.Logger())
// The configured environment. Default is "development" for server and "production" for build.
Environment string
- // The default language code.
- LanguageCode string
+ // Deprecated: Use Locale instead.
+ LanguageCode string `json:"-"`
+
+ // The default locale.
+ Locale string
// Enable if the site content has CJK language (Chinese, Japanese, or Korean). This affects how Hugo counts words.
HasCJKLanguage bool
if !found {
return fmt.Errorf("invalid language configuration for %q", f.Name)
}
- language, err := langs.NewLanguage(f.Name, c.Base.DefaultContentLanguage, v.TimeZone, f.LanguageConfig)
+ language, err := langs.NewLanguage(f.Name, c.Base.DefaultContentLanguage, v.TimeZone, f.LanguageConfig, logger)
if err != nil {
return err
}
key: "languages",
decode: func(d decodeWeight, p decodeConfig) error {
m := hmaps.CleanConfigStringMap(p.p.GetStringMap(d.key))
- if len(m) == 1 {
- // In v0.112.4 we moved this to the language config, but it's very commmon for mono language sites to have this at the top level.
- var first hmaps.Params
- var ok bool
- for _, v := range m {
- first, ok = v.(hmaps.Params)
- if ok {
- break
- }
- }
- if first != nil {
- if _, found := first["languagecode"]; !found {
- first["languagecode"] = p.p.GetString("languagecode")
- }
- }
- }
+ // Root-level locale/languageCode is passed to DecodeConfig so it can
+ // be applied to the default content language inside langs.DecodeConfig.
+ // They are passed separately so that an explicit per-lang languageCode
+ // can override a root-level languageCode (but not a root-level locale).
+ rootLocale := p.p.GetString("locale")
+ rootLanguageCode := p.p.GetString("languagecode")
var (
err error
defaultContentLanguage string
)
- p.c.Languages, defaultContentLanguage, err = langs.DecodeConfig(p.c.RootConfig.DefaultContentLanguage, p.c.RootConfig.DisableLanguages, m)
+ p.c.Languages, defaultContentLanguage, err = langs.DecodeConfig(p.c.RootConfig.DefaultContentLanguage, rootLocale, rootLanguageCode, p.c.RootConfig.DisableLanguages, m)
if err != nil {
return fmt.Errorf("failed to decode languages config: %w", err)
}
format = strings.ToLower(format)
projectConfig := map[string]any{
- "baseURL": "https://example.org/",
- "languageCode": "en-US",
- "title": "My New Hugo Project",
+ "baseURL": "https://example.org/",
+ "locale": "en-US",
+ "title": "My New Hugo Project",
"menus": map[string]any{
"main": []any{
map[string]any{
}
projectConfig := map[string]any{
- "baseURL": "https://example.org/",
- "title": "My New Hugo Project",
- "languageCode": "en-us",
+ "baseURL": "https://example.org/",
+ "locale": "en-us",
+ "title": "My New Hugo Project",
}
err := createProjectConfig(sourceFs, createpath, projectConfig, format)
<!DOCTYPE html>
-<html lang="{{ site.Language.LanguageCode }}" dir="{{ or site.Language.LanguageDirection `ltr` }}">
+<html lang="{{ site.Language.Locale }}" dir="{{ or site.Language.Direction `ltr` }}">
<head>
{{ partial "head.html" . }}
</head>
hint: photo
method: 2
useSharpYuv: false
- languageCode: ''
languages:
en:
+ direction: ''
disabled: false
- languageCode: ''
- languageDirection: ''
- languageName: ''
+ label: ''
+ locale: ''
title: ''
weight: 0
layoutDir: layouts
+ locale: ''
mainSections: null
markup:
asciiDocExt:
`
- b := Test(t, files)
+ b := Test(t, files, TestOptInfo())
+ b.AssertLogContains("deprecated")
b.AssertFileContent("public/index.html", "LanguageCode: en-US|en-US|")
}
sv:
weight: 3
params: *params
-
+
-- layouts/all.html --
Params: {{ site.Params }}|
-- themes/mytheme/hugo.yaml --
p2: p2aliastheme
params: *params
-
-
+
+
`
b := Test(t, files)
sv:
weight: 2
params: *params
-
+
-- layouts/all.html --
Params: {{ site.Params }}|
Test(t, files)
}
+
+// Issue 14269
+// When the legacy API deprecations are promoted to errors: flip Test() to TestE() and assert an error.
+func TestLanguageDeprecated(t *testing.T) {
+ t.Parallel()
+
+ const defaultConfig = `
+disableKinds = ['page','rss','section','sitemap','taxonomy','term']
+defaultContentLanguageInSubdir = true
+`
+
+ tests := []struct {
+ name string
+ config string
+ layout string
+ }{
+ {
+ name: "config key languageCode",
+ config: "languageCode = 'en-US'\n",
+ layout: "-- layouts/home.html --\nhome\n",
+ },
+ {
+ name: "config key languages.en.languageCode",
+ config: "[languages.en]\nlanguageCode = 'en-US'\n",
+ layout: "-- layouts/home.html --\nhome\n",
+ },
+ {
+ name: "config key languages.en.languageDirection",
+ config: "[languages.en]\nlanguageDirection = 'ltr'\n",
+ layout: "-- layouts/home.html --\nhome\n",
+ },
+ {
+ name: "config key languages.en.languageName",
+ config: "[languages.en]\nlanguageName = 'English'\n",
+ layout: "-- layouts/home.html --\nhome\n",
+ },
+ {
+ name: "template Site.LanguageCode",
+ config: "",
+ layout: "-- layouts/home.html --\n{{ .Site.LanguageCode }}\n",
+ },
+ {
+ name: "template Language.LanguageCode",
+ config: "",
+ layout: "-- layouts/home.html --\n{{ .Site.Language.LanguageCode }}\n",
+ },
+ {
+ name: "template Language.LanguageDirection",
+ config: "",
+ layout: "-- layouts/home.html --\n{{ .Site.Language.LanguageDirection }}\n",
+ },
+ {
+ name: "template Language.LanguageName",
+ config: "",
+ layout: "-- layouts/home.html --\n{{ .Site.Language.LanguageName }}\n",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+ files := "-- hugo.toml --\n" + defaultConfig + tt.config + tt.layout
+ b := Test(t, files, TestOptInfo())
+ b.AssertLogMatches("deprecated")
+ })
+ }
+}
+
+// Issue 14269
+// When the legacy API deprecations are promoted to errors: no changes needed.
+func TestLanguageNewAPI(t *testing.T) {
+ t.Parallel()
+
+ const (
+ defaultConfig = `
+disableKinds = ['page','rss','section','sitemap','taxonomy','term']
+defaultContentLanguageInSubdir = true
+`
+ layout = `
+-- layouts/home.html --
+{{ $l := .Site.Language -}}
+{{ printf "NA: %s|" $l.Name -}}
+{{ printf "LO: %s|" $l.Locale -}}
+{{ printf "DI: %s|" $l.Direction -}}
+{{ printf "LA: %s|" $l.Label -}}
+`
+ )
+
+ tests := []struct {
+ name string
+ config string
+ want map[string]string
+ }{
+ {
+ name: "Default config",
+ config: "",
+ want: map[string]string{
+ "public/en/index.html": "NA: en|LO: en|DI: |LA: |",
+ },
+ },
+ {
+ name: "Multilingual per-lang new config keys",
+ config: `
+[languages.en]
+locale = 'en-US'
+direction = 'ltr'
+label = 'English'
+[languages.fr]
+locale = 'fr-FR'
+direction = 'ltr'
+label = 'French'
+`,
+ want: map[string]string{
+ "public/en/index.html": "NA: en|LO: en-US|DI: ltr|LA: English|",
+ "public/fr/index.html": "NA: fr|LO: fr-FR|DI: ltr|LA: French|",
+ },
+ },
+ {
+ name: "Monolingual root locale",
+ config: "locale = 'en-US'\n",
+ want: map[string]string{
+ "public/en/index.html": "NA: en|LO: en-US|DI: |LA: |",
+ },
+ },
+ {
+ name: "Multilingual root locale",
+ config: `
+locale = 'en-US'
+[languages.en]
+[languages.fr]
+`,
+ want: map[string]string{
+ "public/en/index.html": "NA: en|LO: en-US|DI: |LA: |",
+ "public/fr/index.html": "NA: fr|LO: fr|DI: |LA: |",
+ },
+ },
+ {
+ name: "Multilingual per-lang locale overrides root locale",
+ config: `
+locale = 'en-NZ'
+[languages.en]
+locale = 'en-US'
+[languages.fr]
+locale = 'fr-FR'
+`,
+ want: map[string]string{
+ "public/en/index.html": "NA: en|LO: en-US|DI: |LA: |",
+ "public/fr/index.html": "NA: fr|LO: fr-FR|DI: |LA: |",
+ },
+ },
+ {
+ name: "Multilingual non-default defaultContentLanguage root locale",
+ config: `
+defaultContentLanguage = 'fr'
+locale = 'fr-FR'
+[languages.en]
+[languages.fr]
+`,
+ want: map[string]string{
+ "public/en/index.html": "NA: en|LO: en|DI: |LA: |",
+ "public/fr/index.html": "NA: fr|LO: fr-FR|DI: |LA: |",
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+ files := "-- hugo.toml --\n" + defaultConfig + tt.config + layout
+ b := Test(t, files)
+ for path, content := range tt.want {
+ b.AssertFileContent(path, content)
+ }
+ })
+ }
+}
+
+// Issue 14269
+// When the legacy API deprecations are promoted to errors: delete this function.
+func TestLanguageDeprecationMigration(t *testing.T) {
+ t.Parallel()
+
+ const (
+ defaultConfig = `
+disableKinds = ['page','rss','section','sitemap','taxonomy','term']
+defaultContentLanguageInSubdir = true
+`
+ layout = `
+-- layouts/home.html --
+{{ $l := .Site.Language -}}
+{{ printf "LO: %s|" $l.Locale -}}
+{{ printf "DI: %s|" $l.Direction -}}
+{{ printf "LA: %s|" $l.Label -}}
+`
+ )
+
+ tests := []struct {
+ name string
+ config string
+ want map[string]string
+ wantLogMatches []string
+ }{
+ {
+ name: "Monolingual root languageCode",
+ config: `
+languageCode = 'en-US'
+`,
+ want: map[string]string{
+ "public/en/index.html": "LO: en-US|DI: |LA: |",
+ },
+ wantLogMatches: []string{
+ `config key languageCode was deprecated`,
+ `! config key languages.*\.languageCode was deprecated`,
+ `! config key locale was deprecated`,
+ `! config key languages.*\.locale was deprecated`,
+ },
+ },
+ {
+ name: "Monolingual locale wins over languageCode",
+ config: `
+locale = 'en-US'
+languageCode = 'en-CA'
+`,
+ want: map[string]string{
+ "public/en/index.html": "LO: en-US|DI: |LA: |",
+ },
+ wantLogMatches: []string{
+ `config key languageCode was deprecated`,
+ `! config key languages.*\.languageCode was deprecated`,
+ `! config key locale was deprecated`,
+ `! config key languages.*\.locale was deprecated`,
+ },
+ },
+ {
+ name: "Multilingual root languageCode",
+ config: `
+languageCode = 'en-US'
+[languages.en]
+[languages.fr]
+`,
+ want: map[string]string{
+ "public/en/index.html": "LO: en-US|DI: |LA: |",
+ "public/fr/index.html": "LO: fr|DI: |LA: |",
+ },
+ wantLogMatches: []string{
+ `config key languageCode was deprecated`,
+ `! config key languages.*\.languageCode was deprecated`,
+ `! config key locale was deprecated`,
+ `! config key languages.*\.locale was deprecated`,
+ },
+ },
+ {
+ name: "Multilingual per-lang languageCode overrides root languageCode",
+ config: `
+languageCode = 'en-NZ'
+[languages.en]
+languageCode = 'en-US'
+[languages.fr]
+languageCode = 'fr-FR'
+`,
+ want: map[string]string{
+ "public/en/index.html": "LO: en-US|DI: |LA: |",
+ "public/fr/index.html": "LO: fr-FR|DI: |LA: |",
+ },
+ wantLogMatches: []string{
+ `config key languageCode was deprecated`,
+ `config key languages.en.languageCode was deprecated`,
+ `config key languages.fr.languageCode was deprecated`,
+ `! config key locale was deprecated`,
+ `! config key languages.*\.locale was deprecated`,
+ },
+ },
+ {
+ name: "Multilingual per-lang locale overrides root languageCode",
+ config: `
+languageCode = 'en-NZ'
+[languages.en]
+locale = 'en-US'
+[languages.fr]
+locale = 'fr-FR'
+`,
+ want: map[string]string{
+ "public/en/index.html": "LO: en-US|DI: |LA: |",
+ "public/fr/index.html": "LO: fr-FR|DI: |LA: |",
+ },
+ wantLogMatches: []string{
+ `config key languageCode was deprecated`,
+ `! config key languages.*\.languageCode was deprecated`,
+ `! config key locale was deprecated`,
+ `! config key languages.*\.locale was deprecated`,
+ },
+ },
+ {
+ name: "Multilingual root locale overrides per-lang languageCode",
+ config: `
+locale = 'en-NZ'
+[languages.en]
+languageCode = 'en-US'
+[languages.fr]
+languageCode = 'fr-FR'
+`,
+ want: map[string]string{
+ "public/en/index.html": "LO: en-NZ|DI: |LA: |",
+ "public/fr/index.html": "LO: fr-FR|DI: |LA: |",
+ },
+ wantLogMatches: []string{
+ `! config key languageCode was deprecated`,
+ `config key languages.en.languageCode was deprecated`,
+ `config key languages.fr.languageCode was deprecated`,
+ `! config key locale was deprecated`,
+ `! config key languages.*\.locale was deprecated`,
+ },
+ },
+ {
+ name: "Multilingual non-default defaultContentLanguage root languageCode",
+ config: `
+defaultContentLanguage = 'fr'
+languageCode = 'fr-FR'
+[languages.en]
+[languages.fr]
+`,
+ want: map[string]string{
+ "public/en/index.html": "LO: en|DI: |LA: |",
+ "public/fr/index.html": "LO: fr-FR|DI: |LA: |",
+ },
+ wantLogMatches: []string{
+ `config key languageCode was deprecated`,
+ `! config key languages.*\.languageCode was deprecated`,
+ `! config key locale was deprecated`,
+ `! config key languages.*\.locale was deprecated`,
+ },
+ },
+ {
+ name: "Multilingual per-lang old config keys",
+ config: `
+[languages.en]
+languageCode = 'en-US'
+languageDirection = 'ltr'
+languageName = 'English'
+[languages.fr]
+languageCode = 'fr-FR'
+languageDirection = 'ltr'
+languageName = 'French'
+`,
+ want: map[string]string{
+ "public/en/index.html": "LO: en-US|DI: ltr|LA: English|",
+ "public/fr/index.html": "LO: fr-FR|DI: ltr|LA: French|",
+ },
+ wantLogMatches: []string{
+ `! config key languageCode was deprecated`,
+ `config key languages.en.languageCode was deprecated`,
+ `config key languages.fr.languageCode was deprecated`,
+ `config key languages.en.languageDirection was deprecated`,
+ `config key languages.fr.languageDirection was deprecated`,
+ `config key languages.en.languageName was deprecated`,
+ `config key languages.fr.languageName was deprecated`,
+ `! config key locale was deprecated`,
+ `! config key languages.*\.locale was deprecated`,
+ `! config key languages.*\.direction was deprecated`,
+ `! config key languages.*\.label was deprecated`,
+ },
+ },
+ {
+ name: "Multilingual per-lang new config keys override old",
+ config: `
+[languages.en]
+locale = 'en-US'
+direction = 'ltr'
+label = 'English'
+
+languageCode = 'en-US'
+languageDirection = 'foo'
+languageName = 'English New Zealand'
+[languages.fr]
+locale = 'fr-FR'
+direction = 'ltr'
+label = 'French'
+
+languageCode = 'fr-CA'
+languageDirection = 'bar'
+languageName = 'French Canadian'
+`,
+ want: map[string]string{
+ "public/en/index.html": "LO: en-US|DI: ltr|LA: English|",
+ "public/fr/index.html": "LO: fr-FR|DI: ltr|LA: French|",
+ },
+ wantLogMatches: []string{
+ `! config key languageCode was deprecated`,
+ `config key languages.en.languageCode was deprecated`,
+ `config key languages.fr.languageCode was deprecated`,
+ `config key languages.en.languageDirection was deprecated`,
+ `config key languages.fr.languageDirection was deprecated`,
+ `config key languages.en.languageName was deprecated`,
+ `config key languages.fr.languageName was deprecated`,
+ `! config key locale was deprecated`,
+ `! config key languages.*\.locale was deprecated`,
+ `! config key languages.*\.direction was deprecated`,
+ `! config key languages.*\.label was deprecated`,
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+ files := "-- hugo.toml --\n" + defaultConfig + "\n" + tt.config + "\n" + layout
+
+ b := Test(t, files, TestOptInfo())
+ for path, content := range tt.want {
+ b.AssertFileContent(path, content)
+ }
+ for _, pattern := range tt.wantLogMatches {
+ b.AssertLogMatches(pattern)
+ }
+ })
+ }
+}
}
}
+// Deprecated: Use .Language.Locale instead.
func (s *Site) LanguageCode() string {
- return s.Language().LanguageCode()
+ hugo.DeprecateWithLogger(".Site.LanguageCode", "Use .Site.Language.Locale instead.", "v0.158.0", s.Language().Logger())
+ return s.Language().Locale()
}
// Returns all sites for all dimensions.
{{ .AddResource (dict "path" "hello.txt" "title" "Hello" "content" $contentTextEnglish "sites" (dict "matrix" $en )) }}
{{ .AddResource (dict "path" "hello.txt" "title" "Hello" "content" $contentTextSwedish "sites" (dict "matrix" $sv )) }}
{{ .AddResource (dict "path" "hello.txt" "title" "Hello" "content" $contentTextDanish "sites" (dict "matrix" $da )) }}
-
+
-- layouts/all.html --
{{ $hello := .Resources.Get "hello.txt" }}
All. {{ .Title }}|{{ .Site.Language.Name }}|hello: {{ with $hello }}{{ .RelPermalink }}: {{ .Content }}|{{ end }}|
Rotate(role): {{ with .Rotate "role" }}{{ range . }}{{ template "printp" . }}|{{ end }}{{ end }}$
{{ define "printp" }}{{ .RelPermalink }}:{{ with .Site }}{{ template "prints" . }}{{ end }}{{ end }}
{{ define "prints" }}/l:{{ .Language.Name }}/v:{{ .Version.Name }}/r:{{ .Role.Name }}{{ end }}
-
+
`
}
b.Assert(toJSONAndMap(conf.Languages), qt.DeepEquals,
map[string]any{
"en": map[string]any{
- "Disabled": bool(false),
- "LanguageCode": "",
- "LanguageDirection": "",
- "LanguageName": "",
- "Title": "",
- "Weight": float64(0),
+ "Direction": "",
+ "Disabled": bool(false),
+ "Label": "",
+ "Locale": "",
+ "Title": "",
+ "Weight": float64(0),
},
})
// LanguageConfig holds the configuration for a single language.
// This is what is read from the config file.
type LanguageConfig struct {
- // The language name, e.g. "English".
- LanguageName string
+ // Deprecated: Use Label instead.
+ LanguageName string `json:"-"`
- // The language code, e.g. "en-US".
- LanguageCode string
+ // Deprecated: Use Locale instead.
+ LanguageCode string `json:"-"`
// The language title. When set, this will
// override site.Title for this language.
Title string
- // The language direction, e.g. "ltr" or "rtl".
- LanguageDirection string
+ // Deprecated: Use Direction instead.
+ LanguageDirection string `json:"-"`
// The language weight. When set to a non-zero value, this will
// be the main sort criteria for the language.
// Set to true to disable this language.
Disabled bool
+
+ // The language direction, e.g. "ltr" or "rtl".
+ Direction string
+
+ // The language name, e.g. "English".
+ Label string
+
+ // The locale, e.g. "en-US".
+ Locale string
}
type LanguageInternal struct {
}
}
-func (ls *LanguagesInternal) init(defaultContentLanguage string, disabledLanguages []string) (string, error) {
+func (ls *LanguagesInternal) init(defaultContentLanguage, rootLocale, rootLanguageCode string, disabledLanguages []string) (string, error) {
const en = "en"
if len(ls.LanguageConfigs) == 0 {
ls.LanguageConfigs[d.Name] = d.LanguageConfig
ls.Sorted[defaultIdx] = d
defaultContentLanguage = d.Name
+ }
+ // Apply root-level locale to the default content language if it has none.
+ // Done after defaultContentLanguage is resolved and Sorted is built so both
+ // are updated consistently. Other languages fall back to their Lang key.
+ // Priority: per-lang locale > root locale > per-lang languageCode > root
+ // languageCode. Root languageCode only applies when the default language has
+ // neither a per-lang locale nor a per-lang languageCode.
+ applyRootLocale := func(locale string) {
+ if v, ok := ls.LanguageConfigs[defaultContentLanguage]; ok {
+ v.Locale = locale
+ ls.LanguageConfigs[defaultContentLanguage] = v
+ for i, s := range ls.Sorted {
+ if s.Name == defaultContentLanguage {
+ ls.Sorted[i].LanguageConfig.Locale = locale
+ break
+ }
+ }
+ }
+ }
+ if rootLocale != "" {
+ if v, ok := ls.LanguageConfigs[defaultContentLanguage]; ok && v.Locale == "" {
+ applyRootLocale(rootLocale)
+ }
+ } else if rootLanguageCode != "" {
+ if v, ok := ls.LanguageConfigs[defaultContentLanguage]; ok && v.Locale == "" && v.LanguageCode == "" {
+ applyRootLocale(rootLanguageCode)
+ }
}
return defaultContentLanguage, nil
}
-func DecodeConfig(defaultContentLanguage string, disabledLanguages []string, m map[string]any) (*config.ConfigNamespace[map[string]LanguageConfig, LanguagesInternal], string, error) {
+func DecodeConfig(defaultContentLanguage, rootLocale, rootLanguageCode string, disabledLanguages []string, m map[string]any) (*config.ConfigNamespace[map[string]LanguageConfig, LanguagesInternal], string, error) {
v, err := config.DecodeNamespace[map[string]LanguageConfig](m, func(in any) (LanguagesInternal, any, error) {
var languages LanguagesInternal
var conf map[string]LanguageConfig
}
languages.LanguageConfigs = conf
var err error
- if defaultContentLanguage, err = languages.init(defaultContentLanguage, disabledLanguages); err != nil {
+ if defaultContentLanguage, err = languages.init(defaultContentLanguage, rootLocale, rootLanguageCode, disabledLanguages); err != nil {
return languages, nil, err
}
return languages, languages.LanguageConfigs, nil
}
// getTranslateFunc returns the translation function for the language in Deps.
-// We first try the language code (e.g. "en-US"), then the language key (e.g. "en").
+// 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 lc := l.LanguageCode(); lc != "" {
- if fn, ok := tp.t.Lookup(strings.ToLower(lc)); ok {
+ if locale := l.Locale(); locale != "" {
+ if fn, ok := tp.t.Lookup(strings.ToLower(locale)); ok {
return fn
}
}
"golang.org/x/text/collate"
"golang.org/x/text/language"
+ "github.com/bep/logg"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/htime"
+ "github.com/gohugoio/hugo/common/hugo"
+ "github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
"github.com/gohugoio/locales"
translators "github.com/gohugoio/localescompressed"
// This is just an alias of Site.Params.
params hmaps.Params
+
+ logger loggers.Logger
}
// Name is an alias for Lang.
return l.Lang
}
+func (l *Language) Logger() logg.Logger {
+ if l.logger == nil {
+ return loggers.Log().Logger()
+ }
+ return l.logger.Logger()
+}
+
func (l *Language) IsDefault() bool {
return l.isDefault
}
// NewLanguage creates a new language.
-func NewLanguage(lang, defaultContentLanguage, timeZone string, languageConfig LanguageConfig) (*Language, error) {
+func NewLanguage(lang, defaultContentLanguage, timeZone string, languageConfig LanguageConfig, logger loggers.Logger) (*Language, error) {
translator := translators.GetTranslator(lang)
if translator == nil {
translator = translators.GetTranslator(defaultContentLanguage)
collator1: coll1,
collator2: coll2,
isDefault: lang == defaultContentLanguage,
+ logger: logger,
}
return l, l.loadLocation(timeZone)
return l.params
}
+// Deprecated: Use Locale instead.
func (l *Language) LanguageCode() string {
+ hugo.DeprecateWithLogger(".Language.LanguageCode", "Use .Language.Locale instead.", "v0.158.0", l.Logger())
+ return l.Locale()
+}
+
+func (l *Language) Locale() string {
+ if l.LanguageConfig.Locale != "" {
+ return l.LanguageConfig.Locale
+ }
if l.LanguageConfig.LanguageCode != "" {
return l.LanguageConfig.LanguageCode
}
return l.Lang
}
+// Deprecated: Use Direction instead.
+func (l *Language) LanguageDirection() string {
+ hugo.DeprecateWithLogger(".Language.LanguageDirection", "Use .Language.Direction instead.", "v0.158.0", l.Logger())
+ return l.Direction()
+}
+
+func (l *Language) Direction() string {
+ if l.LanguageConfig.Direction != "" {
+ return l.LanguageConfig.Direction
+ }
+ return l.LanguageConfig.LanguageDirection
+}
+
+// Deprecated: Use Label instead.
+func (l *Language) LanguageName() string {
+ hugo.DeprecateWithLogger(".Language.LanguageName", "Use .Language.Label instead.", "v0.158.0", l.Logger())
+ return l.Label()
+}
+
+func (l *Language) Label() string {
+ if l.LanguageConfig.Label != "" {
+ return l.LanguageConfig.Label
+ }
+ return l.LanguageConfig.LanguageName
+}
+
func (l *Language) loadLocation(tzStr string) error {
location, err := time.LoadLocation(tzStr)
if err != nil {
"testing"
qt "github.com/frankban/quicktest"
+ "github.com/gohugoio/hugo/common/loggers"
"golang.org/x/text/collate"
"golang.org/x/text/language"
)
})
})
}
+
+// TestLanguageLegacyFieldFallbacks verifies that Locale(), Direction(), and
+// Label() fall back to legacy LanguageConfig fields when the canonical fields
+// are not set. This matters for programmatic construction that bypasses the
+// allconfig migration (which normally copies legacy→canonical and clears them).
+func TestLanguageLegacyFieldFallbacks(t *testing.T) {
+ c := qt.New(t)
+
+ l, err := NewLanguage("en", "en", "UTC", LanguageConfig{
+ LanguageCode: "en-US",
+ LanguageName: "English",
+ LanguageDirection: "ltr",
+ }, loggers.NewDefault())
+ c.Assert(err, qt.IsNil)
+ c.Assert(l.Locale(), qt.Equals, "en-US")
+ c.Assert(l.Label(), qt.Equals, "English")
+ c.Assert(l.Direction(), qt.Equals, "ltr")
+
+ // Deprecated methods must not panic when logger is nil (no logger provided
+ // at construction). They delegate through Logger() which has a nil guard.
+ lNoLogger, err := NewLanguage("en", "en", "UTC", LanguageConfig{}, nil)
+ c.Assert(err, qt.IsNil)
+ _ = lNoLogger.LanguageCode()
+ _ = lNoLogger.LanguageName()
+ _ = lNoLogger.LanguageDirection()
+}
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/hstore"
+ "github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/config/privacy"
"github.com/gohugoio/hugo/config/services"
"github.com/gohugoio/hugo/hugolib/roles"
"github.com/gohugoio/hugo/hugolib/versions"
"github.com/gohugoio/hugo/identity"
+ "github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/langs"
// Returns the configured title for this Site.
Title() string
- // Deprecated: Use .Language.LanguageCode instead.
+ // Deprecated: Use .Language.Locale instead.
LanguageCode() string
// Returns the configured copyright information for this Site.
}
func (s *siteWrapper) LanguageCode() string {
- return s.s.LanguageCode()
+ hugo.DeprecateWithLogger(".Site.LanguageCode", "Use .Site.Language.Locale instead.", "v0.158.0", s.s.Language().Logger())
+ return s.s.Language().Locale()
}
func (s *siteWrapper) Copyright() string {
return "foo"
}
+// Deprecated: Use .Language.Locale instead.
func (t testSite) LanguageCode() string {
- return t.l.Lang
+ hugo.DeprecateWithLogger(".Site.LanguageCode", "Use .Site.Language.Locale instead.", "v0.158.0", t.l.Logger())
+ return t.l.Locale()
}
func (t testSite) Copyright() string {
opts := HugoInfoOptions{
Conf: conf,
}
+ l, err := langs.NewLanguage("en", "en", "", langs.LanguageConfig{}, loggers.NewDefault())
+ if err != nil {
+ panic(err)
+ }
return testSite{
h: NewHugoInfo(opts),
- l: &langs.Language{
- Lang: "en",
- },
+ l: l,
}
}
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/hstore"
+ "github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/hugofs"
langs.LanguageConfig{
LanguageName: "English",
},
+ loggers.NewDefault(),
)
if err != nil {
panic(err)
<meta property="og:description" content="{{ trim . "\n\r\t " }}">
{{- end }}
-{{- with or .Params.locale site.Language.LanguageCode }}
+{{- with or .Params.locale site.Language.Locale }}
<meta property="og:locale" content="{{ replace . `-` `_` }}">
{{- end }}
<!DOCTYPE html>
-<html lang="{{ site.Language.LanguageCode }}">
+<html lang="{{ site.Language.Locale }}">
<head>
<title>{{ .Permalink }}</title>
{{ with .OutputFormats.Canonical }}<link rel="{{ .Rel }}" href="{{ .Permalink }}">{{ end }}
<link>{{ .Permalink }}</link>
<description>Recent content {{ if ne .Title .Site.Title }}{{ with .Title }}in {{ . }} {{ end }}{{ end }}on {{ .Site.Title }}</description>
<generator>Hugo</generator>
- <language>{{ site.Language.LanguageCode }}</language>{{ with $authorEmail }}
+ <language>{{ site.Language.Locale }}</language>{{ with $authorEmail }}
<managingEditor>{{.}}{{ with $authorName }} ({{ . }}){{ end }}</managingEditor>{{ end }}{{ with $authorEmail }}
<webMaster>{{ . }}{{ with $authorName }} ({{ . }}){{ end }}</webMaster>{{ end }}{{ with .Site.Copyright }}
<copyright>{{ . }}</copyright>{{ end }}{{ if not .Date.IsZero }}
<priority>{{ .Sitemap.Priority }}</priority>{{ end }}{{ if .IsTranslated }}{{ range .Translations }}
<xhtml:link
rel="alternate"
- hreflang="{{ .Language.LanguageCode }}"
+ hreflang="{{ .Language.Locale }}"
href="{{ .Permalink }}"
/>{{ end }}
<xhtml:link
rel="alternate"
- hreflang="{{ .Language.LanguageCode }}"
+ hreflang="{{ .Language.Locale }}"
href="{{ .Permalink }}"
/>{{ end }}
</url>