shithub: hugo

Download patch

ref: aa6b1b9be7c9d7322333893b642aaf8c7a5f2c2e
parent: a1d260b41a6673adef679ec4e262c5f390432cf5
author: Bjørn Erik Pedersen <[email protected]>
date: Sun Jul 2 06:46:28 EDT 2017

output: Support templates per site/language

This applies to both regular templates and shortcodes. So, if the site language is French and the output format is AMP, this is the (start) of the lookup order for the home page:

1. index.fr.amp.html
2. index.amp.html
3. index.fr.html
4. index.html
5. ...

Fixes #3360

--- a/hugolib/hugo_sites_build_test.go
+++ b/hugolib/hugo_sites_build_test.go
@@ -305,12 +305,12 @@
 	require.True(t, strings.Contains(languageRedirect, "0; url=http://example.com/blog/fr"), languageRedirect)
 
 	// check home page content (including data files rendering)
-	th.assertFileContent("public/en/index.html", "Home Page 1", "Hello", "Hugo Rocks!")
-	th.assertFileContent("public/fr/index.html", "Home Page 1", "Bonjour", "Hugo Rocks!")
+	th.assertFileContent("public/en/index.html", "Default Home Page 1", "Hello", "Hugo Rocks!")
+	th.assertFileContent("public/fr/index.html", "French Home Page 1", "Bonjour", "Hugo Rocks!")
 
 	// check single page content
-	th.assertFileContent("public/fr/sect/doc1/index.html", "Single", "Shortcode: Bonjour")
-	th.assertFileContent("public/en/sect/doc1-slug/index.html", "Single", "Shortcode: Hello")
+	th.assertFileContent("public/fr/sect/doc1/index.html", "Single", "Shortcode: Bonjour", "LingoFrench")
+	th.assertFileContent("public/en/sect/doc1-slug/index.html", "Single", "Shortcode: Hello", "LingoDefault")
 
 	// Check node translations
 	homeEn := enSite.getPage(KindHome)
@@ -1042,11 +1042,18 @@
 
 	if err := afero.WriteFile(mf,
 		filepath.Join("layouts", "index.html"),
-		[]byte("{{ $p := .Paginator }}Home Page {{ $p.PageNumber }}: {{ .Title }}|{{ .IsHome }}|{{ i18n \"hello\" }}|{{ .Permalink }}|{{  .Site.Data.hugo.slogan }}"),
+		[]byte("{{ $p := .Paginator }}Default Home Page {{ $p.PageNumber }}: {{ .Title }}|{{ .IsHome }}|{{ i18n \"hello\" }}|{{ .Permalink }}|{{  .Site.Data.hugo.slogan }}"),
 		0755); err != nil {
 		t.Fatalf("Failed to write layout file: %s", err)
 	}
 
+	if err := afero.WriteFile(mf,
+		filepath.Join("layouts", "index.fr.html"),
+		[]byte("{{ $p := .Paginator }}French Home Page {{ $p.PageNumber }}: {{ .Title }}|{{ .IsHome }}|{{ i18n \"hello\" }}|{{ .Permalink }}|{{  .Site.Data.hugo.slogan }}"),
+		0755); err != nil {
+		t.Fatalf("Failed to write layout file: %s", err)
+	}
+
 	// Add a shortcode
 	if err := afero.WriteFile(mf,
 		filepath.Join("layouts", "shortcodes", "shortcode.html"),
@@ -1055,6 +1062,21 @@
 		t.Fatalf("Failed to write layout file: %s", err)
 	}
 
