Avoid reading from Viper for path and URL funcs

The gain, given the "real sites benchmark" below, is obvious:

```
benchmark           old ns/op       new ns/op       delta
BenchmarkHugo-4     14497594101     13084156335     -9.75%

benchmark           old allocs     new allocs     delta
BenchmarkHugo-4     57404335       48282002       -15.89%

benchmark           old bytes       new bytes      delta
BenchmarkHugo-4     9933505624      9721984424     -2.13%
```

Fixes #2495
This commit is contained in:
Bjørn Erik Pedersen 2016-10-24 13:45:30 +02:00 committed by GitHub
parent dffd7da07c
commit a10b2cd372
26 changed files with 348 additions and 90 deletions

View file

@ -21,16 +21,55 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
) )
// A cached version of the current ConfigProvider (language) and relatives. These globals
// are unfortunate, but we still have some places that needs this that does
// not have access to the site configuration.
// These values will be set on initialization when rendering a new language.
//
// TODO(bep) Get rid of these.
var (
currentConfigProvider ConfigProvider
currentPathSpec *PathSpec
)
// ConfigProvider provides the configuration settings for Hugo. // ConfigProvider provides the configuration settings for Hugo.
type ConfigProvider interface { type ConfigProvider interface {
GetString(key string) string GetString(key string) string
GetInt(key string) int GetInt(key string) int
GetBool(key string) bool
GetStringMap(key string) map[string]interface{} GetStringMap(key string) map[string]interface{}
GetStringMapString(key string) map[string]string GetStringMapString(key string) map[string]string
Get(key string) interface{}
} }
// Config returns the currently active Hugo config. This will be set // Config returns the currently active Hugo config. This will be set
// per site (language) rendered. // per site (language) rendered.
func Config() ConfigProvider { func Config() ConfigProvider {
if currentConfigProvider != nil {
return currentConfigProvider
}
// Some tests rely on this. We will fix that, eventually.
return viper.Get("CurrentContentLanguage").(ConfigProvider) return viper.Get("CurrentContentLanguage").(ConfigProvider)
} }
// CurrentPathSpec returns the current PathSpec.
// If it is not set, a new will be created based in the currently active Hugo config.
func CurrentPathSpec() *PathSpec {
if currentPathSpec != nil {
return currentPathSpec
}
// Some tests rely on this. We will fix that, eventually.
return NewPathSpecFromConfig(Config())
}
// InitConfigProviderForCurrentContentLanguage does what it says.
func InitConfigProviderForCurrentContentLanguage() {
currentConfigProvider = viper.Get("CurrentContentLanguage").(ConfigProvider)
currentPathSpec = NewPathSpecFromConfig(currentConfigProvider)
}
// ResetConfigProvider is used in tests.
func ResetConfigProvider() {
currentConfigProvider = nil
currentPathSpec = nil
}

View file

@ -23,6 +23,19 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
) )
// 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,
}
type Language struct { type Language struct {
Lang string Lang string
LanguageName string LanguageName string
@ -81,7 +94,7 @@ func (l *Language) Params() map[string]interface{} {
} }
func (l *Language) SetParam(k string, v interface{}) { func (l *Language) SetParam(k string, v interface{}) {
l.params[k] = v l.params[strings.ToLower(k)] = v
} }
func (l *Language) GetBool(key string) bool { return cast.ToBool(l.Get(key)) } func (l *Language) GetBool(key string) bool { return cast.ToBool(l.Get(key)) }
@ -101,8 +114,10 @@ func (l *Language) Get(key string) interface{} {
panic("language not set") panic("language not set")
} }
key = strings.ToLower(key) key = strings.ToLower(key)
if v, ok := l.params[key]; ok { if !globalOnlySettings[key] {
return v if v, ok := l.params[key]; ok {
return v
}
} }
return viper.Get(key) return viper.Get(key)
} }

32
helpers/language_test.go Normal file
View file

@ -0,0 +1,32 @@
// Copyright 2016-present 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 helpers
import (
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
)
func TestGetGlobalOnlySetting(t *testing.T) {
lang := NewDefaultLanguage()
lang.SetParam("defaultContentLanguageInSubdir", false)
lang.SetParam("paginatePath", "side")
viper.Set("defaultContentLanguageInSubdir", true)
viper.Set("paginatePath", "page")
require.True(t, lang.GetBool("defaultContentLanguageInSubdir"))
require.Equal(t, "side", lang.GetString("paginatePath"))
}

View file

