diff --git a/docs/content/content/multilingual.md b/docs/content/content/multilingual.md
index 35dd1382e..f23a6afcf 100644
--- a/docs/content/content/multilingual.md
+++ b/docs/content/content/multilingual.md
@@ -14,6 +14,8 @@ Hugo supports multiple languages side-by-side (added in `Hugo 0.17`). Define the
Example:
```
+DefaultContentLanguage = "en"
+
Languages:
en:
weight: 1
@@ -34,7 +36,9 @@ Anything not defined in a `[lang]:` block will fall back to the global
value for that key (like `copyright` for the English (`en`) language in this example).
With the config above, all content, sitemap, RSS feeds, paginations
-and taxonomy pages will be rendered below `/en` in English, and below `/fr` in French.
+and taxonomy pages will be rendered below `/` in English (your default content language), and below `/fr` in French.
+
+If you want all of the languages to be put below their respective language code, enable `DefaultContentLanguageInSubdir: true` in your configuration.
Only the obvious non-global options can be overridden per language. Examples of global options are `BaseURL`, `BuildDrafts`, etc.
diff --git a/helpers/language.go b/helpers/language.go
index 4ef5edbc4..994129308 100644
--- a/helpers/language.go
+++ b/helpers/language.go
@@ -31,6 +31,10 @@ type Language struct {
paramsInit sync.Once
}
+func (l *Language) String() string {
+ return l.Lang
+}
+
func NewLanguage(lang string) *Language {
return &Language{Lang: lang, params: make(map[string]interface{})}
}
diff --git a/helpers/url.go b/helpers/url.go
index 83273324d..4d06fb042 100644
--- a/helpers/url.go
+++ b/helpers/url.go
@@ -168,21 +168,32 @@ func AbsURL(in string, addLanguage bool) string {
}
if addLanguage {
- addSlash := in == "" || strings.HasSuffix(in, "/")
- in = path.Join(getLanguagePrefix(), in)
+ prefix := getLanguagePrefix()
- if addSlash {
- in += "/"
+ if prefix != "" {
+ addSlash := in == "" || strings.HasSuffix(in, "/")
+ in = path.Join(prefix, in)
+
+ if addSlash {
+ in += "/"
+ }
}
}
return MakePermalink(baseURL, in).String()
}
func getLanguagePrefix() string {
+ defaultLang := viper.GetString("DefaultContentLanguage")
+ defaultInSubDir := viper.GetBool("DefaultContentLanguageInSubdir")
+
if !viper.GetBool("Multilingual") {
return ""
}
- return viper.Get("CurrentContentLanguage").(*Language).Lang
+ currentLang := viper.Get("CurrentContentLanguage").(*Language).Lang
+ if currentLang == "" || (currentLang == defaultLang && !defaultInSubDir) {
+ return ""
+ }
+ return currentLang
}
// IsAbsURL determines whether the given path points to an absolute URL.
@@ -211,12 +222,15 @@ func RelURL(in string, addLanguage bool) string {
}
if addLanguage {
- hadSlash := strings.HasSuffix(u, "/")
+ prefix := getLanguagePrefix()
+ if prefix != "" {
+ hadSlash := strings.HasSuffix(u, "/")
- u = path.Join(getLanguagePrefix(), u)
+ u = path.Join(prefix, u)
- if hadSlash {
- u += "/"
+ if hadSlash {
+ u += "/"
+ }
}
}
diff --git a/helpers/url_test.go b/helpers/url_test.go
index f6dd9f9da..d1fadb252 100644
--- a/helpers/url_test.go
+++ b/helpers/url_test.go
@@ -45,19 +45,24 @@ func TestURLize(t *testing.T) {
}
func TestAbsURL(t *testing.T) {
- for _, addLanguage := range []bool{true, false} {
- for _, m := range []bool{true, false} {
- for _, l := range []string{"en", "fr"} {
- doTestAbsURL(t, addLanguage, m, l)
+ for _, defaultInSubDir := range []bool{true, false} {
+ for _, addLanguage := range []bool{true, false} {
+ for _, m := range []bool{true, false} {
+ for _, l := range []string{"en", "fr"} {
+ doTestAbsURL(t, defaultInSubDir, addLanguage, m, l)
+ }
}
}
}
}
-func doTestAbsURL(t *testing.T, addLanguage, multilingual bool, lang string) {
+func doTestAbsURL(t *testing.T, defaultInSubDir, addLanguage, multilingual bool, lang string) {
viper.Reset()
viper.Set("Multilingual", multilingual)
viper.Set("CurrentContentLanguage", NewLanguage(lang))
+ viper.Set("DefaultContentLanguage", "en")
+ viper.Set("DefaultContentLanguageInSubdir", defaultInSubDir)
+
tests := []struct {
input string
baseURL string
@@ -79,12 +84,17 @@ func doTestAbsURL(t *testing.T, addLanguage, multilingual bool, lang string) {
output := AbsURL(test.input, addLanguage)
expected := test.expected
if multilingual && addLanguage {
- expected = strings.Replace(expected, "MULTI", lang+"/", 1)
+ if !defaultInSubDir && lang == "en" {
+ expected = strings.Replace(expected, "MULTI", "", 1)
+ } else {
+ 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)
+ t.Fatalf("Expected %#v, got %#v\n", expected, output)
}
}
}
@@ -106,19 +116,23 @@ func TestIsAbsURL(t *testing.T) {
}
func TestRelURL(t *testing.T) {
- for _, addLanguage := range []bool{true, false} {
- for _, m := range []bool{true, false} {
- for _, l := range []string{"en", "fr"} {
- doTestRelURL(t, addLanguage, m, l)
+ for _, defaultInSubDir := range []bool{true, false} {
+ for _, addLanguage := range []bool{true, false} {
+ for _, m := range []bool{true, false} {
+ for _, l := range []string{"en", "fr"} {
+ doTestRelURL(t, defaultInSubDir, addLanguage, m, l)
+ }
}
}
}
}
-func doTestRelURL(t *testing.T, addLanguage, multilingual bool, lang string) {
+func doTestRelURL(t *testing.T, defaultInSubDir, addLanguage, multilingual bool, lang string) {
viper.Reset()
viper.Set("Multilingual", multilingual)
viper.Set("CurrentContentLanguage", NewLanguage(lang))
+ viper.Set("DefaultContentLanguage", "en")
+ viper.Set("DefaultContentLanguageInSubdir", defaultInSubDir)
tests := []struct {
input string
@@ -146,7 +160,11 @@ func doTestRelURL(t *testing.T, addLanguage, multilingual bool, lang string) {
expected := test.expected
if multilingual && addLanguage {
- expected = strings.Replace(expected, "MULTI", "/"+lang, 1)
+ if !defaultInSubDir && lang == "en" {
+ expected = strings.Replace(expected, "MULTI", "", 1)
+ } else {
+ expected = strings.Replace(expected, "MULTI", "/"+lang, 1)
+ }
} else {
expected = strings.Replace(expected, "MULTI", "", 1)
}
diff --git a/hugolib/config.go b/hugolib/config.go
index c4292e81b..17673453e 100644
--- a/hugolib/config.go
+++ b/hugolib/config.go
@@ -104,4 +104,5 @@ func loadDefaultSettings() {
viper.SetDefault("UseModTimeAsFallback", false)
viper.SetDefault("Multilingual", false)
viper.SetDefault("DefaultContentLanguage", "en")
+ viper.SetDefault("DefaultContentLanguageInSubdir", false)
}
diff --git a/hugolib/hugo_sites_test.go b/hugolib/hugo_sites_test.go
index 655feba0f..6ab60d9ec 100644
--- a/hugolib/hugo_sites_test.go
+++ b/hugolib/hugo_sites_test.go
@@ -32,12 +32,134 @@ func testCommonResetState() {
viper.SetFs(hugofs.Source())
loadDefaultSettings()
+ // Default is false, but true is easier to use as default in tests
+ viper.Set("DefaultContentLanguageInSubdir", true)
+
if err := hugofs.Source().Mkdir("content", 0755); err != nil {
panic("Content folder creation failed.")
}
}
+func TestMultiSitesMainLangInRoot(t *testing.T) {
+ for _, b := range []bool{false, true} {
+ doTestMultiSitesMainLangInRoot(t, b)
+ }
+}
+
+func doTestMultiSitesMainLangInRoot(t *testing.T, defaultInSubDir bool) {
+ testCommonResetState()
+ viper.Set("DefaultContentLanguageInSubdir", defaultInSubDir)
+
+ sites := createMultiTestSites(t, multiSiteTomlConfig)
+
+ err := sites.Build(BuildCfg{})
+
+ if err != nil {
+ t.Fatalf("Failed to build sites: %s", err)
+ }
+
+ require.Len(t, sites.Sites, 2)
+
+ enSite := sites.Sites[0]
+ frSite := sites.Sites[1]
+
+ require.Equal(t, "/en", enSite.Info.LanguagePrefix)
+
+ if defaultInSubDir {
+ require.Equal(t, "/fr", frSite.Info.LanguagePrefix)
+ } else {
+ require.Equal(t, "", frSite.Info.LanguagePrefix)
+ }
+
+ doc1en := enSite.Pages[0]
+ doc1fr := frSite.Pages[0]
+
+ enPerm, _ := doc1en.Permalink()
+ enRelPerm, _ := doc1en.RelPermalink()
+ require.Equal(t, "http://example.com/blog/en/sect/doc1-slug/", enPerm)
+ require.Equal(t, "/blog/en/sect/doc1-slug/", enRelPerm)
+
+ frPerm, _ := doc1fr.Permalink()
+ frRelPerm, _ := doc1fr.RelPermalink()
+ // Main language in root
+ require.Equal(t, replaceDefaultContentLanguageValue("http://example.com/blog/fr/sect/doc1/", defaultInSubDir), frPerm)
+ require.Equal(t, replaceDefaultContentLanguageValue("/blog/fr/sect/doc1/", defaultInSubDir), frRelPerm)
+
+ assertFileContent(t, "public/fr/sect/doc1/index.html", defaultInSubDir, "Single", "Bonjour")
+ assertFileContent(t, "public/en/sect/doc1-slug/index.html", defaultInSubDir, "Single", "Hello")
+
+ // Check home
+ if defaultInSubDir {
+ // should have a redirect on top level.
+ assertFileContent(t, "public/index.html", true, ``)
+ }
+ assertFileContent(t, "public/fr/index.html", defaultInSubDir, "Home", "Bonjour")
+ assertFileContent(t, "public/en/index.html", defaultInSubDir, "Home", "Hello")
+
+ // Check list pages
+ assertFileContent(t, "public/fr/sect/index.html", defaultInSubDir, "List", "Bonjour")
+ assertFileContent(t, "public/en/sect/index.html", defaultInSubDir, "List", "Hello")
+ assertFileContent(t, "public/fr/plaques/frtag1/index.html", defaultInSubDir, "List", "Bonjour")
+ assertFileContent(t, "public/en/tags/tag1/index.html", defaultInSubDir, "List", "Hello")
+
+ // Check sitemaps
+ // Sitemaps behaves different: In a multilanguage setup there will always be a index file and
+ // one sitemap in each lang folder.
+ assertFileContent(t, "public/sitemap.xml", true,
+ "http:/example.com/blog/en/sitemap.xml",
+ "http:/example.com/blog/fr/sitemap.xml")
+
+ if defaultInSubDir {
+ assertFileContent(t, "public/fr/sitemap.xml", true, "http://example.com/blog/fr/")
+ } else {
+ assertFileContent(t, "public/fr/sitemap.xml", true, "http://example.com/blog/")
+ }
+ assertFileContent(t, "public/en/sitemap.xml", true, "http://example.com/blog/en/")
+
+ // Check rss
+ assertFileContent(t, "public/fr/index.xml", defaultInSubDir, `{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}{{end}}{{ with .Site.Copyright }}
{{.}}{{end}}{{ if not .Date.IsZero }}
{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}{{ end }}
-
+
{{ range first 15 .Data.Pages }}
-
{{ .Title }}