mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-07 20:30:36 -05:00
f2946da9e8
* Add file context to minifier errors when publishing * Misc fixes (see issues) * Allow custom server error template in layouts/server/error.html To get to this, this commit also cleans up and simplifies the code surrounding errors and files. This also removes the usage of `github.com/pkg/errors`, mostly because of https://github.com/pkg/errors/issues/223 -- but also because most of this is now built-in to Go. Fixes #9852 Fixes #9857 Fixes #9863
331 lines
8.3 KiB
Go
331 lines
8.3 KiB
Go
// 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"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"golang.org/x/text/collate"
|
|
"golang.org/x/text/language"
|
|
|
|
"github.com/gohugoio/hugo/common/htime"
|
|
"github.com/gohugoio/hugo/common/maps"
|
|
"github.com/gohugoio/hugo/config"
|
|
"github.com/gohugoio/locales"
|
|
translators "github.com/gohugoio/localescompressed"
|
|
)
|
|
|
|
// 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,
|
|
strings.ToLower("build"): true,
|
|
}
|
|
|
|
// Language manages specific-language configuration.
|
|
type Language struct {
|
|
Lang string
|
|
LanguageName string
|
|
LanguageDirection string
|
|
Title string
|
|
Weight int
|
|
|
|
// For internal use.
|
|
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.
|
|
// For internal use.
|
|
ContentDir string
|
|
|
|
// Global config.
|
|
// For internal use.
|
|
Cfg config.Provider
|
|
|
|
// Language specific config.
|
|
// For internal use.
|
|
LocalCfg config.Provider
|
|
|
|
// Composite config.
|
|
// For internal use.
|
|
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]any
|
|
paramsMu sync.Mutex
|
|
paramsSet bool
|
|
|
|
// Used for date formatting etc. We don't want these exported to the
|
|
// templates.
|
|
// TODO(bep) do the same for some of the others.
|
|
translator locales.Translator
|
|
timeFormatter htime.TimeFormatter
|
|
tag language.Tag
|
|
collator *Collator
|
|
location *time.Location
|
|
|
|
// Error during initialization. Will fail the buld.
|
|
initErr error
|
|
}
|
|
|
|
// For internal use.
|
|
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]any)
|
|
for k, v := range cfg.GetStringMap("params") {
|
|
params[k] = v
|
|
}
|
|
maps.PrepareParams(params)
|
|
|
|
localCfg := config.New()
|
|
compositeConfig := config.NewCompositeConfig(cfg, localCfg)
|
|
translator := translators.GetTranslator(lang)
|
|
if translator == nil {
|
|
translator = translators.GetTranslator(cfg.GetString("defaultContentLanguage"))
|
|
if translator == nil {
|
|
translator = translators.GetTranslator("en")
|
|
}
|
|
}
|
|
|
|
var coll *Collator
|
|
tag, err := language.Parse(lang)
|
|
if err == nil {
|
|
coll = &Collator{
|
|
c: collate.New(tag),
|
|
}
|
|
} else {
|
|
coll = &Collator{
|
|
c: collate.New(language.English),
|
|
}
|
|
}
|
|
|
|
l := &Language{
|
|
Lang: lang,
|
|
ContentDir: cfg.GetString("contentDir"),
|
|
Cfg: cfg, LocalCfg: localCfg,
|
|
Provider: compositeConfig,
|
|
params: params,
|
|
translator: translator,
|
|
timeFormatter: htime.NewTimeFormatter(translator),
|
|
tag: tag,
|
|
collator: coll,
|
|
}
|
|
|
|
if err := l.loadLocation(cfg.GetString("timeZone")); err != nil {
|
|
l.initErr = err
|
|
}
|
|
|
|
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 returns 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.PrepareParams(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.
|
|
// For internal use.
|
|
func (l *Language) SetParam(k string, v any) {
|
|
l.paramsMu.Lock()
|
|
defer l.paramsMu.Unlock()
|
|
if l.paramsSet {
|
|
panic("params cannot be changed once set")
|
|
}
|
|
l.params[k] = v
|
|
}
|
|
|
|
// GetLocal gets a configuration value set on language level. It will
|
|
// not fall back to any global value.
|
|
// It will return nil if a value with the given key cannot be found.
|
|
// For internal use.
|
|
func (l *Language) GetLocal(key string) any {
|
|
if l == nil {
|
|
panic("language not set")
|
|
}
|
|
key = strings.ToLower(key)
|
|
if !globalOnlySettings[key] {
|
|
return l.LocalCfg.Get(key)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// For internal use.
|
|
func (l *Language) Set(k string, v any) {
|
|
k = strings.ToLower(k)
|
|
if globalOnlySettings[k] {
|
|
return
|
|
}
|
|
l.Provider.Set(k, v)
|
|
}
|
|
|
|
// Merge is currently not supported for Language.
|
|
// For internal use.
|
|
func (l *Language) Merge(key string, value any) {
|
|
panic("Not supported")
|
|
}
|
|
|
|
// IsSet checks whether the key is set in the language or the related config store.
|
|
// For internal use.
|
|
func (l *Language) IsSet(key string) bool {
|
|
key = strings.ToLower(key)
|
|
if !globalOnlySettings[key] {
|
|
return l.Provider.IsSet(key)
|
|
}
|
|
return l.Cfg.IsSet(key)
|
|
}
|
|
|
|
// Internal access to unexported Language fields.
|
|
// This construct is to prevent them from leaking to the templates.
|
|
|
|
func GetTimeFormatter(l *Language) htime.TimeFormatter {
|
|
return l.timeFormatter
|
|
}
|
|
|
|
func GetTranslator(l *Language) locales.Translator {
|
|
return l.translator
|
|
}
|
|
|
|
func GetLocation(l *Language) *time.Location {
|
|
return l.location
|
|
}
|
|
|
|
func GetCollator(l *Language) *Collator {
|
|
return l.collator
|
|
}
|
|
|
|
func (l *Language) loadLocation(tzStr string) error {
|
|
location, err := time.LoadLocation(tzStr)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid timeZone for language %q: %w", l.Lang, err)
|
|
}
|
|
l.location = location
|
|
|
|
return nil
|
|
}
|
|
|
|
type Collator struct {
|
|
sync.Mutex
|
|
c *collate.Collator
|
|
}
|
|
|
|
// CompareStrings compares a and b.
|
|
// It returns -1 if a < b, 1 if a > b and 0 if a == b.
|
|
// Note that the Collator is not thread safe, so you may want
|
|
// to aquire a lock on it before calling this method.
|
|
func (c *Collator) CompareStrings(a, b string) int {
|
|
return c.c.CompareString(a, b)
|
|
}
|