language.go (8496B)
1 // Copyright 2018 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 14 package langs 15 16 import ( 17 "fmt" 18 "sort" 19 "strings" 20 "sync" 21 "time" 22 23 "golang.org/x/text/collate" 24 "golang.org/x/text/language" 25 26 "github.com/gohugoio/hugo/common/htime" 27 "github.com/gohugoio/hugo/common/maps" 28 "github.com/gohugoio/hugo/config" 29 "github.com/gohugoio/locales" 30 translators "github.com/gohugoio/localescompressed" 31 ) 32 33 // These are the settings that should only be looked up in the global Viper 34 // config and not per language. 35 // This list may not be complete, but contains only settings that we know 36 // will be looked up in both. 37 // This isn't perfect, but it is ultimately the user who shoots him/herself in 38 // the foot. 39 // See the pathSpec. 40 var globalOnlySettings = map[string]bool{ 41 strings.ToLower("defaultContentLanguageInSubdir"): true, 42 strings.ToLower("defaultContentLanguage"): true, 43 strings.ToLower("multilingual"): true, 44 strings.ToLower("assetDir"): true, 45 strings.ToLower("resourceDir"): true, 46 strings.ToLower("build"): true, 47 } 48 49 // Language manages specific-language configuration. 50 type Language struct { 51 Lang string 52 LanguageName string 53 LanguageDirection string 54 Title string 55 Weight int 56 57 // For internal use. 58 Disabled bool 59 60 // If set per language, this tells Hugo that all content files without any 61 // language indicator (e.g. my-page.en.md) is in this language. 62 // This is usually a path relative to the working dir, but it can be an 63 // absolute directory reference. It is what we get. 64 // For internal use. 65 ContentDir string 66 67 // Global config. 68 // For internal use. 69 Cfg config.Provider 70 71 // Language specific config. 72 // For internal use. 73 LocalCfg config.Provider 74 75 // Composite config. 76 // For internal use. 77 config.Provider 78 79 // These are params declared in the [params] section of the language merged with the 80 // site's params, the most specific (language) wins on duplicate keys. 81 params map[string]any 82 paramsMu sync.Mutex 83 paramsSet bool 84 85 // Used for date formatting etc. We don't want these exported to the 86 // templates. 87 // TODO(bep) do the same for some of the others. 88 translator locales.Translator 89 timeFormatter htime.TimeFormatter 90 tag language.Tag 91 collator *Collator 92 location *time.Location 93 94 // Error during initialization. Will fail the buld. 95 initErr error 96 } 97 98 // For internal use. 99 func (l *Language) String() string { 100 return l.Lang 101 } 102 103 // NewLanguage creates a new language. 104 func NewLanguage(lang string, cfg config.Provider) *Language { 105 // Note that language specific params will be overridden later. 106 // We should improve that, but we need to make a copy: 107 params := make(map[string]any) 108 for k, v := range cfg.GetStringMap("params") { 109 params[k] = v 110 } 111 maps.PrepareParams(params) 112 113 localCfg := config.New() 114 compositeConfig := config.NewCompositeConfig(cfg, localCfg) 115 translator := translators.GetTranslator(lang) 116 if translator == nil { 117 translator = translators.GetTranslator(cfg.GetString("defaultContentLanguage")) 118 if translator == nil { 119 translator = translators.GetTranslator("en") 120 } 121 } 122 123 var coll *Collator 124 tag, err := language.Parse(lang) 125 if err == nil { 126 coll = &Collator{ 127 c: collate.New(tag), 128 } 129 } else { 130 coll = &Collator{ 131 c: collate.New(language.English), 132 } 133 } 134 135 l := &Language{ 136 Lang: lang, 137 ContentDir: cfg.GetString("contentDir"), 138 Cfg: cfg, LocalCfg: localCfg, 139 Provider: compositeConfig, 140 params: params, 141 translator: translator, 142 timeFormatter: htime.NewTimeFormatter(translator), 143 tag: tag, 144 collator: coll, 145 } 146 147 if err := l.loadLocation(cfg.GetString("timeZone")); err != nil { 148 l.initErr = err 149 } 150 151 return l 152 } 153 154 // NewDefaultLanguage creates the default language for a config.Provider. 155 // If not otherwise specified the default is "en". 156 func NewDefaultLanguage(cfg config.Provider) *Language { 157 defaultLang := cfg.GetString("defaultContentLanguage") 158 159 if defaultLang == "" { 160 defaultLang = "en" 161 } 162 163 return NewLanguage(defaultLang, cfg) 164 } 165 166 // Languages is a sortable list of languages. 167 type Languages []*Language 168 169 // NewLanguages creates a sorted list of languages. 170 // NOTE: function is currently unused. 171 func NewLanguages(l ...*Language) Languages { 172 languages := make(Languages, len(l)) 173 for i := 0; i < len(l); i++ { 174 languages[i] = l[i] 175 } 176 sort.Sort(languages) 177 return languages 178 } 179 180 func (l Languages) Len() int { return len(l) } 181 func (l Languages) Less(i, j int) bool { 182 wi, wj := l[i].Weight, l[j].Weight 183 184 if wi == wj { 185 return l[i].Lang < l[j].Lang 186 } 187 188 return wj == 0 || wi < wj 189 } 190 191 func (l Languages) Swap(i, j int) { l[i], l[j] = l[j], l[i] } 192 193 // Params returns language-specific params merged with the global params. 194 func (l *Language) Params() maps.Params { 195 // TODO(bep) this construct should not be needed. Create the 196 // language params in one go. 197 l.paramsMu.Lock() 198 defer l.paramsMu.Unlock() 199 if !l.paramsSet { 200 maps.PrepareParams(l.params) 201 l.paramsSet = true 202 } 203 return l.params 204 } 205 206 func (l Languages) AsSet() map[string]bool { 207 m := make(map[string]bool) 208 for _, lang := range l { 209 m[lang.Lang] = true 210 } 211 212 return m 213 } 214 215 func (l Languages) AsOrdinalSet() map[string]int { 216 m := make(map[string]int) 217 for i, lang := range l { 218 m[lang.Lang] = i 219 } 220 221 return m 222 } 223 224 // IsMultihost returns whether there are more than one language and at least one of 225 // the languages has baseURL specificed on the language level. 226 func (l Languages) IsMultihost() bool { 227 if len(l) <= 1 { 228 return false 229 } 230 231 for _, lang := range l { 232 if lang.GetLocal("baseURL") != nil { 233 return true 234 } 235 } 236 return false 237 } 238 239 // SetParam sets a param with the given key and value. 240 // SetParam is case-insensitive. 241 // For internal use. 242 func (l *Language) SetParam(k string, v any) { 243 l.paramsMu.Lock() 244 defer l.paramsMu.Unlock() 245 if l.paramsSet { 246 panic("params cannot be changed once set") 247 } 248 l.params[k] = v 249 } 250 251 // GetLocal gets a configuration value set on language level. It will 252 // not fall back to any global value. 253 // It will return nil if a value with the given key cannot be found. 254 // For internal use. 255 func (l *Language) GetLocal(key string) any { 256 if l == nil { 257 panic("language not set") 258 } 259 key = strings.ToLower(key) 260 if !globalOnlySettings[key] { 261 return l.LocalCfg.Get(key) 262 } 263 return nil 264 } 265 266 // For internal use. 267 func (l *Language) Set(k string, v any) { 268 k = strings.ToLower(k) 269 if globalOnlySettings[k] { 270 return 271 } 272 l.Provider.Set(k, v) 273 } 274 275 // Merge is currently not supported for Language. 276 // For internal use. 277 func (l *Language) Merge(key string, value any) { 278 panic("Not supported") 279 } 280 281 // IsSet checks whether the key is set in the language or the related config store. 282 // For internal use. 283 func (l *Language) IsSet(key string) bool { 284 key = strings.ToLower(key) 285 if !globalOnlySettings[key] { 286 return l.Provider.IsSet(key) 287 } 288 return l.Cfg.IsSet(key) 289 } 290 291 // Internal access to unexported Language fields. 292 // This construct is to prevent them from leaking to the templates. 293 294 func GetTimeFormatter(l *Language) htime.TimeFormatter { 295 return l.timeFormatter 296 } 297 298 func GetTranslator(l *Language) locales.Translator { 299 return l.translator 300 } 301 302 func GetLocation(l *Language) *time.Location { 303 return l.location 304 } 305 306 func GetCollator(l *Language) *Collator { 307 return l.collator 308 } 309 310 func (l *Language) loadLocation(tzStr string) error { 311 location, err := time.LoadLocation(tzStr) 312 if err != nil { 313 return fmt.Errorf("invalid timeZone for language %q: %w", l.Lang, err) 314 } 315 l.location = location 316 317 return nil 318 } 319 320 type Collator struct { 321 sync.Mutex 322 c *collate.Collator 323 } 324 325 // CompareStrings compares a and b. 326 // It returns -1 if a < b, 1 if a > b and 0 if a == b. 327 // Note that the Collator is not thread safe, so you may want 328 // to aquire a lock on it before calling this method. 329 func (c *Collator) CompareStrings(a, b string) int { 330 return c.c.CompareString(a, b) 331 }