@ -75,16 +75,16 @@ var fpb filepathBridge
// It does so by creating a Unicode-sanitized string, with the spaces replaced, // It does so by creating a Unicode-sanitized string, with the spaces replaced,
// whilst preserving the original casing of the string. // whilst preserving the original casing of the string.
// E.g. Social Media -> Social-Media // E.g. Social Media -> Social-Media
func MakePath(s string) string { func (p *PathSpec) MakePath(s string) string {
return UnicodeSanitize(strings.Replace(strings.TrimSpace(s), " ", "-", -1)) return p.UnicodeSanitize(strings.Replace(strings.TrimSpace(s), " ", "-", -1))
} }
// MakePathSanitized creates a Unicode-sanitized string, with the spaces replaced // MakePathSanitized creates a Unicode-sanitized string, with the spaces replaced
func MakePathSanitized(s string) string { func (p *PathSpec) MakePathSanitized(s string) string {
if viper.GetBool("DisablePathToLower") { if p.disablePathToLower {
return MakePath(s) return p.MakePath(s)
} }
return strings.ToLower(MakePath(s)) return strings.ToLower(p.MakePath(s))
} }
// MakeTitle converts the path given to a suitable title, trimming whitespace // MakeTitle converts the path given to a suitable title, trimming whitespace
@ -110,7 +110,7 @@ func ishex(c rune) bool {
// a predefined set of special Unicode characters. // a predefined set of special Unicode characters.
// If RemovePathAccents configuration flag is enabled, Uniccode accents // If RemovePathAccents configuration flag is enabled, Uniccode accents
// are also removed. // are also removed.
func UnicodeSanitize(s string) string { func (p *PathSpec) UnicodeSanitize(s string) string {
source := []rune(s) source := []rune(s)
target := make([]rune, 0, len(source)) target := make([]rune, 0, len(source))
@ -124,7 +124,7 @@ func UnicodeSanitize(s string) string {
var result string var result string
if viper.GetBool("RemovePathAccents") { if p.removePathAccents {
// remove accents - see https://blog.golang.org/normalization // remove accents - see https://blog.golang.org/normalization
t := transform.Chain(norm.NFD, transform.RemoveFunc(isMn), norm.NFC) t := transform.Chain(norm.NFD, transform.RemoveFunc(isMn), norm.NFC)
result, _, _ = transform.String(t, string(target)) result, _, _ = transform.String(t, string(target))

View file

@ -33,9 +33,14 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
) )
func initCommonTestConfig() {
viper.Set("CurrentContentLanguage", NewLanguage("en"))
}
func TestMakePath(t *testing.T) { func TestMakePath(t *testing.T) {
viper.Reset() viper.Reset()
defer viper.Reset() defer viper.Reset()
initCommonTestConfig()
tests := []struct { tests := []struct {
input string input string
@ -57,7 +62,8 @@ func TestMakePath(t *testing.T) {
for _, test := range tests { for _, test := range tests {
viper.Set("RemovePathAccents", test.removeAccents) viper.Set("RemovePathAccents", test.removeAccents)
output := MakePath(test.input) p := NewPathSpecFromConfig(viper.GetViper())
output := p.MakePath(test.input)
if output != test.expected { if output != test.expected {
t.Errorf("Expected %#v, got %#v\n", test.expected, output) t.Errorf("Expected %#v, got %#v\n", test.expected, output)
} }
@ -67,6 +73,9 @@ func TestMakePath(t *testing.T) {
func TestMakePathSanitized(t *testing.T) { func TestMakePathSanitized(t *testing.T) {
viper.Reset() viper.Reset()
defer viper.Reset() defer viper.Reset()
initCommonTestConfig()
p := NewPathSpecFromConfig(viper.GetViper())
tests := []struct { tests := []struct {
input string input string
@ -81,7 +90,7 @@ func TestMakePathSanitized(t *testing.T) {
} }
for _, test := range tests { for _, test := range tests {
output := MakePathSanitized(test.input) output := p.MakePathSanitized(test.input)
if output != test.expected { if output != test.expected {
t.Errorf("Expected %#v, got %#v\n", test.expected, output) t.Errorf("Expected %#v, got %#v\n", test.expected, output)
} }
@ -91,7 +100,10 @@ func TestMakePathSanitized(t *testing.T) {
func TestMakePathSanitizedDisablePathToLower(t *testing.T) { func TestMakePathSanitizedDisablePathToLower(t *testing.T) {
viper.Reset() viper.Reset()
defer viper.Reset() defer viper.Reset()
initCommonTestConfig()
viper.Set("DisablePathToLower", true) viper.Set("DisablePathToLower", true)
p := NewPathSpecFromConfig(viper.GetViper())
tests := []struct { tests := []struct {
input string input string
@ -106,7 +118,7 @@ func TestMakePathSanitizedDisablePathToLower(t *testing.T) {
} }
for _, test := range tests { for _, test := range tests {
output := MakePathSanitized(test.input) output := p.MakePathSanitized(test.input)
if output != test.expected { if output != test.expected {
t.Errorf("Expected %#v, got %#v\n", test.expected, output) t.Errorf("Expected %#v, got %#v\n", test.expected, output)
} }

54
helpers/pathspec.go Normal file
View file

@ -0,0 +1,54 @@
// Copyright 2016-present 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 helpers
// PathSpec holds methods that decides how paths in URLs and files in Hugo should look like.
type PathSpec struct {
disablePathToLower bool
removePathAccents bool
uglyURLs bool
canonifyURLs bool
currentContentLanguage *Language
// pagination path handling
paginatePath string
// The PathSpec looks up its config settings in both the current language
// and then in the global Viper config.
// Some settings, the settings listed below, does not make sense to be set
// on per-language-basis. We have no good way of protecting against this
// other than a "white-list". See language.go.
defaultContentLanguageInSubdir bool
defaultContentLanguage string
multilingual bool
}
func NewPathSpecFromConfig(config ConfigProvider) *PathSpec {
return &PathSpec{
disablePathToLower: config.GetBool("disablePathToLower"),
removePathAccents: config.GetBool("removePathAccents"),
uglyURLs: config.GetBool("uglyURLs"),
canonifyURLs: config.GetBool("canonifyURLs"),
multilingual: config.GetBool("multilingual"),
defaultContentLanguageInSubdir: config.GetBool("defaultContentLanguageInSubdir"),
defaultContentLanguage: config.GetString("defaultContentLanguage"),
currentContentLanguage: config.Get("currentContentLanguage").(*Language),
paginatePath: config.GetString("paginatePath"),
}
}
func (p *PathSpec) PaginatePath() string {
return p.paginatePath
}

45
helpers/pathspec_test.go Normal file
View file

@ -0,0 +1,45 @@
// Copyright 2016-present 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 helpers
import (
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
)
func TestNewPathSpecFromConfig(t *testing.T) {
viper.Set("disablePathToLower", true)
viper.Set("removePathAccents", true)
viper.Set("uglyURLs", true)
viper.Set("multilingual", true)
viper.Set("defaultContentLanguageInSubdir", true)
viper.Set("defaultContentLanguage", "no")
viper.Set("currentContentLanguage", NewLanguage("no"))
viper.Set("canonifyURLs", true)
viper.Set("paginatePath", "side")
pathSpec := NewPathSpecFromConfig(viper.GetViper())
require.True(t, pathSpec.canonifyURLs)
require.True(t, pathSpec.defaultContentLanguageInSubdir)
require.True(t, pathSpec.disablePathToLower)
require.True(t, pathSpec.multilingual)
require.True(t, pathSpec.removePathAccents)
require.True(t, pathSpec.uglyURLs)
require.Equal(t, "no", pathSpec.defaultContentLanguage)
require.Equal(t, "no", pathSpec.currentContentLanguage.Lang)
require.Equal(t, "side", pathSpec.paginatePath)
}

View file

@ -101,8 +101,8 @@ func SanitizeURLKeepTrailingSlash(in string) string {
// Example: // Example:
// uri: Vim (text editor) // uri: Vim (text editor)
// urlize: vim-text-editor // urlize: vim-text-editor
func URLize(uri string) string { func (p *PathSpec) URLize(uri string) string {
sanitized := MakePathSanitized(uri) sanitized := p.MakePathSanitized(uri)
// escape unicode letters // escape unicode letters
parsedURI, err := url.Parse(sanitized) parsedURI, err := url.Parse(sanitized)
@ -147,7 +147,7 @@ func MakePermalink(host, plink string) *url.URL {
} }
// AbsURL creates a absolute URL from the relative path given and the BaseURL set in config. // AbsURL creates a absolute URL from the relative path given and the BaseURL set in config.
func AbsURL(in string, addLanguage bool) string { func (p *PathSpec) AbsURL(in string, addLanguage bool) string {
url, err := url.Parse(in) url, err := url.Parse(in)
if err != nil { if err != nil {
return in return in
@ -168,7 +168,7 @@ func AbsURL(in string, addLanguage bool) string {
} }
if addLanguage { if addLanguage {
prefix := getLanguagePrefix() prefix := p.getLanguagePrefix()
if prefix != "" { if prefix != "" {
hasPrefix := false hasPrefix := false
// avoid adding language prefix if already present // avoid adding language prefix if already present
@ -191,15 +191,15 @@ func AbsURL(in string, addLanguage bool) string {
return MakePermalink(baseURL, in).String() return MakePermalink(baseURL, in).String()
} }
func getLanguagePrefix() string { func (p *PathSpec) getLanguagePrefix() string {
if !viper.GetBool("Multilingual") { if !p.multilingual {
return "" return ""
} }
defaultLang := viper.GetString("DefaultContentLanguage") defaultLang := p.defaultContentLanguage
defaultInSubDir := viper.GetBool("DefaultContentLanguageInSubdir") defaultInSubDir := p.defaultContentLanguageInSubdir
currentLang := viper.Get("CurrentContentLanguage").(*Language).Lang currentLang := p.currentContentLanguage.Lang
if currentLang == "" || (currentLang == defaultLang && !defaultInSubDir) { if currentLang == "" || (currentLang == defaultLang && !defaultInSubDir) {
return "" return ""
} }
@ -218,9 +218,9 @@ func IsAbsURL(path string) bool {
// RelURL creates a URL relative to the BaseURL root. // RelURL creates a URL relative to the BaseURL root.
// Note: The result URL will not include the context root if canonifyURLs is enabled. // Note: The result URL will not include the context root if canonifyURLs is enabled.
func RelURL(in string, addLanguage bool) string { func (p *PathSpec) RelURL(in string, addLanguage bool) string {
baseURL := viper.GetString("BaseURL") baseURL := viper.GetString("BaseURL")
canonifyURLs := viper.GetBool("canonifyURLs") canonifyURLs := p.canonifyURLs
if (!strings.HasPrefix(in, baseURL) && strings.HasPrefix(in, "http")) || strings.HasPrefix(in, "//") { if (!strings.HasPrefix(in, baseURL) && strings.HasPrefix(in, "http")) || strings.HasPrefix(in, "//") {
return in return in
} }
@ -232,7 +232,7 @@ func RelURL(in string, addLanguage bool) string {
} }
if addLanguage { if addLanguage {
prefix := getLanguagePrefix() prefix := p.getLanguagePrefix()
if prefix != "" { if prefix != "" {
hasPrefix := false hasPrefix := false
// avoid adding language prefix if already present // avoid adding language prefix if already present
@ -288,8 +288,8 @@ func AddContextRoot(baseURL, relativePath string) string {
return newPath return newPath
} }
func URLizeAndPrep(in string) string { func (p *PathSpec) URLizeAndPrep(in string) string {
return URLPrep(viper.GetBool("UglyURLs"), URLize(in)) return URLPrep(p.uglyURLs, p.URLize(in))
} }
func URLPrep(ugly bool, in string) string { func URLPrep(ugly bool, in string) string {

View file

@ -24,6 +24,10 @@ import (
) )
func TestURLize(t *testing.T) { func TestURLize(t *testing.T) {
initCommonTestConfig()
p := NewPathSpecFromConfig(viper.GetViper())
tests := []struct { tests := []struct {
input string input string
expected string expected string
@ -37,7 +41,7 @@ func TestURLize(t *testing.T) {
} }
for _, test := range tests { for _, test := range tests {
output := URLize(test.input) output := p.URLize(test.input)
if output != test.expected { if output != test.expected {
t.Errorf("Expected %#v, got %#v\n", test.expected, output) t.Errorf("Expected %#v, got %#v\n", test.expected, output)
} }
@ -83,7 +87,8 @@ func doTestAbsURL(t *testing.T, defaultInSubDir, addLanguage, multilingual bool,
for _, test := range tests { for _, test := range tests {
viper.Set("BaseURL", test.baseURL) viper.Set("BaseURL", test.baseURL)
output := AbsURL(test.input, addLanguage) p := NewPathSpecFromConfig(viper.GetViper())
output := p.AbsURL(test.input, addLanguage)
expected := test.expected expected := test.expected
if multilingual && addLanguage { if multilingual && addLanguage {
if !defaultInSubDir && lang == "en" { if !defaultInSubDir && lang == "en" {
@ -159,8 +164,9 @@ func doTestRelURL(t *testing.T, defaultInSubDir, addLanguage, multilingual bool,
for i, test := range tests { for i, test := range tests {
viper.Set("BaseURL", test.baseURL) viper.Set("BaseURL", test.baseURL)
viper.Set("canonifyURLs", test.canonify) viper.Set("canonifyURLs", test.canonify)
p := NewPathSpecFromConfig(viper.GetViper())
output := RelURL(test.input, addLanguage) output := p.RelURL(test.input, addLanguage)
expected := test.expected expected := test.expected
if multilingual && addLanguage { if multilingual && addLanguage {

View file

@ -35,6 +35,7 @@ func testCommonResetState() {
hugofs.InitMemFs() hugofs.InitMemFs()
viper.Reset() viper.Reset()
viper.SetFs(hugofs.Source()) viper.SetFs(hugofs.Source())
helpers.ResetConfigProvider()
loadDefaultSettings() loadDefaultSettings()
// Default is false, but true is easier to use as default in tests // Default is false, but true is easier to use as default in tests

View file

@ -178,7 +178,7 @@ func (n *Node) URL() string {
} }
func (n *Node) Permalink() string { func (n *Node) Permalink() string {
return permalink(n.URL()) return n.Site.permalink(n.URL())
} }
// Scratch returns the writable context associated with this Node. // Scratch returns the writable context associated with this Node.

View file

@ -569,9 +569,9 @@ func (p *Page) analyzePage() {
func (p *Page) permalink() (*url.URL, error) { func (p *Page) permalink() (*url.URL, error) {
baseURL := string(p.Site.BaseURL) baseURL := string(p.Site.BaseURL)
dir := strings.TrimSpace(helpers.MakePath(filepath.ToSlash(strings.ToLower(p.Source.Dir())))) dir := strings.TrimSpace(p.Site.pathSpec.MakePath(filepath.ToSlash(strings.ToLower(p.Source.Dir()))))
pSlug := strings.TrimSpace(helpers.URLize(p.Slug)) pSlug := strings.TrimSpace(p.Site.pathSpec.URLize(p.Slug))
pURL := strings.TrimSpace(helpers.URLize(p.URLPath.URL)) pURL := strings.TrimSpace(p.Site.pathSpec.URLize(p.URLPath.URL))
var permalink string var permalink string
var err error var err error
@ -1171,5 +1171,6 @@ func (p *Page) TargetPath() (outfile string) {
outfile = helpers.ReplaceExtension(p.Source.TranslationBaseName(), p.Extension()) outfile = helpers.ReplaceExtension(p.Source.TranslationBaseName(), p.Extension())
} }
return p.addLangFilepathPrefix(filepath.Join(strings.ToLower(helpers.MakePath(p.Source.Dir())), strings.TrimSpace(outfile))) return p.addLangFilepathPrefix(filepath.Join(strings.ToLower(
p.Site.pathSpec.MakePath(p.Source.Dir())), strings.TrimSpace(outfile)))
} }

View file

@ -20,6 +20,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/spf13/hugo/helpers"
"github.com/spf13/hugo/source" "github.com/spf13/hugo/source"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -134,6 +135,8 @@ func setSortVals(dates [3]time.Time, titles [3]string, weights [3]int, pages Pag
func createSortTestPages(num int) Pages { func createSortTestPages(num int) Pages {
pages := make(Pages, num) pages := make(Pages, num)
info := newSiteInfo(siteBuilderCfg{baseURL: "http://base", language: helpers.NewDefaultLanguage()})
for i := 0; i < num; i++ { for i := 0; i < num; i++ {
pages[i] = &Page{ pages[i] = &Page{
Node: Node{ Node: Node{
@ -141,7 +144,7 @@ func createSortTestPages(num int) Pages {
Section: "z", Section: "z",
URL: fmt.Sprintf("http://base/x/y/p%d.html", i), URL: fmt.Sprintf("http://base/x/y/p%d.html", i),
}, },
Site: newSiteInfoDefaultLanguage("http://base/"), Site: &info,
}, },
Source: Source{File: *source.NewFile(filepath.FromSlash(fmt.Sprintf("/x/y/p%d.md", i)))}, Source: Source{File: *source.NewFile(filepath.FromSlash(fmt.Sprintf("/x/y/p%d.md", i)))},
} }

View file

@ -18,6 +18,7 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/spf13/hugo/helpers"
"github.com/spf13/hugo/source" "github.com/spf13/hugo/source"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
@ -59,17 +60,18 @@ func TestPermalink(t *testing.T) {
} }
viper.Set("DefaultExtension", "html") viper.Set("DefaultExtension", "html")
for i, test := range tests { for i, test := range tests {
viper.Set("uglyurls", test.uglyURLs) viper.Set("uglyurls", test.uglyURLs)
viper.Set("canonifyurls", test.canonifyURLs) viper.Set("canonifyurls", test.canonifyURLs)
info := newSiteInfo(siteBuilderCfg{baseURL: string(test.base), language: helpers.NewDefaultLanguage()})
p := &Page{ p := &Page{
Node: Node{ Node: Node{
URLPath: URLPath{ URLPath: URLPath{
Section: "z", Section: "z",
URL: test.url, URL: test.url,
}, },
Site: newSiteInfoDefaultLanguage(string(test.base)), Site: &info,
}, },
Source: Source{File: *source.NewFile(filepath.FromSlash(test.file))}, Source: Source{File: *source.NewFile(filepath.FromSlash(test.file))},
} }

View file

@ -1122,7 +1122,8 @@ func TestPagePaths(t *testing.T) {
for _, test := range tests { for _, test := range tests {
p, _ := NewPageFrom(strings.NewReader(test.content), filepath.FromSlash(test.path)) p, _ := NewPageFrom(strings.NewReader(test.content), filepath.FromSlash(test.path))
p.Node.Site = newSiteInfoDefaultLanguage("") info := newSiteInfo(siteBuilderCfg{language: helpers.NewDefaultLanguage()})
p.Node.Site = &info
if test.hasPermalink { if test.hasPermalink {
p.Node.Site.Permalinks = siteParmalinksSetting p.Node.Site.Permalinks = siteParmalinksSetting

View file

@ -508,16 +508,16 @@ func newPaginator(elements []paginatedElement, total, size int, urlFactory pagin
} }
func newPaginationURLFactory(pathElements ...string) paginationURLFactory { func newPaginationURLFactory(pathElements ...string) paginationURLFactory {
paginatePath := helpers.Config().GetString("paginatePath") pathSpec := helpers.CurrentPathSpec()
return func(page int) string { return func(page int) string {
var rel string var rel string
if page == 1 { if page == 1 {
rel = fmt.Sprintf("/%s/", path.Join(pathElements...)) rel = fmt.Sprintf("/%s/", path.Join(pathElements...))
} else { } else {
rel = fmt.Sprintf("/%s/%s/%d/", path.Join(pathElements...), paginatePath, page) rel = fmt.Sprintf("/%s/%s/%d/", path.Join(pathElements...), pathSpec.PaginatePath(), page)
} }
return helpers.URLizeAndPrep(rel) return pathSpec.URLizeAndPrep(rel)
} }
} }

View file

@ -19,6 +19,7 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/spf13/hugo/helpers"
"github.com/spf13/hugo/source" "github.com/spf13/hugo/source"
"github.com/spf13/viper" "github.com/spf13/viper"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -453,6 +454,7 @@ func TestPage(t *testing.T) {
func createTestPages(num int) Pages { func createTestPages(num int) Pages {
pages := make(Pages, num) pages := make(Pages, num)
info := newSiteInfo(siteBuilderCfg{baseURL: "http://base/", language: helpers.NewDefaultLanguage()})
for i := 0; i < num; i++ { for i := 0; i < num; i++ {
pages[i] = &Page{ pages[i] = &Page{
Node: Node{ Node: Node{
@ -460,7 +462,7 @@ func createTestPages(num int) Pages {
Section: "z", Section: "z",
URL: fmt.Sprintf("http://base/x/y/p%d.html", i), URL: fmt.Sprintf("http://base/x/y/p%d.html", i),
}, },
Site: newSiteInfoDefaultLanguage("http://base/"), Site: &info,
}, },
Source: Source{File: *source.NewFile(filepath.FromSlash(fmt.Sprintf("/x/y/p%d.md", i)))}, Source: Source{File: *source.NewFile(filepath.FromSlash(fmt.Sprintf("/x/y/p%d.md", i)))},
} }

View file

@ -19,8 +19,6 @@ import (
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"github.com/spf13/hugo/helpers"
) )
// pathPattern represents a string which builds up a URL from attributes // pathPattern represents a string which builds up a URL from attributes
@ -152,14 +150,14 @@ func pageToPermalinkDate(p *Page, dateField string) (string, error) {
func pageToPermalinkTitle(p *Page, _ string) (string, error) { func pageToPermalinkTitle(p *Page, _ string) (string, error) {
// Page contains Node which has Title // Page contains Node which has Title
// (also contains URLPath which has Slug, sometimes) // (also contains URLPath which has Slug, sometimes)
return helpers.URLize(p.Title), nil return p.Site.pathSpec.URLize(p.Title), nil
} }
// pageToPermalinkFilename returns the URL-safe form of the filename // pageToPermalinkFilename returns the URL-safe form of the filename
func pageToPermalinkFilename(p *Page, _ string) (string, error) { func pageToPermalinkFilename(p *Page, _ string) (string, error) {
//var extension = p.Source.Ext //var extension = p.Source.Ext
//var name = p.Source.Path()[0 : len(p.Source.Path())-len(extension)] //var name = p.Source.Path()[0 : len(p.Source.Path())-len(extension)]
return helpers.URLize(p.Source.TranslationBaseName()), nil return p.Site.pathSpec.URLize(p.Source.TranslationBaseName()), nil
} }
// if the page has a slug, return the slug, else return the title // if the page has a slug, return the slug, else return the title
@ -173,7 +171,7 @@ func pageToPermalinkSlugElseTitle(p *Page, a string) (string, error) {
if strings.HasSuffix(p.Slug, "-") { if strings.HasSuffix(p.Slug, "-") {
p.Slug = p.Slug[0 : len(p.Slug)-1] p.Slug = p.Slug[0 : len(p.Slug)-1]
} }
return helpers.URLize(p.Slug), nil return p.Site.pathSpec.URLize(p.Slug), nil
} }
return pageToPermalinkTitle(p, a) return pageToPermalinkTitle(p, a)
} }

View file

@ -16,6 +16,8 @@ package hugolib
import ( import (
"strings" "strings"
"testing" "testing"
"github.com/spf13/hugo/helpers"
) )
// testdataPermalinks is used by a couple of tests; the expandsTo content is // testdataPermalinks is used by a couple of tests; the expandsTo content is
@ -70,6 +72,8 @@ func TestPermalinkValidation(t *testing.T) {
func TestPermalinkExpansion(t *testing.T) { func TestPermalinkExpansion(t *testing.T) {
page, err := NewPageFrom(strings.NewReader(simplePageJSON), "blue/test-page.md") page, err := NewPageFrom(strings.NewReader(simplePageJSON), "blue/test-page.md")
info := newSiteInfo(siteBuilderCfg{language: helpers.NewDefaultLanguage()})
page.Site = &info
if err != nil { if err != nil {
t.Fatalf("failed before we began, could not parse SIMPLE_PAGE_JSON: %s", err) t.Fatalf("failed before we began, could not parse SIMPLE_PAGE_JSON: %s", err)
} }

View file

@ -117,7 +117,8 @@ func (s *Site) reset() *Site {
// newSite creates a new site in the given language. // newSite creates a new site in the given language.
func newSite(lang *helpers.Language) *Site { func newSite(lang *helpers.Language) *Site {
return &Site{Language: lang, Info: SiteInfo{multilingual: newMultiLingualForLanguage(lang)}} return &Site{Language: lang, Info: newSiteInfo(siteBuilderCfg{language: lang})}
} }
// newSite creates a new site in the default language. // newSite creates a new site in the default language.
@ -139,9 +140,12 @@ func newSiteFromSources(pathContentPairs ...string) *Site {
sources = append(sources, source.ByteSource{Name: filepath.FromSlash(path), Content: []byte(content)}) sources = append(sources, source.ByteSource{Name: filepath.FromSlash(path), Content: []byte(content)})
} }
lang := helpers.NewDefaultLanguage()
return &Site{ return &Site{
Source: &source.InMemorySource{ByteSource: sources}, Source: &source.InMemorySource{ByteSource: sources},
Language: helpers.NewDefaultLanguage(), Language: lang,
Info: newSiteInfo(siteBuilderCfg{language: lang}),
} }
} }
@ -195,16 +199,25 @@ type SiteInfo struct {
LanguagePrefix string LanguagePrefix string
Languages helpers.Languages Languages helpers.Languages
defaultContentLanguageInSubdir bool defaultContentLanguageInSubdir bool
pathSpec *helpers.PathSpec
} }
// Used in tests. // Used in tests.
func newSiteInfoDefaultLanguage(baseURL string, pages ...*Page) *SiteInfo {
ps := Pages(pages)
return &SiteInfo{ type siteBuilderCfg struct {
BaseURL: template.URL(baseURL), language *helpers.Language
rawAllPages: &ps, baseURL string
multilingual: newMultiLingualDefaultLanguage(),
pages *Pages
}
func newSiteInfo(cfg siteBuilderCfg) SiteInfo {
return SiteInfo{
BaseURL: template.URL(cfg.baseURL),
rawAllPages: cfg.pages,
pathSpec: helpers.NewPathSpecFromConfig(cfg.language),
multilingual: newMultiLingualForLanguage(cfg.language),
} }
} }
@ -808,7 +821,9 @@ func (s *Site) setCurrentLanguageConfig() error {
// There are sadly some global template funcs etc. that need the language information. // There are sadly some global template funcs etc. that need the language information.
viper.Set("Multilingual", s.multilingualEnabled()) viper.Set("Multilingual", s.multilingualEnabled())
viper.Set("CurrentContentLanguage", s.Language) viper.Set("CurrentContentLanguage", s.Language)
return tpl.SetTranslateLang(s.Language.Lang) // Cache the current config.
helpers.InitConfigProviderForCurrentContentLanguage()
return tpl.SetTranslateLang(s.Language)
} }
func (s *Site) render() (err error) { func (s *Site) render() (err error) {
@ -887,7 +902,7 @@ func (s *SiteInfo) HomeAbsURL() string {
if s.IsMultiLingual() { if s.IsMultiLingual() {
base = s.Language.Lang base = s.Language.Lang
} }
return helpers.AbsURL(base, false) return s.pathSpec.AbsURL(base, false)
} }
// SitemapAbsURL is a convenience method giving the absolute URL to the sitemap. // SitemapAbsURL is a convenience method giving the absolute URL to the sitemap.
@ -946,7 +961,6 @@ func (s *Site) initializeSiteInfo() {
Languages: languages, Languages: languages,
defaultContentLanguageInSubdir: defaultContentInSubDir, defaultContentLanguageInSubdir: defaultContentInSubDir,
GoogleAnalytics: lang.GetString("GoogleAnalytics"), GoogleAnalytics: lang.GetString("GoogleAnalytics"),
RSSLink: permalinkStr(lang.GetString("RSSUri")),
BuildDrafts: viper.GetBool("BuildDrafts"), BuildDrafts: viper.GetBool("BuildDrafts"),
canonifyURLs: viper.GetBool("CanonifyURLs"), canonifyURLs: viper.GetBool("CanonifyURLs"),
preserveTaxonomyNames: lang.GetBool("PreserveTaxonomyNames"), preserveTaxonomyNames: lang.GetBool("PreserveTaxonomyNames"),
@ -959,7 +973,10 @@ func (s *Site) initializeSiteInfo() {
Permalinks: permalinks, Permalinks: permalinks,
Data: &s.Data, Data: &s.Data,
owner: s.owner, owner: s.owner,
pathSpec: helpers.NewPathSpecFromConfig(lang),
} }
s.Info.RSSLink = s.Info.permalinkStr(lang.GetString("RSSUri"))
} }
func (s *Site) hasTheme() bool { func (s *Site) hasTheme() bool {
@ -1407,7 +1424,7 @@ func (s *SiteInfo) createNodeMenuEntryURL(in string) string {
} }
// make it match the nodes // make it match the nodes
menuEntryURL := in menuEntryURL := in
menuEntryURL = helpers.SanitizeURLKeepTrailingSlash(helpers.URLize(menuEntryURL)) menuEntryURL = helpers.SanitizeURLKeepTrailingSlash(s.pathSpec.URLize(menuEntryURL))
if !s.canonifyURLs { if !s.canonifyURLs {
menuEntryURL = helpers.AddContextRoot(string(s.BaseURL), menuEntryURL) menuEntryURL = helpers.AddContextRoot(string(s.BaseURL), menuEntryURL)
} }
@ -1586,13 +1603,13 @@ func (s *Site) renderAliases() error {
if s.owner.multilingual.enabled() { if s.owner.multilingual.enabled() {
mainLang := s.owner.multilingual.DefaultLang.Lang mainLang := s.owner.multilingual.DefaultLang.Lang
if s.Info.defaultContentLanguageInSubdir { if s.Info.defaultContentLanguageInSubdir {
mainLangURL := helpers.AbsURL(mainLang, false) mainLangURL := s.Info.pathSpec.AbsURL(mainLang, false)
jww.DEBUG.Printf("Write redirect to main language %s: %s", mainLang, mainLangURL) jww.DEBUG.Printf("Write redirect to main language %s: %s", mainLang, mainLangURL)
if err := s.publishDestAlias(s.languageAliasTarget(), "/", mainLangURL, nil); err != nil { if err := s.publishDestAlias(s.languageAliasTarget(), "/", mainLangURL, nil); err != nil {
return err return err
} }
} else { } else {
mainLangURL := helpers.AbsURL("", false) mainLangURL := s.Info.pathSpec.AbsURL("", false)
jww.DEBUG.Printf("Write redirect to main language %s: %s", mainLang, mainLangURL) jww.DEBUG.Printf("Write redirect to main language %s: %s", mainLang, mainLangURL)
if err := s.publishDestAlias(s.languageAliasTarget(), mainLang, mainLangURL, nil); err != nil { if err := s.publishDestAlias(s.languageAliasTarget(), mainLang, mainLangURL, nil); err != nil {
return err return err
@ -1763,7 +1780,7 @@ func (s *Site) newTaxonomyNode(prepare bool, t taxRenderInfo, counter int) (*Nod
n := s.nodeLookup(fmt.Sprintf("tax-%s-%s", t.plural, key), counter, prepare) n := s.nodeLookup(fmt.Sprintf("tax-%s-%s", t.plural, key), counter, prepare)
if s.Info.preserveTaxonomyNames { if s.Info.preserveTaxonomyNames {
key = helpers.MakePathSanitized(key) key = s.Info.pathSpec.MakePathSanitized(key)
} }
base := t.plural + "/" + key base := t.plural + "/" + key
@ -1952,7 +1969,7 @@ func (s *Site) renderSectionLists(prepare bool) error {
[]string{"section/" + section + ".html", "_default/section.html", "_default/list.html", "indexes/" + section + ".html", "_default/indexes.html"}) []string{"section/" + section + ".html", "_default/section.html", "_default/list.html", "indexes/" + section + ".html", "_default/indexes.html"})
if s.Info.preserveTaxonomyNames { if s.Info.preserveTaxonomyNames {
section = helpers.MakePathSanitized(section) section = s.Info.pathSpec.MakePathSanitized(section)
} }
base := n.addLangPathPrefix(section) base := n.addLangPathPrefix(section)
@ -1966,7 +1983,7 @@ func (s *Site) renderSectionLists(prepare bool) error {
paginatePath := helpers.Config().GetString("paginatePath") paginatePath := helpers.Config().GetString("paginatePath")
// write alias for page 1 // write alias for page 1
s.writeDestAlias(helpers.PaginateAliasPath(base, 1), permalink(base), nil) s.writeDestAlias(helpers.PaginateAliasPath(base, 1), s.Info.permalink(base), nil)
pagers := n.paginator.Pagers() pagers := n.paginator.Pagers()
@ -2111,6 +2128,15 @@ func (s *Site) newHomeNode(prepare bool, counter int) *Node {
return n return n
} }
func (s *Site) newPage() *Page {
page := &Page{}
page.language = s.Language
page.Date = s.Info.LastChange
page.Lastmod = s.Info.LastChange
page.Site = &s.Info
return page
}
func (s *Site) renderSitemap() error { func (s *Site) renderSitemap() error {
if viper.GetBool("DisableSitemap") { if viper.GetBool("DisableSitemap") {
return nil return nil
@ -2123,11 +2149,7 @@ func (s *Site) renderSitemap() error {
// Prepend homepage to the list of pages // Prepend homepage to the list of pages
pages := make(Pages, 0) pages := make(Pages, 0)
page := &Page{} page := s.newPage()
page.language = s.Language
page.Date = s.Info.LastChange
page.Lastmod = s.Info.LastChange
page.Site = &s.Info
page.URLPath.URL = "" page.URLPath.URL = ""
page.Sitemap.ChangeFreq = sitemapDefault.ChangeFreq page.Sitemap.ChangeFreq = sitemapDefault.ChangeFreq
page.Sitemap.Priority = sitemapDefault.Priority page.Sitemap.Priority = sitemapDefault.Priority
@ -2199,18 +2221,21 @@ func (s *Site) Stats() {
} }
func (s *Site) setURLs(n *Node, in string) { func (s *Site) setURLs(n *Node, in string) {
n.URLPath.URL = helpers.URLizeAndPrep(in) n.URLPath.URL = s.Info.pathSpec.URLizeAndPrep(in)
n.URLPath.Permalink = permalink(n.URLPath.URL) n.URLPath.Permalink = s.Info.permalink(n.URLPath.URL)
n.RSSLink = template.HTML(permalink(in + ".xml")) n.RSSLink = template.HTML(s.Info.permalink(in + ".xml"))
} }
func permalink(plink string) string { func (s *SiteInfo) permalink(plink string) string {
return permalinkStr(plink) return s.permalinkStr(plink)
} }
func permalinkStr(plink string) string { func (s *SiteInfo) permalinkStr(plink string) string {
return helpers.MakePermalink(viper.GetString("BaseURL"), helpers.URLizeAndPrep(plink)).String() return helpers.MakePermalink(
viper.GetString("BaseURL"),
s.pathSpec.URLizeAndPrep(plink)).String()
} }
func (s *Site) newNode(nodeID string) *Node { func (s *Site) newNode(nodeID string) *Node {
return s.nodeLookup(nodeID, 0, true) return s.nodeLookup(nodeID, 0, true)
} }

View file

@ -52,7 +52,7 @@ type OrderedTaxonomyEntry struct {
// KeyPrep... Taxonomies should be case insensitive. Can make it easily conditional later. // KeyPrep... Taxonomies should be case insensitive. Can make it easily conditional later.
func kp(in string) string { func kp(in string) string {
return helpers.MakePathSanitized(in) return helpers.CurrentPathSpec().MakePathSanitized(in)
} }
// Get the weighted pages for the given key. // Get the weighted pages for the given key.

View file

@ -94,6 +94,10 @@ func New() Template {
localTemplates = &templates.Template localTemplates = &templates.Template
// The URL funcs in the funcMap is somewhat language dependent,
// so need to be reinit per site.
initFuncMap()
for k, v := range funcMap { for k, v := range funcMap {
amber.FuncMap[k] = v amber.FuncMap[k] = v
} }

View file

@ -47,7 +47,9 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
) )
var funcMap template.FuncMap var (
funcMap template.FuncMap
)
// eq returns the boolean truth of arg1 == arg2. // eq returns the boolean truth of arg1 == arg2.
func eq(x, y interface{}) bool { func eq(x, y interface{}) bool {
@ -1940,7 +1942,7 @@ func absURL(a interface{}) (template.HTML, error) {
if err != nil { if err != nil {
return "", nil return "", nil
} }
return template.HTML(helpers.AbsURL(s, false)), nil return template.HTML(helpers.CurrentPathSpec().AbsURL(s, false)), nil
} }
func relURL(a interface{}) (template.HTML, error) { func relURL(a interface{}) (template.HTML, error) {
@ -1948,10 +1950,10 @@ func relURL(a interface{}) (template.HTML, error) {
if err != nil { if err != nil {
return "", nil return "", nil
} }
return template.HTML(helpers.RelURL(s, false)), nil return template.HTML(helpers.CurrentPathSpec().RelURL(s, false)), nil
} }
func init() { func initFuncMap() {
funcMap = template.FuncMap{ funcMap = template.FuncMap{
"absURL": absURL, "absURL": absURL,
"absLangURL": func(i interface{}) (template.HTML, error) { "absLangURL": func(i interface{}) (template.HTML, error) {
@ -1959,7 +1961,7 @@ func init() {
if err != nil { if err != nil {
return "", err return "", err
} }
return template.HTML(helpers.AbsURL(s, true)), nil return template.HTML(helpers.CurrentPathSpec().AbsURL(s, true)), nil
}, },
"add": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '+') }, "add": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '+') },
"after": after, "after": after,
@ -2020,7 +2022,7 @@ func init() {
if err != nil { if err != nil {
return "", err return "", err
} }
return template.HTML(helpers.RelURL(s, true)), nil return template.HTML(helpers.CurrentPathSpec().RelURL(s, true)), nil
}, },
"relref": relRef, "relref": relRef,
"replace": replace, "replace": replace,
@ -2047,7 +2049,7 @@ func init() {
"time": asTime, "time": asTime,
"trim": trim, "trim": trim,
"upper": func(a string) string { return strings.ToUpper(a) }, "upper": func(a string) string { return strings.ToUpper(a) },
"urlize": helpers.URLize, "urlize": helpers.CurrentPathSpec().URLize,
"where": where, "where": where,
"i18n": I18nTranslate, "i18n": I18nTranslate,
"T": I18nTranslate, "T": I18nTranslate,

View file

@ -63,6 +63,11 @@ func tstIsLt(tp tstCompareType) bool {
return tp == tstLt || tp == tstLe return tp == tstLt || tp == tstLe
} }
func tstInitTemplates() {
viper.Set("CurrentContentLanguage", helpers.NewLanguage("en"))
helpers.ResetConfigProvider()
}
func TestFuncsInTemplate(t *testing.T) { func TestFuncsInTemplate(t *testing.T) {
viper.Reset() viper.Reset()
@ -234,6 +239,8 @@ urlize: bat-man
viper.Set("baseURL", "http://mysite.com/hugo/") viper.Set("baseURL", "http://mysite.com/hugo/")
tstInitTemplates()
if err != nil { if err != nil {
t.Fatal("Got error on parse", err) t.Fatal("Got error on parse", err)
} }
@ -2498,6 +2505,7 @@ func TestPartialCached(t *testing.T) {
data.Section = "blog" data.Section = "blog"
data.Params = map[string]interface{}{"langCode": "en"} data.Params = map[string]interface{}{"langCode": "en"}
tstInitTemplates()
InitializeT() InitializeT()
for i, tc := range testCases { for i, tc := range testCases {
var tmp string var tmp string

View file

@ -25,6 +25,7 @@ import (
var ( var (
Logi18nWarnings bool Logi18nWarnings bool
i18nWarningLogger = helpers.NewDistinctFeedbackLogger() i18nWarningLogger = helpers.NewDistinctFeedbackLogger()
currentLanguage *helpers.Language
) )
type translate struct { type translate struct {
@ -37,11 +38,12 @@ var translator *translate
// SetTranslateLang sets the translations language to use during template processing. // SetTranslateLang sets the translations language to use during template processing.
// This construction is unfortunate, but the template system is currently global. // This construction is unfortunate, but the template system is currently global.
func SetTranslateLang(lang string) error { func SetTranslateLang(language *helpers.Language) error {
if f, ok := translator.translateFuncs[lang]; ok { currentLanguage = language
if f, ok := translator.translateFuncs[language.Lang]; ok {
translator.current = f translator.current = f
} else { } else {
jww.WARN.Printf("Translation func for language %v not found, use default.", lang) jww.WARN.Printf("Translation func for language %v not found, use default.", language.Lang)
translator.current = translator.translateFuncs[viper.GetString("DefaultContentLanguage")] translator.current = translator.translateFuncs[viper.GetString("DefaultContentLanguage")]
} }
return nil return nil

View file

@ -17,6 +17,7 @@ import (
"testing" "testing"
"github.com/nicksnyder/go-i18n/i18n/bundle" "github.com/nicksnyder/go-i18n/i18n/bundle"
"github.com/spf13/hugo/helpers"
"github.com/spf13/viper" "github.com/spf13/viper"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -116,7 +117,7 @@ func doTestI18nTranslate(t *testing.T, data map[string][]byte, lang, id string,
} }
SetI18nTfuncs(i18nBundle) SetI18nTfuncs(i18nBundle)
SetTranslateLang(lang) SetTranslateLang(helpers.NewLanguage(lang))
translated, err := I18nTranslate(id, args) translated, err := I18nTranslate(id, args)
if err != nil { if err != nil {
@ -129,6 +130,7 @@ func TestI18nTranslate(t *testing.T) {
var actual, expected string var actual, expected string
viper.SetDefault("DefaultContentLanguage", "en") viper.SetDefault("DefaultContentLanguage", "en")
viper.Set("CurrentContentLanguage", helpers.NewLanguage("en"))
// Test without and with placeholders // Test without and with placeholders
for _, enablePlaceholders := range []bool{false, true} { for _, enablePlaceholders := range []bool{false, true} {