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 }