Improve language handling in URLs

The current "rendering language" is needed outside of Site. This commit moves the Language type to the helpers package, and then used to get correct correct language configuration in the markdownify template func.
This commit also adds two new template funcs: relLangURL and absLangURL.

See #2309
This commit is contained in:
Bjørn Erik Pedersen 2016-08-07 22:01:55 +02:00
parent 2079a23dd8
commit 54141f71dd
20 changed files with 316 additions and 172 deletions

View file

@ -135,7 +135,7 @@ This uses a definition like this one in `i18n/en-US.yaml`:
### Multilingual Themes support ### Multilingual Themes support
To support Multilingual mode in your themes, some considerations must be taken for the URLs in the templates. If there are more than one language, URLs must either come from the built-in `.Permalink` or `.URL`, be constructed with `relURL` or `absURL` -- or prefixed with `{{.LanguagePrefix }}`. To support Multilingual mode in your themes, some considerations must be taken for the URLs in the templates. If there are more than one language, URLs must either come from the built-in `.Permalink` or `.URL`, be constructed with `relLangURL` or `absLangURL` template funcs -- or prefixed with `{{.LanguagePrefix }}`.
If there are more than one language defined, the`LanguagePrefix` variable will equal `"/en"` (or whatever your `CurrentLanguage` is). If not enabled, it will be an empty string, so it is harmless for single-language sites. If there are more than one language defined, the`LanguagePrefix` variable will equal `"/en"` (or whatever your `CurrentLanguage` is). If not enabled, it will be an empty string, so it is harmless for single-language sites.

View file

@ -787,6 +787,13 @@ e.g.: `{{ i18n "translation_id" }}`
* `{{ mul 1000 (time "2016-05-28T10:30:00.00+10:00").Unix }}` → 1464395400000 (Unix time in milliseconds) * `{{ mul 1000 (time "2016-05-28T10:30:00.00+10:00").Unix }}` → 1464395400000 (Unix time in milliseconds)
## URLs ## URLs
### absLangURL, relLangURL
These are similar to the `absURL` and `relURL` relatives below, but will add the correct language prefix when the site is configured with more than one language.
So for a site `baseURL` set to `http://mysite.com/hugo/` and the current language is `en`:
* `{{ "blog/" | absLangURL }}` → "http://mysite.com/hugo/en/blog/"
* `{{ "blog/" | relLangURL }}` → "/hugo/en/blog/"
### absURL, relURL ### absURL, relURL

100
helpers/language.go Normal file
View file

@ -0,0 +1,100 @@
// 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 (
"sort"
"strings"
"sync"
"github.com/spf13/cast"
"github.com/spf13/viper"
)
type Language struct {
Lang string
Title string
Weight int
params map[string]interface{}
paramsInit sync.Once
}
func NewLanguage(lang string) *Language {
return &Language{Lang: lang, params: make(map[string]interface{})}
}
func NewDefaultLanguage() *Language {
defaultLang := viper.GetString("DefaultContentLanguage")
if defaultLang == "" {
defaultLang = "en"
}
return NewLanguage(defaultLang)
}
type Languages []*Language
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 { return l[i].Weight < l[j].Weight }
func (l Languages) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
func (l *Language) Params() map[string]interface{} {
l.paramsInit.Do(func() {
// Merge with global config.
// TODO(bep) consider making this part of a constructor func.
globalParams := viper.GetStringMap("Params")
for k, v := range globalParams {
if _, ok := l.params[k]; !ok {
l.params[k] = v
}
}
})
return l.params
}
func (l *Language) SetParam(k string, v interface{}) {
l.params[k] = v
}
func (l *Language) GetString(key string) string { return cast.ToString(l.Get(key)) }
func (ml *Language) GetStringMap(key string) map[string]interface{} {
return cast.ToStringMap(ml.Get(key))
}
func (l *Language) GetStringMapString(key string) map[string]string {
return cast.ToStringMapString(l.Get(key))
}
func (l *Language) Get(key string) interface{} {
if l == nil {
panic("language not set")
}
key = strings.ToLower(key)
if v, ok := l.params[key]; ok {
return v
}
return viper.Get(key)
}

View file

