diff options
Diffstat (limited to 'langs')
-rw-r--r-- | langs/config.go | 217 | ||||
-rw-r--r-- | langs/i18n/i18n.go | 117 | ||||
-rw-r--r-- | langs/i18n/i18n_test.go | 272 | ||||
-rw-r--r-- | langs/i18n/translationProvider.go | 135 | ||||
-rw-r--r-- | langs/language.go | 258 | ||||
-rw-r--r-- | langs/language_test.go | 49 |
6 files changed, 1048 insertions, 0 deletions
diff --git a/langs/config.go b/langs/config.go new file mode 100644 index 000000000..184223650 --- /dev/null +++ b/langs/config.go @@ -0,0 +1,217 @@ +// Copyright 2018 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 langs + +import ( + "fmt" + "path/filepath" + "sort" + "strings" + + "github.com/gohugoio/hugo/common/maps" + + "github.com/spf13/cast" + + "github.com/pkg/errors" + + "github.com/gohugoio/hugo/config" +) + +type LanguagesConfig struct { + Languages Languages + Multihost bool + DefaultContentLanguageInSubdir bool +} + +func LoadLanguageSettings(cfg config.Provider, oldLangs Languages) (c LanguagesConfig, err error) { + + defaultLang := cfg.GetString("defaultContentLanguage") + if defaultLang == "" { + defaultLang = "en" + cfg.Set("defaultContentLanguage", defaultLang) + } + + var languages map[string]interface{} + + languagesFromConfig := cfg.GetStringMap("languages") + disableLanguages := cfg.GetStringSlice("disableLanguages") + + if len(disableLanguages) == 0 { + languages = languagesFromConfig + } else { + languages = make(map[string]interface{}) + for k, v := range languagesFromConfig { + for _, disabled := range disableLanguages { + if disabled == defaultLang { + return c, fmt.Errorf("cannot disable default language %q", defaultLang) + } + + if strings.EqualFold(k, disabled) { + v.(map[string]interface{})["disabled"] = true + break + } + } + languages[k] = v + } + } + + var languages2 Languages + + if len(languages) == 0 { + languages2 = append(languages2, NewDefaultLanguage(cfg)) + } else { + languages2, err = toSortedLanguages(cfg, languages) + if err != nil { + return c, errors.Wrap(err, "Failed to parse multilingual config") + } + } + + if oldLangs != nil { + // When in multihost mode, the languages are mapped to a server, so + // some structural language changes will need a restart of the dev server. + // The validation below isn't complete, but should cover the most + // important cases. + var invalid bool + if languages2.IsMultihost() != oldLangs.IsMultihost() { + invalid = true + } else { + if languages2.IsMultihost() && len(languages2) != len(oldLangs) { + invalid = true + } + } + + if invalid { + return c, errors.New("language change needing a server restart detected") + } + + if languages2.IsMultihost() { + // We need to transfer any server baseURL to the new language + for i, ol := range oldLangs { + nl := languages2[i] + nl.Set("baseURL", ol.GetString("baseURL")) + } + } + } + + // The defaultContentLanguage is something the user has to decide, but it needs + // to match a language in the language definition list. + langExists := false + for _, lang := range languages2 { + if lang.Lang == defaultLang { + langExists = true + break + } + } + + if !langExists { + return c, fmt.Errorf("site config value %q for defaultContentLanguage does not match any language definition", defaultLang) + } + + c.Languages = languages2 + c.Multihost = languages2.IsMultihost() + c.DefaultContentLanguageInSubdir = c.Multihost + + sortedDefaultFirst := make(Languages, len(c.Languages)) + for i, v := range c.Languages { + sortedDefaultFirst[i] = v + } + sort.Slice(sortedDefaultFirst, func(i, j int) bool { + li, lj := sortedDefaultFirst[i], sortedDefaultFirst[j] + if li.Lang == defaultLang { + return true + } + + if lj.Lang == defaultLang { + return false + } + + return i < j + }) + + cfg.Set("languagesSorted", c.Languages) + cfg.Set("languagesSortedDefaultFirst", sortedDefaultFirst) + cfg.Set("multilingual", len(languages2) > 1) + + multihost := c.Multihost + + if multihost { + cfg.Set("defaultContentLanguageInSubdir", true) + cfg.Set("multihost", true) + } + + if multihost { + // The baseURL may be provided at the language level. If that is true, + // then every language must have a baseURL. In this case we always render + // to a language sub folder, which is then stripped from all the Permalink URLs etc. + for _, l := range languages2 { + burl := l.GetLocal("baseURL") + if burl == nil { + return c, errors.New("baseURL must be set on all or none of the languages") + } + } + + } + + return c, nil +} + +func toSortedLanguages(cfg config.Provider, l map[string]interface{}) (Languages, error) { + languages := make(Languages, len(l)) + i := 0 + + for lang, langConf := range l { + langsMap, err := maps.ToStringMapE(langConf) + + if err != nil { + return nil, fmt.Errorf("Language config is not a map: %T", langConf) + } + + language := NewLanguage(lang, cfg) + + for loki, v := range langsMap { + switch loki { + case "title": + language.Title = cast.ToString(v) + case "languagename": + language.LanguageName = cast.ToString(v) + case "weight": + language.Weight = cast.ToInt(v) + case "contentdir": + language.ContentDir = filepath.Clean(cast.ToString(v)) + case "disabled": + language.Disabled = cast.ToBool(v) + case "params": + m := maps.ToStringMap(v) + // Needed for case insensitive fetching of params values + maps.ToLower(m) + for k, vv := range m { + language.SetParam(k, vv) + } + } + + // Put all into the Params map + language.SetParam(loki, v) + + // Also set it in the configuration map (for baseURL etc.) + language.Set(loki, v) + } + + languages[i] = language + i++ + } + + sort.Sort(languages) + + return languages, nil +} diff --git a/langs/i18n/i18n.go b/langs/i18n/i18n.go new file mode 100644 index 000000000..5beef8683 --- /dev/null +++ b/langs/i18n/i18n.go @@ -0,0 +1,117 @@ +// Copyright 2017 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 i18n + +import ( + "github.com/gohugoio/hugo/common/loggers" + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/helpers" + + "github.com/nicksnyder/go-i18n/i18n/bundle" + "github.com/nicksnyder/go-i18n/i18n/translation" +) + +var ( + i18nWarningLogger = helpers.NewDistinctFeedbackLogger() +) + +// Translator handles i18n translations. +type Translator struct { + translateFuncs map[string]bundle.TranslateFunc + cfg config.Provider + logger *loggers.Logger +} + +// NewTranslator creates a new Translator for the given language bundle and configuration. +func NewTranslator(b *bundle.Bundle, cfg config.Provider, logger *loggers.Logger) Translator { + t := Translator{cfg: cfg, logger: logger, translateFuncs: make(map[string]bundle.TranslateFunc)} + t.initFuncs(b) + return t +} + +// Func gets the translate func for the given language, or for the default +// configured language if not found. +func (t Translator) Func(lang string) bundle.TranslateFunc { + if f, ok := t.translateFuncs[lang]; ok { + return f + } + t.logger.INFO.Printf("Translation func for language %v not found, use default.", lang) + if f, ok := t.translateFuncs[t.cfg.GetString("defaultContentLanguage")]; ok { + return f + } + t.logger.INFO.Println("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 ...interface{}) string { + return "" + } + +} + +func (t Translator) initFuncs(bndl *bundle.Bundle) { + defaultContentLanguage := t.cfg.GetString("defaultContentLanguage") + + defaultT, err := bndl.Tfunc(defaultContentLanguage) + if err != nil { + t.logger.INFO.Printf("No translation bundle found for default language %q", defaultContentLanguage) + } + + translations := bndl.Translations() + + enableMissingTranslationPlaceholders := t.cfg.GetBool("enableMissingTranslationPlaceholders") + for _, lang := range bndl.LanguageTags() { + currentLang := lang + + t.translateFuncs[currentLang] = func(translationID string, args ...interface{}) string { + tFunc, err := bndl.Tfunc(currentLang) + if err != nil { + t.logger.WARN.Printf("could not load translations for language %q (%s), will use default content language.\n", lang, err) + } + + translated := tFunc(translationID, args...) + if translated != translationID { + return translated + } + // If there is no translation for translationID, + // then Tfunc returns translationID itself. + // But if user set same translationID and translation, we should check + // if it really untranslated: + if isIDTranslated(translations, currentLang, translationID) { + return translated + } + + if t.cfg.GetBool("logI18nWarnings") { + i18nWarningLogger.Printf("i18n|MISSING_TRANSLATION|%s|%s", currentLang, translationID) + } + if enableMissingTranslationPlaceholders { + return "[i18n] " + translationID + } + if defaultT != nil { + translated := defaultT(translationID, args...) + if translated != translationID { + return translated + } + if isIDTranslated(translations, defaultContentLanguage, translationID) { + return translated + } + } + return "" + } + } +} + +// If the translation map contains translationID for specified currentLang, +// then the translationID is actually translated. +func isIDTranslated(translations map[string]map[string]translation.Translation, lang, id string) bool { + _, contains := translations[lang][id] + return contains +} diff --git a/langs/i18n/i18n_test.go b/langs/i18n/i18n_test.go new file mode 100644 index 000000000..d9215952a --- /dev/null +++ b/langs/i18n/i18n_test.go @@ -0,0 +1,272 @@ +// Copyright 2017 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 i18n + +import ( + "path/filepath" + "testing" + + "github.com/gohugoio/hugo/modules" + + "github.com/gohugoio/hugo/tpl/tplimpl" + + "github.com/gohugoio/hugo/common/loggers" + "github.com/gohugoio/hugo/langs" + "github.com/gohugoio/hugo/resources/page" + "github.com/spf13/afero" + "github.com/spf13/viper" + + "github.com/gohugoio/hugo/deps" + + qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/hugofs" +) + +var logger = loggers.NewErrorLogger() + +type i18nTest struct { + name string + data map[string][]byte + args interface{} + lang, id, expected, expectedFlag string +} + +var i18nTests = []i18nTest{ + // All translations present + { + name: "all-present", + data: map[string][]byte{ + "en.toml": []byte("[hello]\nother = \"Hello, World!\""), + "es.toml": []byte("[hello]\nother = \"¡Hola, Mundo!\""), + }, + args: nil, + lang: "es", + id: "hello", + expected: "¡Hola, Mundo!", + expectedFlag: "¡Hola, Mundo!", + }, + // Translation missing in current language but present in default + { + name: "present-in-default", + data: map[string][]byte{ + "en.toml": []byte("[hello]\nother = \"Hello, World!\""), + "es.toml": []byte("[goodbye]\nother = \"¡Adiós, Mundo!\""), + }, + args: nil, + lang: "es", + id: "hello", + expected: "Hello, World!", + expectedFlag: "[i18n] hello", + }, + // Translation missing in default language but present in current + { + name: "present-in-current", + data: map[string][]byte{ + "en.toml": []byte("[goodbye]\nother = \"Goodbye, World!\""), + "es.toml": []byte("[hello]\nother = \"¡Hola, Mundo!\""), + }, + args: nil, + lang: "es", + id: "hello", + expected: "¡Hola, Mundo!", + expectedFlag: "¡Hola, Mundo!", + }, + // Translation missing in both default and current language + { + name: "missing", + data: map[string][]byte{ + "en.toml": []byte("[goodbye]\nother = \"Goodbye, World!\""), + "es.toml": []byte("[goodbye]\nother = \"¡Adiós, Mundo!\""), + }, + args: nil, + lang: "es", + id: "hello", + expected: "", + expectedFlag: "[i18n] hello", + }, + // Default translation file missing or empty + { + name: "file-missing", + data: map[string][]byte{ + "en.toml": []byte(""), + }, + args: nil, + lang: "es", + id: "hello", + expected: "", + expectedFlag: "[i18n] hello", + }, + // Context provided + { + name: "context-provided", + data: map[string][]byte{ + "en.toml": []byte("[wordCount]\nother = \"Hello, {{.WordCount}} people!\""), + "es.toml": []byte("[wordCount]\nother = \"¡Hola, {{.WordCount}} gente!\""), + }, + args: struct { + WordCount int + }{ + 50, + }, + lang: "es", + id: "wordCount", + expected: "¡Hola, 50 gente!", + expectedFlag: "¡Hola, 50 gente!", + }, + // Same id and translation in current language + // https://github.com/gohugoio/hugo/issues/2607 + { + name: "same-id-and-translation", + data: map[string][]byte{ + "es.toml": []byte("[hello]\nother = \"hello\""), + "en.toml": []byte("[hello]\nother = \"hi\""), + }, + args: nil, + lang: "es", + id: "hello", + expected: "hello", + expectedFlag: "hello", + }, + // Translation missing in current language, but same id and translation in default + { + name: "same-id-and-translation-default", + data: map[string][]byte{ + "es.toml": []byte("[bye]\nother = \"bye\""), + "en.toml": []byte("[hello]\nother = \"hello\""), + }, + args: nil, + lang: "es", + id: "hello", + expected: "hello", + expectedFlag: "[i18n] hello", + }, + // Unknown language code should get its plural spec from en + { + name: "unknown-language-code", + data: map[string][]byte{ + "en.toml": []byte(`[readingTime] +one ="one minute read" +other = "{{.Count}} minutes read"`), + "klingon.toml": []byte(`[readingTime] +one = "eitt minutt med lesing" +other = "{{ .Count }} minuttar lesing"`), + }, + args: 3, + lang: "klingon", + id: "readingTime", + expected: "3 minuttar lesing", + expectedFlag: "3 minuttar lesing", + }, +} + +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) + +} + +func prepareTranslationProvider(t testing.TB, test i18nTest, cfg config.Provider) *TranslationProvider { + c := qt.New(t) + fs := hugofs.NewMem(cfg) + + for file, content := range test.data { + err := afero.WriteFile(fs.Source, filepath.Join("i18n", file), []byte(content), 0755) + c.Assert(err, qt.IsNil) + } + + tp := NewTranslationProvider() + depsCfg := newDepsConfig(tp, cfg, fs) + d, err := deps.New(depsCfg) + c.Assert(err, qt.IsNil) + c.Assert(d.LoadResources(), qt.IsNil) + + return tp +} + +func newDepsConfig(tp *TranslationProvider, cfg config.Provider, fs *hugofs.Fs) deps.DepsCfg { + l := langs.NewLanguage("en", cfg) + l.Set("i18nDir", "i18n") + return deps.DepsCfg{ + Language: l, + Site: page.NewDummyHugoSite(cfg), + Cfg: cfg, + Fs: fs, + Logger: logger, + TemplateProvider: tplimpl.DefaultTemplateProvider, + TranslationProvider: tp, + } +} + +func getConfig() *viper.Viper { + v := viper.New() + v.SetDefault("defaultContentLanguage", "en") + v.Set("contentDir", "content") + v.Set("dataDir", "data") + v.Set("i18nDir", "i18n") + v.Set("layoutDir", "layouts") + v.Set("archetypeDir", "archetypes") + v.Set("assetDir", "assets") + v.Set("resourceDir", "resources") + v.Set("publishDir", "public") + langs.LoadLanguageSettings(v, nil) + mod, err := modules.CreateProjectModule(v) + if err != nil { + panic(err) + } + v.Set("allModules", modules.Modules{mod}) + + return v + +} + +func TestI18nTranslate(t *testing.T) { + c := qt.New(t) + var actual, expected string + v := getConfig() + + // Test without and with placeholders + for _, enablePlaceholders := range []bool{false, true} { + v.Set("enableMissingTranslationPlaceholders", enablePlaceholders) + + for _, test := range i18nTests { + if enablePlaceholders { + expected = test.expectedFlag + } else { + expected = test.expected + } + actual = doTestI18nTranslate(t, test, v) + c.Assert(actual, qt.Equals, expected) + } + } +} + +func BenchmarkI18nTranslate(b *testing.B) { + v := getConfig() + for _, test := range i18nTests { + b.Run(test.name, func(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) + if actual != test.expected { + b.Fatalf("expected %v got %v", test.expected, actual) + } + } + }) + } + +} diff --git a/langs/i18n/translationProvider.go b/langs/i18n/translationProvider.go new file mode 100644 index 000000000..4ce9b59fe --- /dev/null +++ b/langs/i18n/translationProvider.go @@ -0,0 +1,135 @@ +// Copyright 2017 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 i18n + +import ( + "errors" + + "github.com/gohugoio/hugo/common/herrors" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/hugofs" + "github.com/gohugoio/hugo/source" + "github.com/nicksnyder/go-i18n/i18n/bundle" + "github.com/nicksnyder/go-i18n/i18n/language" + _errors "github.com/pkg/errors" +) + +// TranslationProvider provides translation handling, i.e. loading +// of bundles etc. +type TranslationProvider struct { + t Translator +} + +// NewTranslationProvider creates a new translation provider. +func NewTranslationProvider() *TranslationProvider { + return &TranslationProvider{} +} + +// Update updates the i18n func in the provided Deps. +func (tp *TranslationProvider) Update(d *deps.Deps) error { + spec := source.NewSourceSpec(d.PathSpec, nil) + + i18nBundle := bundle.New() + + en := language.GetPluralSpec("en") + if en == nil { + return errors.New("the English language has vanished like an old oak table") + } + var newLangs []string + + // The source dirs are ordered so the most important comes first. Since this is a + // last key win situation, we have to reverse the iteration order. + dirs := d.BaseFs.I18n.Dirs + for i := len(dirs) - 1; i >= 0; i-- { + dir := dirs[i] + src := spec.NewFilesystemFromFileMetaInfo(dir) + + files, err := src.Files() + if err != nil { + return err + } + + for _, r := range files { + currentSpec := language.GetPluralSpec(r.BaseFileName()) + if currentSpec == nil { + // This may is a language code not supported by go-i18n, it may be + // Klingon or ... not even a fake language. Make sure it works. + newLangs = append(newLangs, r.BaseFileName()) + } + } + + if len(newLangs) > 0 { + language.RegisterPluralSpec(newLangs, en) + } + + for _, file := range files { + if err := addTranslationFile(i18nBundle, file); err != nil { + return err + } + } + } + + tp.t = NewTranslator(i18nBundle, d.Cfg, d.Log) + + d.Translate = tp.t.Func(d.Language.Lang) + + return nil + +} + +func addTranslationFile(bundle *bundle.Bundle, r source.File) error { + f, err := r.FileInfo().Meta().Open() + if err != nil { + return _errors.Wrapf(err, "failed to open translations file %q:", r.LogicalName()) + } + err = bundle.ParseTranslationFileBytes(r.LogicalName(), helpers.ReaderToBytes(f)) + f.Close() + if err != nil { + return errWithFileContext(_errors.Wrapf(err, "failed to load translations"), r) + } + return nil +} + +// 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) + + return nil +} + +func errWithFileContext(inerr error, r source.File) error { + fim, ok := r.FileInfo().(hugofs.FileMetaInfo) + if !ok { + return inerr + } + + meta := fim.Meta() + realFilename := meta.Filename() + f, err := meta.Open() + if err != nil { + return inerr + } + defer f.Close() + + err, _ = herrors.WithFileContext( + inerr, + realFilename, + f, + herrors.SimpleLineMatcher) + + return err + +} diff --git a/langs/language.go b/langs/language.go new file mode 100644 index 000000000..0e04324e9 --- /dev/null +++ b/langs/language.go @@ -0,0 +1,258 @@ +// Copyright 2018 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 langs + +import ( + "sort" + "strings" + "sync" + + "github.com/gohugoio/hugo/common/maps" + "github.com/gohugoio/hugo/config" + "github.com/spf13/cast" +) + +// These are the settings that should only be looked up in the global Viper +// config and not per language. +// This list may not be complete, but contains only settings that we know +// will be looked up in both. +// This isn't perfect, but it is ultimately the user who shoots him/herself in +// the foot. +// See the pathSpec. +var globalOnlySettings = map[string]bool{ + strings.ToLower("defaultContentLanguageInSubdir"): true, + strings.ToLower("defaultContentLanguage"): true, + strings.ToLower("multilingual"): true, + strings.ToLower("assetDir"): true, + strings.ToLower("resourceDir"): true, +} + +// Language manages specific-language configuration. +type Language struct { + Lang string + LanguageName string + Title string + Weight int + + Disabled bool + + // If set per language, this tells Hugo that all content files without any + // language indicator (e.g. my-page.en.md) is in this language. + // This is usually a path relative to the working dir, but it can be an + // absolute directory reference. It is what we get. + ContentDir string + + Cfg config.Provider + + // These are params declared in the [params] section of the language merged with the + // site's params, the most specific (language) wins on duplicate keys. + params map[string]interface{} + paramsMu sync.Mutex + paramsSet bool + + // These are config values, i.e. the settings declared outside of the [params] section of the language. + // This is the map Hugo looks in when looking for configuration values (baseURL etc.). + // Values in this map can also be fetched from the params map above. + settings map[string]interface{} +} + +func (l *Language) String() string { + return l.Lang +} + +// NewLanguage creates a new language. +func NewLanguage(lang string, cfg config.Provider) *Language { + // Note that language specific params will be overridden later. + // We should improve that, but we need to make a copy: + params := make(map[string]interface{}) + for k, v := range cfg.GetStringMap("params") { + params[k] = v + } + maps.ToLower(params) + + l := &Language{Lang: lang, ContentDir: cfg.GetString("contentDir"), Cfg: cfg, params: params, settings: make(map[string]interface{})} + return l +} + +// NewDefaultLanguage creates the default language for a config.Provider. +// If not otherwise specified the default is "en". +func NewDefaultLanguage(cfg config.Provider) *Language { + defaultLang := cfg.GetString("defaultContentLanguage") + + if defaultLang == "" { + defaultLang = "en" + } + + return NewLanguage(defaultLang, cfg) +} + +// Languages is a sortable list of languages. +type Languages []*Language + +// NewLanguages creates a sorted list of languages. +// NOTE: function is currently unused. +func NewLanguages(l ...*Language) Languages { + languages := make(Languages, len(l)) + for i := 0; i < len(l); i++ { + languages[i] = l[i] + } + sort.Sort(languages) + return languages +} + +func (l Languages) Len() int { return len(l) } +func (l Languages) Less(i, j int) bool { + wi, wj := l[i].Weight, l[j].Weight + + if wi == wj { + return l[i].Lang < l[j].Lang + } + + return wj == 0 || wi < wj + +} + +func (l Languages) Swap(i, j int) { l[i], l[j] = l[j], l[i] } + +// Params retunrs language-specific params merged with the global params. +func (l *Language) Params() maps.Params { + // TODO(bep) this construct should not be needed. Create the + // language params in one go. + l.paramsMu.Lock() + defer l.paramsMu.Unlock() + if !l.paramsSet { + maps.ToLower(l.params) + l.paramsSet = true + } + return l.params +} + +func (l Languages) AsSet() map[string]bool { + m := make(map[string]bool) + for _, lang := range l { + m[lang.Lang] = true + } + + return m +} + +func (l Languages) AsOrdinalSet() map[string]int { + m := make(map[string]int) + for i, lang := range l { + m[lang.Lang] = i + } + + return m +} + +// IsMultihost returns whether there are more than one language and at least one of +// the languages has baseURL specificed on the language level. +func (l Languages) IsMultihost() bool { + if len(l) <= 1 { + return false + } + + for _, lang := range l { + if lang.GetLocal("baseURL") != nil { + return true + } + } + return false +} + +// SetParam sets a param with the given key and value. +// SetParam is case-insensitive. +func (l *Language) SetParam(k string, v interface{}) { + l.paramsMu.Lock() + defer l.paramsMu.Unlock() + if l.paramsSet { + panic("params cannot be changed once set") + } + l.params[k] = v +} + +// |