+	// A shortcode in multiple languages
+	if err := afero.WriteFile(mf,
+		filepath.Join("layouts", "shortcodes", "lingo.html"),
+		[]byte("LingoDefault"),
+		0755); err != nil {
+		t.Fatalf("Failed to write layout file: %s", err)
+	}
+
+	if err := afero.WriteFile(mf,
+		filepath.Join("layouts", "shortcodes", "lingo.fr.html"),
+		[]byte("LingoFrench"),
+		0755); err != nil {
+		t.Fatalf("Failed to write layout file: %s", err)
+	}
+
 	// Add some language files
 	if err := afero.WriteFile(mf,
 		filepath.Join("i18n", "en.yaml"),
@@ -1098,6 +1120,8 @@
 
 {{< shortcode >}}
 
+{{< lingo >}}
+
 NOTE: slug should be used as URL
 `)},
 		{Name: filepath.FromSlash("sect/doc1.fr.md"), Content: []byte(`---
@@ -1112,6 +1136,8 @@
 *quelque "contenu"*
 
 {{< shortcode >}}
+
+{{< lingo >}}
 
 NOTE: should be in the 'en' Page's 'Translations' field.
 NOTE: date is after "doc3"
--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -250,6 +250,7 @@
 	return output.LayoutDescriptor{
 		Kind:    p.Kind,
 		Type:    p.Type(),
+		Lang:    p.Lang(),
 		Layout:  p.Layout,
 		Section: section,
 	}
--- a/hugolib/shortcode.go
+++ b/hugolib/shortcode.go
@@ -157,6 +157,7 @@
 // Note that in the below, OutputFormat may be empty.
 // We will try to look for the most specific shortcode template available.
 type scKey struct {
+	Lang                 string
 	OutputFormat         string
 	Suffix               string
 	ShortcodePlaceholder string
@@ -166,8 +167,8 @@
 	return scKey{Suffix: m.Suffix, ShortcodePlaceholder: shortcodeplaceholder}
 }
 
-func newScKeyFromOutputFormat(o output.Format, shortcodeplaceholder string) scKey {
-	return scKey{Suffix: o.MediaType.Suffix, OutputFormat: o.Name, ShortcodePlaceholder: shortcodeplaceholder}
+func newScKeyFromLangAndOutputFormat(lang string, o output.Format, shortcodeplaceholder string) scKey {
+	return scKey{Lang: lang, Suffix: o.MediaType.Suffix, OutputFormat: o.Name, ShortcodePlaceholder: shortcodeplaceholder}
 }
 
 func newDefaultScKey(shortcodeplaceholder string) scKey {
@@ -251,10 +252,11 @@
 func prepareShortcodeForPage(placeholder string, sc shortcode, parent *ShortcodeWithPage, p *Page) map[scKey]func() (string, error) {
 
 	m := make(map[scKey]func() (string, error))
+	lang := p.Lang()
 
 	for _, f := range p.outputFormats {
 		// The most specific template will win.
-		key := newScKeyFromOutputFormat(f, placeholder)
+		key := newScKeyFromLangAndOutputFormat(lang, f, placeholder)
 		m[key] = func() (string, error) {
 			return renderShortcode(key, sc, nil, p), nil
 		}
@@ -371,9 +373,11 @@
 
 func (s *shortcodeHandler) contentShortcodesForOutputFormat(f output.Format) map[scKey]func() (string, error) {
 	contentShortcodesForOuputFormat := make(map[scKey]func() (string, error))
+	lang := s.p.Lang()
+
 	for shortcodePlaceholder := range s.shortcodes {
 
-		key := newScKeyFromOutputFormat(f, shortcodePlaceholder)
+		key := newScKeyFromLangAndOutputFormat(lang, f, shortcodePlaceholder)
 		renderFn, found := s.contentShortcodes[key]
 
 		if !found {
@@ -390,7 +394,7 @@
 		if !found {
 			panic(fmt.Sprintf("Shortcode %q could not be found", shortcodePlaceholder))
 		}
-		contentShortcodesForOuputFormat[newScKeyFromOutputFormat(f, shortcodePlaceholder)] = renderFn
+		contentShortcodesForOuputFormat[newScKeyFromLangAndOutputFormat(lang, f, shortcodePlaceholder)] = renderFn
 	}
 
 	return contentShortcodesForOuputFormat
@@ -676,12 +680,19 @@
 
 	suffix := strings.ToLower(key.Suffix)
 	outFormat := strings.ToLower(key.OutputFormat)
+	lang := strings.ToLower(key.Lang)
 
 	if outFormat != "" && suffix != "" {
+		if lang != "" {
+			names = append(names, fmt.Sprintf("%s.%s.%s.%s", shortcodeName, lang, outFormat, suffix))
+		}
 		names = append(names, fmt.Sprintf("%s.%s.%s", shortcodeName, outFormat, suffix))
 	}
 
 	if suffix != "" {
+		if lang != "" {
+			names = append(names, fmt.Sprintf("%s.%s.%s", shortcodeName, lang, suffix))
+		}
 		names = append(names, fmt.Sprintf("%s.%s", shortcodeName, suffix))
 	}
 
--- a/hugolib/shortcode_test.go
+++ b/hugolib/shortcode_test.go
@@ -837,8 +837,8 @@
 func TestScKey(t *testing.T) {
 	require.Equal(t, scKey{Suffix: "xml", ShortcodePlaceholder: "ABCD"},
 		newScKey(media.XMLType, "ABCD"))
-	require.Equal(t, scKey{Suffix: "html", OutputFormat: "AMP", ShortcodePlaceholder: "EFGH"},
-		newScKeyFromOutputFormat(output.AMPFormat, "EFGH"))
+	require.Equal(t, scKey{Lang: "en", Suffix: "html", OutputFormat: "AMP", ShortcodePlaceholder: "EFGH"},
+		newScKeyFromLangAndOutputFormat("en", output.AMPFormat, "EFGH"))
 	require.Equal(t, scKey{Suffix: "html", ShortcodePlaceholder: "IJKL"},
 		newDefaultScKey("IJKL"))
 
--- a/output/docshelper.go
+++ b/output/docshelper.go
@@ -44,6 +44,7 @@
 		f              Format
 	}{
 		{`AMP home, with theme "demoTheme".`, LayoutDescriptor{Kind: "home"}, true, "", AMPFormat},
+		{`AMP home, French language".`, LayoutDescriptor{Kind: "home", Lang: "fr"}, false, "", AMPFormat},
 		{"JSON home, no theme.", LayoutDescriptor{Kind: "home"}, false, "", JSONFormat},
 		{fmt.Sprintf(`CSV regular, "layout: %s" in front matter.`, demoLayout), LayoutDescriptor{Kind: "page", Layout: demoLayout}, false, "", CSVFormat},
 		{fmt.Sprintf(`JSON regular, "type: %s" in front matter.`, demoType), LayoutDescriptor{Kind: "page", Type: demoType}, false, "", JSONFormat},
--- a/output/layout.go
+++ b/output/layout.go
@@ -26,6 +26,7 @@
 	Type    string
 	Section string
 	Kind    string
+	Lang    string
 	Layout  string
 }
 
@@ -55,31 +56,33 @@
 
 const (
 
+	// TODO(bep) variations reduce to 1 "."
+
 	// The RSS templates doesn't map easily into the regular pages.
-	layoutsRSSHome         = `NAME.SUFFIX _default/NAME.SUFFIX _internal/_default/rss.xml`
-	layoutsRSSSection      = `section/SECTION.NAME.SUFFIX _default/NAME.SUFFIX NAME.SUFFIX _internal/_default/rss.xml`
-	layoutsRSSTaxonomy     = `taxonomy/SECTION.NAME.SUFFIX _default/NAME.SUFFIX NAME.SUFFIX _internal/_default/rss.xml`
-	layoutsRSSTaxonomyTerm = `taxonomy/SECTION.terms.NAME.SUFFIX _default/NAME.SUFFIX NAME.SUFFIX _internal/_default/rss.xml`
+	layoutsRSSHome         = `VARIATIONS _default/VARIATIONS _internal/_default/rss.xml`
+	layoutsRSSSection      = `section/SECTION.VARIATIONS _default/VARIATIONS VARIATIONS _internal/_default/rss.xml`
+	layoutsRSSTaxonomy     = `taxonomy/SECTION.VARIATIONS _default/VARIATIONS VARIATIONS _internal/_default/rss.xml`
+	layoutsRSSTaxonomyTerm = `taxonomy/SECTION.terms.VARIATIONS _default/VARIATIONS VARIATIONS _internal/_default/rss.xml`
 
-	layoutsHome    = "index.NAME.SUFFIX index.SUFFIX _default/list.NAME.SUFFIX _default/list.SUFFIX"
+	layoutsHome    = "index.VARIATIONS _default/list.VARIATIONS"
 	layoutsSection = `
-section/SECTION.NAME.SUFFIX section/SECTION.SUFFIX
-SECTION/list.NAME.SUFFIX SECTION/list.SUFFIX
-_default/section.NAME.SUFFIX _default/section.SUFFIX
-_default/list.NAME.SUFFIX _default/list.SUFFIX
-indexes/SECTION.NAME.SUFFIX indexes/SECTION.SUFFIX
-_default/indexes.NAME.SUFFIX _default/indexes.SUFFIX
+section/SECTION.VARIATIONS
+SECTION/list.VARIATIONS
+_default/section.VARIATIONS
+_default/list.VARIATIONS
+indexes/SECTION.VARIATIONS
+_default/indexes.VARIATIONS
 `
 	layoutsTaxonomy = `
-taxonomy/SECTION.NAME.SUFFIX taxonomy/SECTION.SUFFIX
-indexes/SECTION.NAME.SUFFIX indexes/SECTION.SUFFIX 
-_default/taxonomy.NAME.SUFFIX _default/taxonomy.SUFFIX
-_default/list.NAME.SUFFIX _default/list.SUFFIX
+taxonomy/SECTION.VARIATIONS
+indexes/SECTION.VARIATIONS 
+_default/taxonomy.VARIATIONS
+_default/list.VARIATIONS
 `
 	layoutsTaxonomyTerm = `
-taxonomy/SECTION.terms.NAME.SUFFIX taxonomy/SECTION.terms.SUFFIX
-_default/terms.NAME.SUFFIX _default/terms.SUFFIX
-indexes/indexes.NAME.SUFFIX indexes/indexes.SUFFIX
+taxonomy/SECTION.terms.VARIATIONS
+_default/terms.VARIATIONS
+indexes/indexes.VARIATIONS
 `
 )
 
@@ -185,15 +188,42 @@
 }
 
 func resolveTemplate(templ string, d LayoutDescriptor, f Format) []string {
-	delim := "."
-	if f.MediaType.Delimiter == "" {
-		delim = ""
+
+	// VARIATIONS will be replaced with
+	// .lang.name.suffix
+	// .name.suffix
+	// .lang.suffix
+	// .suffix
+	var replacementValues []string
+
+	name := strings.ToLower(f.Name)
+
+	if d.Lang != "" {
+		replacementValues = append(replacementValues, fmt.Sprintf("%s.%s.%s", d.Lang, name, f.MediaType.Suffix))
 	}
-	layouts := strings.Fields(replaceKeyValues(templ,
-		".SUFFIX", delim+f.MediaType.Suffix,
-		"NAME", strings.ToLower(f.Name),
-		"SECTION", d.Section))
 
+	replacementValues = append(replacementValues, fmt.Sprintf("%s.%s", name, f.MediaType.Suffix))
+
+	if d.Lang != "" {
+		replacementValues = append(replacementValues, fmt.Sprintf("%s.%s", d.Lang, f.MediaType.Suffix))
+	}
+
+	isRSS := f.Name == RSSFormat.Name
+
+	if !isRSS {
+		replacementValues = append(replacementValues, f.MediaType.Suffix)
+	}
+
+	var layouts []string
+
+	templFields := strings.Fields(templ)
+
+	for _, field := range templFields {
+		for _, replacements := range replacementValues {
+			layouts = append(layouts, replaceKeyValues(field, "VARIATIONS", replacements, "SECTION", d.Section))
+		}
+	}
+
 	return filterDotLess(layouts)
 }
 
@@ -201,9 +231,7 @@
 	var filteredLayouts []string
 
 	for _, l := range layouts {
-		// This may be constructed, but media types can be suffix-less, but can contain
-		// a delimiter.
-		l = strings.TrimSuffix(l, ".")
+		l = strings.Trim(l, ".")
 		// If media type has no suffix, we have "index" type of layouts in this list, which
 		// doesn't make much sense.
 		if strings.Contains(l, ".") {
--- a/output/layout_test.go
+++ b/output/layout_test.go
@@ -59,6 +59,8 @@
 	}{
 		{"Home", LayoutDescriptor{Kind: "home"}, true, "", ampType,
 			[]string{"index.amp.html", "index.html", "_default/list.amp.html", "_default/list.html", "theme/index.amp.html", "theme/index.html"}},
+		{"Home, french language", LayoutDescriptor{Kind: "home", Lang: "fr"}, true, "", ampType,
+			[]string{"index.fr.amp.html", "index.amp.html", "index.fr.html", "index.html", "_default/list.fr.amp.html", "_default/list.amp.html", "_default/list.fr.html", "_default/list.html", "theme/index.fr.amp.html", "theme/index.amp.html", "theme/index.fr.html"}},
 		{"Home, no ext or delim", LayoutDescriptor{Kind: "home"}, true, "", noExtDelimFormat,
 			[]string{"index.nem", "_default/list.nem"}},
 		{"Home, no ext", LayoutDescriptor{Kind: "home"}, true, "", noExt,