@ -147,18 +147,18 @@ 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(path string) string { func AbsURL(in string, addLanguage bool) string {
url, err := url.Parse(path) url, err := url.Parse(in)
if err != nil { if err != nil {
return path return in
} }
if url.IsAbs() || strings.HasPrefix(path, "//") { if url.IsAbs() || strings.HasPrefix(in, "//") {
return path return in
} }
baseURL := viper.GetString("BaseURL") baseURL := viper.GetString("BaseURL")
if strings.HasPrefix(path, "/") { if strings.HasPrefix(in, "/") {
p, err := url.Parse(baseURL) p, err := url.Parse(baseURL)
if err != nil { if err != nil {
panic(err) panic(err)
@ -166,7 +166,23 @@ func AbsURL(path string) string {
p.Path = "" p.Path = ""
baseURL = p.String() baseURL = p.String()
} }
return MakePermalink(baseURL, path).String()
if addLanguage {
addSlash := in == "" || strings.HasSuffix(in, "/")
in = path.Join(getLanguagePrefix(), in)
if addSlash {
in += "/"
}
}
return MakePermalink(baseURL, in).String()
}
func getLanguagePrefix() string {
if !viper.GetBool("Multilingual") {
return ""
}
return viper.Get("CurrentContentLanguage").(*Language).Lang
} }
// IsAbsURL determines whether the given path points to an absolute URL. // IsAbsURL determines whether the given path points to an absolute URL.
@ -182,23 +198,34 @@ 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(path string) string { func RelURL(in string, addLanguage bool) string {
baseURL := viper.GetString("BaseURL") baseURL := viper.GetString("BaseURL")
canonifyURLs := viper.GetBool("canonifyURLs") canonifyURLs := viper.GetBool("canonifyURLs")
if (!strings.HasPrefix(path, baseURL) && strings.HasPrefix(path, "http")) || strings.HasPrefix(path, "//") { if (!strings.HasPrefix(in, baseURL) && strings.HasPrefix(in, "http")) || strings.HasPrefix(in, "//") {
return path return in
} }
u := path u := in
if strings.HasPrefix(path, baseURL) { if strings.HasPrefix(in, baseURL) {
u = strings.TrimPrefix(u, baseURL) u = strings.TrimPrefix(u, baseURL)
} }
if addLanguage {
hadSlash := strings.HasSuffix(u, "/")
u = path.Join(getLanguagePrefix(), u)
if hadSlash {
u += "/"
}
}
if !canonifyURLs { if !canonifyURLs {
u = AddContextRoot(baseURL, u) u = AddContextRoot(baseURL, u)
} }
if path == "" && !strings.HasSuffix(u, "/") && strings.HasSuffix(baseURL, "/") {
if in == "" && !strings.HasSuffix(u, "/") && strings.HasSuffix(baseURL, "/") {
u += "/" u += "/"
} }

View file

@ -14,11 +14,13 @@
package helpers package helpers
import ( import (
"fmt"
"strings" "strings"
"testing" "testing"
"github.com/spf13/viper" "github.com/spf13/viper"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestURLize(t *testing.T) { func TestURLize(t *testing.T) {
@ -43,62 +45,114 @@ func TestURLize(t *testing.T) {
} }
func TestAbsURL(t *testing.T) { func TestAbsURL(t *testing.T) {
defer viper.Reset() for _, addLanguage := range []bool{true, false} {
for _, m := range []bool{true, false} {
for _, l := range []string{"en", "fr"} {
doTestAbsURL(t, addLanguage, m, l)
}
}
}
}
func doTestAbsURL(t *testing.T, addLanguage, multilingual bool, lang string) {
viper.Reset()
viper.Set("Multilingual", multilingual)
viper.Set("CurrentContentLanguage", NewLanguage(lang))
tests := []struct { tests := []struct {
input string input string
baseURL string baseURL string
expected string expected string
}{ }{
{"/test/foo", "http://base/", "http://base/test/foo"}, {"/test/foo", "http://base/", "http://base/MULTItest/foo"},
{"", "http://base/ace/", "http://base/ace/"}, {"", "http://base/ace/", "http://base/ace/MULTI"},
{"/test/2/foo/", "http://base", "http://base/test/2/foo/"}, {"/test/2/foo/", "http://base", "http://base/MULTItest/2/foo/"},
{"http://abs", "http://base/", "http://abs"}, {"http://abs", "http://base/", "http://abs"},
{"schema://abs", "http://base/", "schema://abs"}, {"schema://abs", "http://base/", "schema://abs"},
{"//schemaless", "http://base/", "//schemaless"}, {"//schemaless", "http://base/", "//schemaless"},
{"test/2/foo/", "http://base/path", "http://base/path/test/2/foo/"}, {"test/2/foo/", "http://base/path", "http://base/path/MULTItest/2/foo/"},
{"/test/2/foo/", "http://base/path", "http://base/test/2/foo/"}, {"/test/2/foo/", "http://base/path", "http://base/MULTItest/2/foo/"},
{"http//foo", "http://base/path", "http://base/path/http/foo"}, {"http//foo", "http://base/path", "http://base/path/MULTIhttp/foo"},
} }
for _, test := range tests { for _, test := range tests {
viper.Reset()
viper.Set("BaseURL", test.baseURL) viper.Set("BaseURL", test.baseURL)
output := AbsURL(test.input) output := AbsURL(test.input, addLanguage)
if output != test.expected { expected := test.expected
t.Errorf("Expected %#v, got %#v\n", test.expected, output) if multilingual && addLanguage {
expected = strings.Replace(expected, "MULTI", lang+"/", 1)
} else {
expected = strings.Replace(expected, "MULTI", "", 1)
}
if output != expected {
t.Errorf("Expected %#v, got %#v\n", expected, output)
} }
} }
} }
func TestIsAbsURL(t *testing.T) {
for i, this := range []struct {
a string
b bool
}{
{"http://gohugo.io", true},
{"https://gohugo.io", true},
{"//gohugo.io", true},
{"http//gohugo.io", false},
{"/content", false},
{"content", false},
} {
require.True(t, IsAbsURL(this.a) == this.b, fmt.Sprintf("Test %d", i))
}
}
func TestRelURL(t *testing.T) { func TestRelURL(t *testing.T) {
defer viper.Reset() for _, addLanguage := range []bool{true, false} {
//defer viper.Set("canonifyURLs", viper.GetBool("canonifyURLs")) for _, m := range []bool{true, false} {
for _, l := range []string{"en", "fr"} {
doTestRelURL(t, addLanguage, m, l)
}
}
}
}
func doTestRelURL(t *testing.T, addLanguage, multilingual bool, lang string) {
viper.Reset()
viper.Set("Multilingual", multilingual)
viper.Set("CurrentContentLanguage", NewLanguage(lang))
tests := []struct { tests := []struct {
input string input string
baseURL string baseURL string
canonify bool canonify bool
expected string expected string
}{ }{
{"/test/foo", "http://base/", false, "/test/foo"}, {"/test/foo", "http://base/", false, "MULTI/test/foo"},
{"test.css", "http://base/sub", false, "/sub/test.css"}, {"test.css", "http://base/sub", false, "/subMULTI/test.css"},
{"test.css", "http://base/sub", true, "/test.css"}, {"test.css", "http://base/sub", true, "MULTI/test.css"},
{"/test/", "http://base/", false, "/test/"}, {"/test/", "http://base/", false, "MULTI/test/"},
{"/test/", "http://base/sub/", false, "/sub/test/"}, {"/test/", "http://base/sub/", false, "/subMULTI/test/"},
{"/test/", "http://base/sub/", true, "/test/"}, {"/test/", "http://base/sub/", true, "MULTI/test/"},
{"", "http://base/ace/", false, "/ace/"}, {"", "http://base/ace/", false, "/aceMULTI/"},
{"", "http://base/ace", false, "/ace"}, {"", "http://base/ace", false, "/aceMULTI"},
{"http://abs", "http://base/", false, "http://abs"}, {"http://abs", "http://base/", false, "http://abs"},
{"//schemaless", "http://base/", false, "//schemaless"}, {"//schemaless", "http://base/", false, "//schemaless"},
} }
for i, test := range tests { for i, test := range tests {
viper.Reset()
viper.Set("BaseURL", test.baseURL) viper.Set("BaseURL", test.baseURL)
viper.Set("canonifyURLs", test.canonify) viper.Set("canonifyURLs", test.canonify)
output := RelURL(test.input) output := RelURL(test.input, addLanguage)
if output != test.expected {
t.Errorf("[%d][%t] Expected %#v, got %#v\n", i, test.canonify, test.expected, output) expected := test.expected
if multilingual && addLanguage {
expected = strings.Replace(expected, "MULTI", "/"+lang, 1)
} else {
expected = strings.Replace(expected, "MULTI", "", 1)
}
if output != expected {
t.Errorf("[%d][%t] Expected %#v, got %#v\n", i, test.canonify, expected, output)
} }
} }
} }

View file

@ -46,7 +46,7 @@ func TestDefaultHandler(t *testing.T) {
s := &Site{ s := &Site{
Source: &source.InMemorySource{ByteSource: sources}, Source: &source.InMemorySource{ByteSource: sources},
targets: targetList{page: &target.PagePub{UglyURLs: true, PublishDir: "public"}}, targets: targetList{page: &target.PagePub{UglyURLs: true, PublishDir: "public"}},
Language: NewLanguage("en"), Language: helpers.NewLanguage("en"),
} }
if err := buildAndRenderSite(s, if err := buildAndRenderSite(s,

View file

@ -63,7 +63,7 @@ func createSitesFromConfig() ([]*Site, error) {
multilingual := viper.GetStringMap("Languages") multilingual := viper.GetStringMap("Languages")
if len(multilingual) == 0 { if len(multilingual) == 0 {
// TODO(bep) multilingo langConfigsList = append(langConfigsList, NewLanguage("en")) // TODO(bep) multilingo langConfigsList = append(langConfigsList, NewLanguage("en"))
sites = append(sites, newSite(NewLanguage("en"))) sites = append(sites, newSite(helpers.NewLanguage("en")))
} }
if len(multilingual) > 0 { if len(multilingual) > 0 {
@ -481,7 +481,7 @@ func doBuildSite(s *Site, render bool, additionalTemplates ...string) error {
} }
// Convenience func used in tests. // Convenience func used in tests.
func newHugoSitesFromSourceAndLanguages(input []source.ByteSource, languages Languages) (*HugoSites, error) { func newHugoSitesFromSourceAndLanguages(input []source.ByteSource, languages helpers.Languages) (*HugoSites, error) {
if len(languages) == 0 { if len(languages) == 0 {
panic("Must provide at least one language") panic("Must provide at least one language")
} }
@ -504,10 +504,10 @@ func newHugoSitesFromSourceAndLanguages(input []source.ByteSource, languages Lan
} }
// Convenience func used in tests. // Convenience func used in tests.
func newHugoSitesFromLanguages(languages Languages) (*HugoSites, error) { func newHugoSitesFromLanguages(languages helpers.Languages) (*HugoSites, error) {
return newHugoSitesFromSourceAndLanguages(nil, languages) return newHugoSitesFromSourceAndLanguages(nil, languages)
} }
func newHugoSitesDefaultLanguage() (*HugoSites, error) { func newHugoSitesDefaultLanguage() (*HugoSites, error) {
return newHugoSitesFromSourceAndLanguages(nil, Languages{newDefaultLanguage()}) return newHugoSitesFromSourceAndLanguages(nil, helpers.Languages{helpers.NewDefaultLanguage()})
} }

View file

@ -18,6 +18,8 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/spf13/hugo/helpers"
"path/filepath" "path/filepath"
toml "github.com/pelletier/go-toml" toml "github.com/pelletier/go-toml"
@ -673,7 +675,7 @@ func createTestSite(pageSources []source.ByteSource) *Site {
return &Site{ return &Site{
Source: &source.InMemorySource{ByteSource: pageSources}, Source: &source.InMemorySource{ByteSource: pageSources},
Language: newDefaultLanguage(), Language: helpers.NewDefaultLanguage(),
} }
} }

View file

@ -1,3 +1,16 @@
// 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 hugolib package hugolib
import ( import (
@ -10,58 +23,21 @@ import (
"fmt" "fmt"
"github.com/spf13/cast" "github.com/spf13/cast"
"github.com/spf13/viper" "github.com/spf13/hugo/helpers"
) )
type Language struct {
Lang string
Title string
Weight int
params map[string]interface{}
paramsInit sync.Once
}
func NewLanguage(lang string) *Language {
return &Language{Lang: lang, params: make(map[string]interface{})}
}
func newDefaultLanguage() *Language {
defaultLang := viper.GetString("DefaultContentLanguage")
if defaultLang == "" {
defaultLang = "en"
}
return NewLanguage(defaultLang)
}
type Languages []*Language
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 { return l[i].Weight < l[j].Weight }
func (l Languages) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
type Multilingual struct { type Multilingual struct {
Languages Languages Languages helpers.Languages
DefaultLang *Language DefaultLang *helpers.Language
langMap map[string]*Language langMap map[string]*helpers.Language
langMapInit sync.Once langMapInit sync.Once
} }
func (ml *Multilingual) Language(lang string) *Language { func (ml *Multilingual) Language(lang string) *helpers.Language {
ml.langMapInit.Do(func() { ml.langMapInit.Do(func() {
ml.langMap = make(map[string]*Language) ml.langMap = make(map[string]*helpers.Language)
for _, l := range ml.Languages { for _, l := range ml.Languages {
ml.langMap[l.Lang] = l ml.langMap[l.Lang] = l
} }
@ -70,7 +46,7 @@ func (ml *Multilingual) Language(lang string) *Language {
} }
func newMultiLingualFromSites(sites ...*Site) (*Multilingual, error) { func newMultiLingualFromSites(sites ...*Site) (*Multilingual, error) {
languages := make(Languages, len(sites)) languages := make(helpers.Languages, len(sites))
for i, s := range sites { for i, s := range sites {
if s.Language == nil { if s.Language == nil {
@ -79,61 +55,22 @@ func newMultiLingualFromSites(sites ...*Site) (*Multilingual, error) {
languages[i] = s.Language languages[i] = s.Language
} }
return &Multilingual{Languages: languages, DefaultLang: newDefaultLanguage()}, nil return &Multilingual{Languages: languages, DefaultLang: helpers.NewDefaultLanguage()}, nil
} }
func newMultiLingualDefaultLanguage() *Multilingual { func newMultiLingualDefaultLanguage() *Multilingual {
return newMultiLingualForLanguage(newDefaultLanguage()) return newMultiLingualForLanguage(helpers.NewDefaultLanguage())
} }
func newMultiLingualForLanguage(language *Language) *Multilingual { func newMultiLingualForLanguage(language *helpers.Language) *Multilingual {
languages := Languages{language} languages := helpers.Languages{language}
return &Multilingual{Languages: languages, DefaultLang: language} return &Multilingual{Languages: languages, DefaultLang: language}
} }
func (ml *Multilingual) enabled() bool { func (ml *Multilingual) enabled() bool {
return len(ml.Languages) > 1 return len(ml.Languages) > 1
} }
func (l *Language) Params() map[string]interface{} {
l.paramsInit.Do(func() {
// Merge with global config.
// TODO(bep) consider making this part of a constructor func.
globalParams := viper.GetStringMap("Params")
for k, v := range globalParams {
if _, ok := l.params[k]; !ok {
l.params[k] = v
}
}
})
return l.params
}
func (l *Language) SetParam(k string, v interface{}) {
l.params[k] = v
}
func (l *Language) GetString(key string) string { return cast.ToString(l.Get(key)) }
func (ml *Language) GetStringMap(key string) map[string]interface{} {
return cast.ToStringMap(ml.Get(key))
}
func (l *Language) GetStringMapString(key string) map[string]string {
return cast.ToStringMapString(l.Get(key))
}
func (l *Language) Get(key string) interface{} {
if l == nil {
panic("language not set")
}
key = strings.ToLower(key)
if v, ok := l.params[key]; ok {
return v
}
return viper.Get(key)
}
func (s *Site) multilingualEnabled() bool { func (s *Site) multilingualEnabled() bool {
return s.Multilingual != nil && s.Multilingual.enabled() return s.Multilingual != nil && s.Multilingual.enabled()
} }
@ -143,12 +80,12 @@ func (s *Site) currentLanguageString() string {
return s.currentLanguage().Lang return s.currentLanguage().Lang
} }
func (s *Site) currentLanguage() *Language { func (s *Site) currentLanguage() *helpers.Language {
return s.Language return s.Language
} }
func toSortedLanguages(l map[string]interface{}) (Languages, error) { func toSortedLanguages(l map[string]interface{}) (helpers.Languages, error) {
langs := make(Languages, len(l)) langs := make(helpers.Languages, len(l))
i := 0 i := 0
for lang, langConf := range l { for lang, langConf := range l {
@ -158,7 +95,7 @@ func toSortedLanguages(l map[string]interface{}) (Languages, error) {
return nil, fmt.Errorf("Language config is not a map: %v", langsMap) return nil, fmt.Errorf("Language config is not a map: %v", langsMap)
} }
language := NewLanguage(lang) language := helpers.NewLanguage(lang)
for k, v := range langsMap { for k, v := range langsMap {
loki := strings.ToLower(k) loki := strings.ToLower(k)

View file

@ -47,7 +47,7 @@ type Node struct {
paginatorInit sync.Once paginatorInit sync.Once
scratch *Scratch scratch *Scratch
language *Language language *helpers.Language
languageInit sync.Once languageInit sync.Once
lang string // TODO(bep) multilingo lang string // TODO(bep) multilingo
@ -193,7 +193,7 @@ func (n *Node) Scratch() *Scratch {
} }
// TODO(bep) multilingo consolidate. See Page. // TODO(bep) multilingo consolidate. See Page.
func (n *Node) Language() *Language { func (n *Node) Language() *helpers.Language {
n.initLanguage() n.initLanguage()
return n.language return n.language
} }

View file

@ -39,7 +39,7 @@ func TestRobotsTXTOutput(t *testing.T) {
s := &Site{ s := &Site{
Source: &source.InMemorySource{ByteSource: weightedSources}, Source: &source.InMemorySource{ByteSource: weightedSources},
Language: newDefaultLanguage(), Language: helpers.NewDefaultLanguage(),
} }
if err := buildAndRenderSite(s, "robots.txt", robotTxtTemplate); err != nil { if err := buildAndRenderSite(s, "robots.txt", robotTxtTemplate); err != nil {

View file

@ -55,7 +55,7 @@ func TestRSSOutput(t *testing.T) {
hugofs.InitMemFs() hugofs.InitMemFs()
s := &Site{ s := &Site{
Source: &source.InMemorySource{ByteSource: weightedSources}, Source: &source.InMemorySource{ByteSource: weightedSources},
Language: newDefaultLanguage(), Language: helpers.NewDefaultLanguage(),
} }
if err := buildAndRenderSite(s, "rss.xml", rssTemplate); err != nil { if err := buildAndRenderSite(s, "rss.xml", rssTemplate); err != nil {

View file

@ -562,7 +562,7 @@ tags:
s := &Site{ s := &Site{
Source: &source.InMemorySource{ByteSource: sources}, Source: &source.InMemorySource{ByteSource: sources},
targets: targetList{page: &target.PagePub{UglyURLs: false}}, targets: targetList{page: &target.PagePub{UglyURLs: false}},
Language: newDefaultLanguage(), Language: helpers.NewDefaultLanguage(),
} }
addTemplates := func(templ tpl.Template) error { addTemplates := func(templ tpl.Template) error {

View file

@ -96,7 +96,7 @@ type Site struct {
futureCount int futureCount int
expiredCount int expiredCount int
Data map[string]interface{} Data map[string]interface{}
Language *Language Language *helpers.Language
} }
// Reset returns a new Site prepared for rebuild. // Reset returns a new Site prepared for rebuild.
@ -106,13 +106,13 @@ 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 *Language) *Site { func newSite(lang *helpers.Language) *Site {
return &Site{Language: lang, Info: SiteInfo{multilingual: newMultiLingualForLanguage(lang)}} return &Site{Language: lang, Info: SiteInfo{multilingual: newMultiLingualForLanguage(lang)}}
} }
// newSite creates a new site in the default language. // newSite creates a new site in the default language.
func newSiteDefaultLang() *Site { func newSiteDefaultLang() *Site {
return newSite(newDefaultLanguage()) return newSite(helpers.NewDefaultLanguage())
} }
// Convenience func used in tests. // Convenience func used in tests.
@ -131,7 +131,7 @@ func newSiteFromSources(pathContentPairs ...string) *Site {
return &Site{ return &Site{
Source: &source.InMemorySource{ByteSource: sources}, Source: &source.InMemorySource{ByteSource: sources},
Language: newDefaultLanguage(), Language: helpers.NewDefaultLanguage(),
} }
} }
@ -173,9 +173,9 @@ type SiteInfo struct {
Data *map[string]interface{} Data *map[string]interface{}
multilingual *Multilingual multilingual *Multilingual
Language *Language Language *helpers.Language
LanguagePrefix string LanguagePrefix string
Languages Languages Languages helpers.Languages
} }
// Used in tests. // Used in tests.
@ -782,6 +782,9 @@ func (s *Site) setupPrevNext() {
} }
func (s *Site) render() (err error) { func (s *Site) render() (err error) {
// There are sadly some global template funcs etc. that needs the language information.
viper.Set("Multilingual", s.multilingualEnabled())
viper.Set("CurrentContentLanguage", s.Language)
if err = tpl.SetTranslateLang(s.Language.Lang); err != nil { if err = tpl.SetTranslateLang(s.Language.Lang); err != nil {
return return
} }
@ -851,11 +854,11 @@ func (s *Site) initialize() (err error) {
// HomeAbsURL is a convenience method giving the absolute URL to the home page. // HomeAbsURL is a convenience method giving the absolute URL to the home page.
func (s *SiteInfo) HomeAbsURL() string { func (s *SiteInfo) HomeAbsURL() string {
base := "/" base := ""
if s.IsMultiLingual() { if s.IsMultiLingual() {
base = s.Language.Lang base = s.Language.Lang
} }
return helpers.AbsURL(base) return helpers.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.
@ -867,8 +870,8 @@ func (s *SiteInfo) SitemapAbsURL() string {
func (s *Site) initializeSiteInfo() { func (s *Site) initializeSiteInfo() {
var ( var (
lang *Language = s.Language lang *helpers.Language = s.Language
languages Languages languages helpers.Languages
) )
if s.Multilingual != nil { if s.Multilingual != nil {
@ -1435,7 +1438,7 @@ func (s *Site) renderAliases() error {
if s.Multilingual.enabled() { if s.Multilingual.enabled() {
mainLang := s.Multilingual.DefaultLang.Lang mainLang := s.Multilingual.DefaultLang.Lang
mainLangURL := helpers.AbsURL(mainLang) mainLangURL := helpers.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); err != nil { if err := s.publishDestAlias(s.languageAliasTarget(), "/", mainLangURL); err != nil {
return err return err

View file

@ -112,7 +112,7 @@ func _TestPageTargetUgly(t *testing.T) {
s := &Site{ s := &Site{
targets: targetList{page: &target.PagePub{UglyURLs: true, PublishDir: "public"}}, targets: targetList{page: &target.PagePub{UglyURLs: true, PublishDir: "public"}},
Source: &source.InMemorySource{ByteSource: fakeSource}, Source: &source.InMemorySource{ByteSource: fakeSource},
Language: newDefaultLanguage(), Language: helpers.NewDefaultLanguage(),
} }
if err := buildAndRenderSite(s); err != nil { if err := buildAndRenderSite(s); err != nil {

View file

@ -128,7 +128,7 @@ func TestDraftAndFutureRender(t *testing.T) {
siteSetup := func(t *testing.T) *Site { siteSetup := func(t *testing.T) *Site {
s := &Site{ s := &Site{
Source: &source.InMemorySource{ByteSource: sources}, Source: &source.InMemorySource{ByteSource: sources},
Language: newDefaultLanguage(), Language: helpers.NewDefaultLanguage(),
} }
if err := buildSiteSkipRender(s); err != nil { if err := buildSiteSkipRender(s); err != nil {
@ -186,7 +186,7 @@ func TestFutureExpirationRender(t *testing.T) {
siteSetup := func(t *testing.T) *Site { siteSetup := func(t *testing.T) *Site {
s := &Site{ s := &Site{
Source: &source.InMemorySource{ByteSource: sources}, Source: &source.InMemorySource{ByteSource: sources},
Language: newDefaultLanguage(), Language: helpers.NewDefaultLanguage(),
} }
if err := buildSiteSkipRender(s); err != nil { if err := buildSiteSkipRender(s); err != nil {
@ -280,7 +280,7 @@ THE END.`, refShortcode)),
s := &Site{ s := &Site{
Source: &source.InMemorySource{ByteSource: sources}, Source: &source.InMemorySource{ByteSource: sources},
targets: targetList{page: &target.PagePub{UglyURLs: uglyURLs}}, targets: targetList{page: &target.PagePub{UglyURLs: uglyURLs}},
Language: newDefaultLanguage(), Language: helpers.NewDefaultLanguage(),
} }
if err := buildAndRenderSite(s, "_default/single.html", "{{.Content}}"); err != nil { if err := buildAndRenderSite(s, "_default/single.html", "{{.Content}}"); err != nil {
@ -348,7 +348,7 @@ func doTestShouldAlwaysHaveUglyURLs(t *testing.T, uglyURLs bool) {
s := &Site{ s := &Site{
Source: &source.InMemorySource{ByteSource: sources}, Source: &source.InMemorySource{ByteSource: sources},
targets: targetList{page: &target.PagePub{UglyURLs: uglyURLs, PublishDir: "public"}}, targets: targetList{page: &target.PagePub{UglyURLs: uglyURLs, PublishDir: "public"}},
Language: newDefaultLanguage(), Language: helpers.NewDefaultLanguage(),
} }
if err := buildAndRenderSite(s, if err := buildAndRenderSite(s,
@ -438,7 +438,7 @@ func doTestSectionNaming(t *testing.T, canonify, uglify, pluralize bool) {
s := &Site{ s := &Site{
Source: &source.InMemorySource{ByteSource: sources}, Source: &source.InMemorySource{ByteSource: sources},
targets: targetList{page: &target.PagePub{UglyURLs: uglify}}, targets: targetList{page: &target.PagePub{UglyURLs: uglify}},
Language: newDefaultLanguage(), Language: helpers.NewDefaultLanguage(),
} }
if err := buildAndRenderSite(s, if err := buildAndRenderSite(s,
@ -500,7 +500,7 @@ func TestSkipRender(t *testing.T) {
s := &Site{ s := &Site{
Source: &source.InMemorySource{ByteSource: sources}, Source: &source.InMemorySource{ByteSource: sources},
targets: targetList{page: &target.PagePub{UglyURLs: true}}, targets: targetList{page: &target.PagePub{UglyURLs: true}},
Language: newDefaultLanguage(), Language: helpers.NewDefaultLanguage(),
} }
if err := buildAndRenderSite(s, if err := buildAndRenderSite(s,
@ -555,7 +555,7 @@ func TestAbsURLify(t *testing.T) {
s := &Site{ s := &Site{
Source: &source.InMemorySource{ByteSource: sources}, Source: &source.InMemorySource{ByteSource: sources},
targets: targetList{page: &target.PagePub{UglyURLs: true}}, targets: targetList{page: &target.PagePub{UglyURLs: true}},
Language: newDefaultLanguage(), Language: helpers.NewDefaultLanguage(),
} }
t.Logf("Rendering with BaseURL %q and CanonifyURLs set %v", viper.GetString("baseURL"), canonify) t.Logf("Rendering with BaseURL %q and CanonifyURLs set %v", viper.GetString("baseURL"), canonify)
@ -649,7 +649,7 @@ func TestOrderedPages(t *testing.T) {
viper.Set("baseurl", "http://auth/bub") viper.Set("baseurl", "http://auth/bub")
s := &Site{ s := &Site{
Source: &source.InMemorySource{ByteSource: weightedSources}, Source: &source.InMemorySource{ByteSource: weightedSources},
Language: newDefaultLanguage(), Language: helpers.NewDefaultLanguage(),
} }
if err := buildSiteSkipRender(s); err != nil { if err := buildSiteSkipRender(s); err != nil {
@ -718,7 +718,7 @@ func TestGroupedPages(t *testing.T) {
viper.Set("baseurl", "http://auth/bub") viper.Set("baseurl", "http://auth/bub")
s := &Site{ s := &Site{
Source: &source.InMemorySource{ByteSource: groupedSources}, Source: &source.InMemorySource{ByteSource: groupedSources},
Language: newDefaultLanguage(), Language: helpers.NewDefaultLanguage(),
} }
if err := buildSiteSkipRender(s); err != nil { if err := buildSiteSkipRender(s); err != nil {
@ -903,7 +903,7 @@ func TestWeightedTaxonomies(t *testing.T) {
viper.Set("taxonomies", taxonomies) viper.Set("taxonomies", taxonomies)
s := &Site{ s := &Site{
Source: &source.InMemorySource{ByteSource: sources}, Source: &source.InMemorySource{ByteSource: sources},
Language: newDefaultLanguage(), Language: helpers.NewDefaultLanguage(),
} }
if err := buildSiteSkipRender(s); err != nil { if err := buildSiteSkipRender(s); err != nil {
@ -972,7 +972,7 @@ func setupLinkingMockSite(t *testing.T) *Site {
site := &Site{ site := &Site{
Source: &source.InMemorySource{ByteSource: sources}, Source: &source.InMemorySource{ByteSource: sources},
Language: newDefaultLanguage(), Language: helpers.NewDefaultLanguage(),
} }
if err := buildSiteSkipRender(site); err != nil { if err := buildSiteSkipRender(site); err != nil {

View file

@ -17,6 +17,8 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/spf13/hugo/helpers"
"html/template" "html/template"
"github.com/spf13/hugo/hugofs" "github.com/spf13/hugo/hugofs"
@ -90,7 +92,7 @@ func TestPageCount(t *testing.T) {
viper.Set("paginate", 10) viper.Set("paginate", 10)
s := &Site{ s := &Site{
Source: &source.InMemorySource{ByteSource: urlFakeSource}, Source: &source.InMemorySource{ByteSource: urlFakeSource},
Language: newDefaultLanguage(), Language: helpers.NewDefaultLanguage(),
} }
if err := buildAndRenderSite(s, "indexes/blue.html", indexTemplate); err != nil { if err := buildAndRenderSite(s, "indexes/blue.html", indexTemplate); err != nil {

View file

@ -43,7 +43,7 @@ func TestSitemapOutput(t *testing.T) {
s := &Site{ s := &Site{
Source: &source.InMemorySource{ByteSource: weightedSources}, Source: &source.InMemorySource{ByteSource: weightedSources},
Language: newDefaultLanguage(), Language: helpers.NewDefaultLanguage(),
} }
if err := buildAndRenderSite(s, "sitemap.xml", SITEMAP_TEMPLATE); err != nil { if err := buildAndRenderSite(s, "sitemap.xml", SITEMAP_TEMPLATE); err != nil {

View file

@ -1214,9 +1214,11 @@ func markdownify(in interface{}) (template.HTML, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
// TODO(bep) ml language
language := viper.Get("CurrentContentLanguage").(*helpers.Language)
m := helpers.RenderBytes(&helpers.RenderingContext{ m := helpers.RenderBytes(&helpers.RenderingContext{
ConfigProvider: viper.GetViper(), ConfigProvider: language,
Content: []byte(text), PageFmt: "markdown"}) Content: []byte(text), PageFmt: "markdown"})
m = bytes.TrimPrefix(m, markdownTrimPrefix) m = bytes.TrimPrefix(m, markdownTrimPrefix)
m = bytes.TrimSuffix(m, markdownTrimSuffix) m = bytes.TrimSuffix(m, markdownTrimSuffix)
@ -1831,7 +1833,7 @@ func absURL(a interface{}) (template.HTML, error) {
if err != nil { if err != nil {
return "", nil return "", nil
} }
return template.HTML(helpers.AbsURL(s)), nil return template.HTML(helpers.AbsURL(s, false)), nil
} }
func relURL(a interface{}) (template.HTML, error) { func relURL(a interface{}) (template.HTML, error) {
@ -1839,12 +1841,13 @@ func relURL(a interface{}) (template.HTML, error) {
if err != nil { if err != nil {
return "", nil return "", nil
} }
return template.HTML(helpers.RelURL(s)), nil return template.HTML(helpers.RelURL(s, false)), nil
} }
func init() { func init() {
funcMap = template.FuncMap{ funcMap = template.FuncMap{
"absURL": absURL, "absURL": absURL,
"absLangURL": func(a string) template.HTML { return template.HTML(helpers.AbsURL(a, true)) },
"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,
"apply": apply, "apply": apply,
@ -1898,6 +1901,7 @@ func init() {
"readFile": readFileFromWorkingDir, "readFile": readFileFromWorkingDir,
"ref": ref, "ref": ref,
"relURL": relURL, "relURL": relURL,
"relLangURL": func(a string) template.HTML { return template.HTML(helpers.RelURL(a, true)) },
"relref": relRef, "relref": relRef,
"replace": replace, "replace": replace,
"replaceRE": replaceRE, "replaceRE": replaceRE,

View file

@ -71,6 +71,8 @@ func TestFuncsInTemplate(t *testing.T) {
workingDir := "/home/hugo" workingDir := "/home/hugo"
viper.Set("WorkingDir", workingDir) viper.Set("WorkingDir", workingDir)
viper.Set("CurrentContentLanguage", helpers.NewDefaultLanguage())
viper.Set("Multilingual", true)
fs := &afero.MemMapFs{} fs := &afero.MemMapFs{}
hugofs.InitFs(fs) hugofs.InitFs(fs)
@ -80,7 +82,8 @@ func TestFuncsInTemplate(t *testing.T) {
// Add the examples from the docs: As a smoke test and to make sure the examples work. // Add the examples from the docs: As a smoke test and to make sure the examples work.
// TODO(bep): docs: fix title example // TODO(bep): docs: fix title example
in := in :=
`absURL: {{ "http://gohugo.io/" | absURL }} `absLangURL: {{ "index.html" | absLangURL }}
absURL: {{ "http://gohugo.io/" | absURL }}
absURL: {{ "mystyle.css" | absURL }} absURL: {{ "mystyle.css" | absURL }}
absURL: {{ 42 | absURL }} absURL: {{ 42 | absURL }}
add: {{add 1 2}} add: {{add 1 2}}
@ -120,6 +123,7 @@ pluralize: {{ "cat" | pluralize }}
querify: {{ (querify "foo" 1 "bar" 2 "baz" "with spaces" "qux" "this&that=those") | safeHTML }} querify: {{ (querify "foo" 1 "bar" 2 "baz" "with spaces" "qux" "this&that=those") | safeHTML }}
readDir: {{ range (readDir ".") }}{{ .Name }}{{ end }} readDir: {{ range (readDir ".") }}{{ .Name }}{{ end }}
readFile: {{ readFile "README.txt" }} readFile: {{ readFile "README.txt" }}
relLangURL: {{ "index.html" | relLangURL }}
relURL 1: {{ "http://gohugo.io/" | relURL }} relURL 1: {{ "http://gohugo.io/" | relURL }}
relURL 2: {{ "mystyle.css" | relURL }} relURL 2: {{ "mystyle.css" | relURL }}
relURL 3: {{ mul 2 21 | relURL }} relURL 3: {{ mul 2 21 | relURL }}
@ -146,7 +150,8 @@ upper: {{upper "BatMan"}}
urlize: {{ "Bat Man" | urlize }} urlize: {{ "Bat Man" | urlize }}
` `
expected := `absURL: http://gohugo.io/ expected := `absLangURL: http://mysite.com/hugo/en/index.html
absURL: http://gohugo.io/
absURL: http://mysite.com/hugo/mystyle.css absURL: http://mysite.com/hugo/mystyle.css
absURL: http://mysite.com/hugo/42 absURL: http://mysite.com/hugo/42
add: 3 add: 3
@ -186,6 +191,7 @@ pluralize: cats
querify: bar=2&baz=with+spaces&foo=1&qux=this%26that%3Dthose querify: bar=2&baz=with+spaces&foo=1&qux=this%26that%3Dthose
readDir: README.txt readDir: README.txt
readFile: Hugo Rocks! readFile: Hugo Rocks!
relLangURL: /hugo/en/index.html
relURL 1: http://gohugo.io/ relURL 1: http://gohugo.io/
relURL 2: /hugo/mystyle.css relURL 2: /hugo/mystyle.css
relURL 3: /hugo/42 relURL 3: /hugo/42
@ -1733,6 +1739,8 @@ func TestReturnWhenSet(t *testing.T) {
} }
func TestMarkdownify(t *testing.T) { func TestMarkdownify(t *testing.T) {
viper.Set("CurrentContentLanguage", helpers.NewDefaultLanguage())
for i, this := range []struct { for i, this := range []struct {
in interface{} in interface{}
expect interface{} expect interface{}