Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/gohugoio/hugo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--commands/benchmark.go4
-rw-r--r--commands/hugo.go73
-rw-r--r--commands/list.go6
-rw-r--r--commands/multilingual.go41
-rw-r--r--docs/content/content/multilingual.md238
-rw-r--r--docs/content/taxonomies/displaying.md8
-rw-r--r--docs/content/taxonomies/ordering.md4
-rw-r--r--docs/content/templates/functions.md26
-rw-r--r--docs/content/templates/terms.md8
-rw-r--r--docs/content/templates/variables.md27
-rw-r--r--helpers/path.go6
-rw-r--r--hugolib/embedded_shortcodes_test.go4
-rw-r--r--hugolib/i18n.go36
-rw-r--r--hugolib/menu_test.go8
-rw-r--r--hugolib/multilingual.go48
-rw-r--r--hugolib/page.go67
-rw-r--r--hugolib/permalinks.go2
-rw-r--r--hugolib/planner.go2
-rw-r--r--hugolib/robotstxt_test.go8
-rw-r--r--hugolib/rss_test.go8
-rw-r--r--hugolib/site.go226
-rw-r--r--hugolib/site_test.go242
-rw-r--r--hugolib/site_url_test.go7
-rw-r--r--hugolib/sitemap_test.go8
-rw-r--r--hugolib/taxonomy_test.go12
-rw-r--r--hugolib/translations.go59
-rw-r--r--source/file.go28
-rw-r--r--tpl/template_funcs.go2
-rw-r--r--tpl/template_i18n.go47
29 files changed, 1013 insertions, 242 deletions
diff --git a/commands/benchmark.go b/commands/benchmark.go
index 3f5aa6ef3..53d2c8e9e 100644
--- a/commands/benchmark.go
+++ b/commands/benchmark.go
@@ -57,7 +57,7 @@ func benchmark(cmd *cobra.Command, args []string) error {
return err
}
for i := 0; i < benchmarkTimes; i++ {
- MainSite = nil
+ MainSites = nil
_ = buildSite()
}
pprof.WriteHeapProfile(f)
@@ -76,7 +76,7 @@ func benchmark(cmd *cobra.Command, args []string) error {
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
for i := 0; i < benchmarkTimes; i++ {
- MainSite = nil
+ MainSites = nil
_ = buildSite()
}
}
diff --git a/commands/hugo.go b/commands/hugo.go
index 7afd78a9d..57a426458 100644
--- a/commands/hugo.go
+++ b/commands/hugo.go
@@ -46,10 +46,10 @@ import (
"github.com/spf13/viper"
)
-// MainSite represents the Hugo site to build. This variable is exported as it
+// MainSites represents the Hugo sites to build. This variable is exported as it
// is used by at least one external library (the Hugo caddy plugin). We should
// provide a cleaner external API, but until then, this is it.
-var MainSite *hugolib.Site
+var MainSites map[string]*hugolib.Site
// Reset resets Hugo ready for a new full build. This is mainly only useful
// for benchmark testing etc. via the CLI commands.
@@ -287,6 +287,7 @@ func loadDefaultSettings() {
viper.SetDefault("ArchetypeDir", "archetypes")
viper.SetDefault("PublishDir", "public")
viper.SetDefault("DataDir", "data")
+ viper.SetDefault("I18nDir", "i18n")
viper.SetDefault("ThemesDir", "themes")
viper.SetDefault("DefaultLayout", "post")
viper.SetDefault("BuildDrafts", false)
@@ -323,6 +324,8 @@ func loadDefaultSettings() {
viper.SetDefault("EnableEmoji", false)
viper.SetDefault("PygmentsCodeFencesGuessSyntax", false)
viper.SetDefault("UseModTimeAsFallback", false)
+ viper.SetDefault("Multilingual", false)
+ viper.SetDefault("DefaultContentLanguage", "en")
}
// InitializeConfig initializes a config file with sensible default configuration flags.
@@ -490,6 +493,8 @@ func InitializeConfig(subCmdVs ...*cobra.Command) error {
helpers.HugoReleaseVersion(), minVersion)
}
+ readMultilingualConfiguration()
+
return nil
}
@@ -506,7 +511,7 @@ func watchConfig() {
viper.OnConfigChange(func(e fsnotify.Event) {
fmt.Println("Config file changed:", e.Name)
// Force a full rebuild
- MainSite = nil
+ MainSites = nil
utils.CheckErr(buildSite(true))
if !viper.GetBool("DisableLiveReload") {
// Will block forever trying to write to a channel that nobody is reading if livereload isn't initialized
@@ -632,6 +637,7 @@ func copyStatic() error {
func getDirList() []string {
var a []string
dataDir := helpers.AbsPathify(viper.GetString("DataDir"))
+ i18nDir := helpers.AbsPathify(viper.GetString("I18nDir"))
layoutDir := helpers.AbsPathify(viper.GetString("LayoutDir"))
staticDir := helpers.AbsPathify(viper.GetString("StaticDir"))
walker := func(path string, fi os.FileInfo, err error) error {
@@ -639,8 +645,13 @@ func getDirList() []string {
if path == dataDir && os.IsNotExist(err) {
jww.WARN.Println("Skip DataDir:", err)
return nil
+ }
+ if path == i18nDir && os.IsNotExist(err) {
+ jww.WARN.Println("Skip I18nDir:", err)
+ return nil
}
+
if path == layoutDir && os.IsNotExist(err) {
jww.WARN.Println("Skip LayoutDir:", err)
return nil
@@ -684,6 +695,7 @@ func getDirList() []string {
helpers.SymbolicWalk(hugofs.Source(), dataDir, walker)
helpers.SymbolicWalk(hugofs.Source(), helpers.AbsPathify(viper.GetString("ContentDir")), walker)
+ helpers.SymbolicWalk(hugofs.Source(), i18nDir, walker)
helpers.SymbolicWalk(hugofs.Source(), helpers.AbsPathify(viper.GetString("LayoutDir")), walker)
helpers.SymbolicWalk(hugofs.Source(), staticDir, walker)
if helpers.ThemeSet() {
@@ -695,31 +707,52 @@ func getDirList() []string {
func buildSite(watching ...bool) (err error) {
fmt.Println("Started building site")
- startTime := time.Now()
- if MainSite == nil {
- MainSite = new(hugolib.Site)
- }
- if len(watching) > 0 && watching[0] {
- MainSite.RunMode.Watching = true
+ t0 := time.Now()
+
+ if MainSites == nil {
+ MainSites = make(map[string]*hugolib.Site)
}
- err = MainSite.Build()
- if err != nil {
- return err
+
+ for _, lang := range langConfigsList {
+ t1 := time.Now()
+ mainSite, present := MainSites[lang]
+ if !present {
+ mainSite = new(hugolib.Site)
+ MainSites[lang] = mainSite
+ mainSite.SetMultilingualConfig(lang, langConfigsList, langConfigs)
+ }
+
+ if len(watching) > 0 && watching[0] {
+ mainSite.RunMode.Watching = true
+ }
+
+ if err := mainSite.Build(); err != nil {
+ return err
+ }
+
+ mainSite.Stats(lang, t1)
}
- MainSite.Stats()
- jww.FEEDBACK.Printf("in %v ms\n", int(1000*time.Since(startTime).Seconds()))
+
+ jww.FEEDBACK.Printf("total in %v ms\n", int(1000*time.Since(t0).Seconds()))
return nil
}
func rebuildSite(events []fsnotify.Event) error {
- startTime := time.Now()
- err := MainSite.ReBuild(events)
- if err != nil {
- return err
+ t0 := time.Now()
+
+ for _, lang := range langConfigsList {
+ t1 := time.Now()
+ mainSite := MainSites[lang]
+
+ if err := mainSite.ReBuild(events); err != nil {
+ return err
+ }
+
+ mainSite.Stats(lang, t1)
}
- MainSite.Stats()
- jww.FEEDBACK.Printf("in %v ms\n", int(1000*time.Since(startTime).Seconds()))
+
+ jww.FEEDBACK.Printf("total in %v ms\n", int(1000*time.Since(t0).Seconds()))
return nil
}
diff --git a/commands/list.go b/commands/list.go
index 5267a4f8b..bc5bb557a 100644
--- a/commands/list.go
+++ b/commands/list.go
@@ -57,7 +57,7 @@ var listDraftsCmd = &cobra.Command{
return newSystemError("Error Processing Source Content", err)
}
- for _, p := range site.Pages {
+ for _, p := range site.AllPages {
if p.IsDraft() {
fmt.Println(filepath.Join(p.File.Dir(), p.File.LogicalName()))
}
@@ -88,7 +88,7 @@ posted in the future.`,
return newSystemError("Error Processing Source Content", err)
}
- for _, p := range site.Pages {
+ for _, p := range site.AllPages {
if p.IsFuture() {
fmt.Println(filepath.Join(p.File.Dir(), p.File.LogicalName()))
}
@@ -119,7 +119,7 @@ expired.`,
return newSystemError("Error Processing Source Content", err)
}
- for _, p := range site.Pages {
+ for _, p := range site.AllPages {
if p.IsExpired() {
fmt.Println(filepath.Join(p.File.Dir(), p.File.LogicalName()))
}
diff --git a/commands/multilingual.go b/commands/multilingual.go
new file mode 100644
index 000000000..68da7c96d
--- /dev/null
+++ b/commands/multilingual.go
@@ -0,0 +1,41 @@
+package commands
+
+import (
+ "sort"
+
+ "github.com/spf13/cast"
+ "github.com/spf13/viper"
+)
+
+var langConfigs map[string]interface{}
+var langConfigsList langConfigsSortable
+
+func readMultilingualConfiguration() {
+ multilingual := viper.GetStringMap("Multilingual")
+ if len(multilingual) == 0 {
+ langConfigsList = append(langConfigsList, "")
+ return
+ }
+
+ langConfigs = make(map[string]interface{})
+ for lang, config := range multilingual {
+ langConfigs[lang] = config
+ langConfigsList = append(langConfigsList, lang)
+ }
+ sort.Sort(langConfigsList)
+}
+
+type langConfigsSortable []string
+
+func (p langConfigsSortable) Len() int { return len(p) }
+func (p langConfigsSortable) Less(i, j int) bool { return weightForLang(p[i]) < weightForLang(p[j]) }
+func (p langConfigsSortable) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
+
+func weightForLang(lang string) int {
+ conf := langConfigs[lang]
+ if conf == nil {
+ return 0
+ }
+ m := cast.ToStringMap(conf)
+ return cast.ToInt(m["weight"])
+}
diff --git a/docs/content/content/multilingual.md b/docs/content/content/multilingual.md
new file mode 100644
index 000000000..8edc6a600
--- /dev/null
+++ b/docs/content/content/multilingual.md
@@ -0,0 +1,238 @@
+---
+date: 2016-01-02T21:21:00Z
+menu:
+ main:
+ parent: content
+next: /content/example
+prev: /content/summaries
+title: Multilingual Mode
+weight: 68
+toc: true
+---
+
+Since version 0.17, Hugo supports a native Multilingual mode. In your
+top-level `config.yaml` (or equivalent), you define the available
+languages in a `Multilingual` section such as:
+
+```
+Multilingual:
+ en:
+ weight: 1
+ title: "My blog"
+ params:
+ linkedin: "english-link"
+ fr:
+ weight: 2
+
+ title: "Mon blog"
+ params:
+ linkedin: "lien-francais"
+ copyright: "Tout est miens"
+
+copyright: "Everything is mine"
+```
+
+Anything not defined in a `[lang]:` block will fall back to the global
+value for that key (like `copyright` for the `en` lang in this
+example).
+
+With the config above, all content, sitemap, RSS feeds, paginations
+and taxonomy pages will be rendered under `/en` in English, and under
+`/fr` in French.
+
+Only those keys are read under `Multilingual`: `weight`, `title`,
+`author`, `social`, `languageCode`, `copyright`, `disqusShortname`,
+`params` (which can contain a map of several other keys).
+
+
+### Translating your content
+
+Translated articles are picked up by the name of the content files.
+
+Example of translated articles:
+
+1. `/content/about.en.md`
+2. `/content/about.fr.md`
+
+You can also have:
+
+1. `/content/about.md`
+2. `/content/about.fr.md`
+
+in which case the config variable `DefaultContentLanguage` will be
+used to affect the default language `about.md`. This way, you can
+slowly start to translate your current content without having to
+rename everything.
+
+If left unspecified, the value for `DefaultContentLanguage` defaults
+to `en`.
+
+By having the same _base file name_, the content pieces are linked
+together as translated pieces. Only the content pieces in the language
+defined by **.Site.CurrentLanguage** will be rendered in a run of
+`hugo`. The translated content will be available in the
+`.Page.Translations` so you can create links to the corresponding
+translated pieces.
+
+
+### Language switching links
+
+Here is a simple example if all your pages are translated:
+
+```
+{{if .IsPage}}
+ {{ range $txLang := .Site.Languages }}
+ {{if isset $.Translations $txLang}}
+ <a href="{{ (index $.Translations $txLang).Permalink }}">{{ i18n ( printf "language_switcher_%s" $txLang ) }}</a>
+ {{end}}
+ {{end}}
+{{end}}
+
+{{if .IsNode}}
+ {{ range $txLang := .Site.Languages }}
+ <a href="/{{$txLang}}">{{ i18n ( printf "language_switcher_%s" $txLang ) }}</a>
+ {{end}}
+{{end}}
+```
+
+This is a more complete example. It handles missing translations and will support non-multilingual sites. Better for theme authors:
+
+```
+{{if .Site.Multilingual}}
+ {{if .IsPage}}
+ {{ range $txLang := .Site.Languages }}
+ {{if isset $.Translations $txLang}}
+ <a href="{{ (index $.Translations $txLang).Permalink }}">{{ i18n ( printf "language_switcher_%s" $txLang ) }}</a>
+ {{else}}
+ <a href="/{{$txLang}}">{{ i18n ( printf "language_switcher_%s" $txLang ) }}</a>
+ {{end}}
+ {{end}}
+ {{end}}
+
+ {{if .IsNode}}
+ {{ range $txLang := .Site.Languages }}
+ <a href="/{{$txLang}}">{{ i18n ( printf "language_switcher_%s" $txLang ) }}</a>
+ {{end}}
+ {{end}}
+{{end}}
+```
+
+This makes use of the **.Site.Languages** variable to create links to
+the other available languages. The order in which the languages are
+listed is defined by the `weight` attribute in each language under
+`Multilingual`.
+
+This will also require you to have some content in your `i18n/` files
+(see below) that would look like:
+
+```
+- id: language_switcher_en
+ translation: "English"
+- id: language_switcher_fr
+ translation: "Français"
+```
+
+and a copy of this in translations for each language.
+
+As you might notice, node pages link to the root of the other
+available translations (`/en`), as those pages do not necessarily have
+a translated counterpart.
+
+Taxonomies (tags, categories) are completely segregated between
+translations and will have their own tag clouds and list views.
+
+
+### Translation of strings
+
+Hugo uses [go-i18n](https://github.com/nicksnyder/go-i18n) to support
+string translations. Follow the link to find tools to manage your
+translation workflows.
+
+Translations are collected from the `themes/[name]/i18n/` folder
+(built into the theme), as well as translations present in `i18n/` at
+the root of your project. In the `i18n`, the translations will be
+merged and take precedence over what is in the theme folder. Files in
+there follow RFC 5646 and should be named something like `en-US.yaml`,
+`fr.yaml`, etc..
+
+From within your templates, use the `i18n` function as such:
+
+```
+{{ i18n "home" }}
+```
+
+to use a definition like this one in `i18n/en-US.yaml`:
+
+```
+- id: home
+ translation: "Home"
+```
+
+
+### Multilingual Themes support
+
+To support Multilingual mode in your themes, you only need to make
+sure URLs defined manually (those not using `.Permalink` or `.URL`
+variables) in your templates are prefixed with `{{
+.Site.LanguagePrefix }}`. If `Multilingual` mode is enabled, 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 non-multilingual sites.
+
+
+### Multilingual index.html and 404.html
+
+To redirect your users to their closest language, drop an `index.html`
+in `/static` of your site, with the following content (tailored to
+your needs) to redirect based on their browser's language:
+
+```
+<html><head>
+<meta http-equiv="refresh" content="1;url=/en" /><!-- just in case JS doesn't work -->
+<script>
+lang = window.navigator.language.substr(0, 2);
+if (lang == "fr") {
+ window.location = "/fr";
+} else {
+ window.location = "/en";
+}
+
+/* or simply:
+window.location = "/en";
+*/
+</script></head><body></body></html>
+```
+
+An even simpler version will always redirect your users to a given language:
+
+```
+<html><head>
+<meta http-equiv="refresh" content="0;url=/en" />
+</head><body></body></html>
+```
+
+You can do something similar with your `404.html` page, as you don't
+know the language of someone arriving at a non-existing page. You
+could inspect the prefix of the navigator path in Javascript or use
+the browser's language detection like above.
+
+
+### Sitemaps
+
+As sitemaps are generated once per language and live in
+`[lang]/sitemap.xml`. Write this content in `static/sitemap.xml` to
+link all your sitemaps together:
+
+```
+<?xml version="1.0" encoding="UTF-8"?>
+<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
+ <sitemap>
+ <loc>https://example.com/en/sitemap.xml</loc>
+ </sitemap>
+ <sitemap>
+ <loc>https://example.com/fr/sitemap.xml</loc>
+ </sitemap>
+</sitemapindex>
+```
+
+and explicitly list all the languages you want referenced.
diff --git a/docs/content/taxonomies/displaying.md b/docs/content/taxonomies/displaying.md
index 8719807f9..c66c3de56 100644
--- a/docs/content/taxonomies/displaying.md
+++ b/docs/content/taxonomies/displaying.md
@@ -38,7 +38,7 @@ each content piece are located in the usual place
<ul id="tags">
{{ range .Params.tags }}
- <li><a href="{{ "/tags/" | relURL }}{{ . | urlize }}">{{ . }}</a> </li>
+ <li><a href="{{ "/tags/" | relLangURL }}{{ . | urlize }}">{{ . }}</a> </li>
{{ end }}
</ul>
@@ -110,7 +110,8 @@ The following example displays all tag keys:
<ul id="all-tags">
{{ range $name, $taxonomy := .Site.Taxonomies.tags }}
- <li><a href="{{ "/tags/" | relURL }}{{ $name | urlize }}">{{ $name }}</a></li>
+<<<<<<< HEAD
+ <li><a href="{{ "/tags/" | relLangURL }}{{ $name | urlize }}">{{ $name }}</a></li>
{{ end }}
</ul>
@@ -120,7 +121,7 @@ This example will list all taxonomies, each of their keys and all the content as
<section>
<ul>
{{ range $taxonomyname, $taxonomy := .Site.Taxonomies }}
- <li><a href="{{ "/" | relURL}}{{ $taxonomyname | urlize }}">{{ $taxonomyname }}</a>
+ <li><a href="{{ "/" | relLangURL}}{{ $taxonomyname | urlize }}">{{ $taxonomyname }}</a>
<ul>
{{ range $key, $value := $taxonomy }}
<li> {{ $key }} </li>
@@ -135,4 +136,3 @@ This example will list all taxonomies, each of their keys and all the content as
{{ end }}
</ul>
</section>
-
diff --git a/docs/content/taxonomies/ordering.md b/docs/content/taxonomies/ordering.md
index baecd7b4c..ac86bc69d 100644
--- a/docs/content/taxonomies/ordering.md
+++ b/docs/content/taxonomies/ordering.md
@@ -29,7 +29,7 @@ Taxonomies can be ordered by either alphabetical key or by the number of content
<ul>
{{ $data := .Data }}
{{ range $key, $value := .Data.Taxonomy.Alphabetical }}
- <li><a href="{{ $data.Plural }}/{{ $value.Name | urlize }}"> {{ $value.Name }} </a> {{ $value.Count }} </li>
+ <li><a href="{{ .Site.LanguagePrefix }}/{{ $data.Plural }}/{{ $value.Name | urlize }}"> {{ $value.Name }} </a> {{ $value.Count }} </li>
{{ end }}
</ul>
@@ -38,7 +38,7 @@ Taxonomies can be ordered by either alphabetical key or by the number of content
<ul>
{{ $data := .Data }}
{{ range $key, $value := .Data.Taxonomy.ByCount }}
- <li><a href="{{ $data.Plural }}/{{ $value.Name | urlize }}"> {{ $value.Name }} </a> {{ $value.Count }} </li>
+ <li><a href="{{ .Site.LanguagePrefix }}/{{ $data.Plural }}/{{ $value.Name | urlize }}"> {{ $value.Name }} </a> {{ $value.Count }} </li>
{{ end }}
</ul>
diff --git a/docs/content/templates/functions.md b/docs/content/templates/functions.md
index cd212b85d..aa55ced4f 100644
--- a/docs/content/templates/functions.md
+++ b/docs/content/templates/functions.md
@@ -435,6 +435,13 @@ e.g.
## Strings
+### printf
+
+Format a string using the standard `fmt.Sprintf` function. See [the go
+doc](https://golang.org/pkg/fmt/) for reference.
+
+e.g., `{{ i18n ( printf "combined_%s" $var ) }}` or `{{ printf "formatted %.2f" 3.1416 }}`
+
### chomp
Removes any trailing newline characters. Useful in a pipeline to remove newlines added by other processing (including `markdownify`).
@@ -726,7 +733,6 @@ CJK-like languages.
<!-- outputs a content length of 8 runes. -->
```
-
### md5
`md5` hashes the given input and returns its MD5 checksum.
@@ -752,6 +758,23 @@ This can be useful if you want to use Gravatar for generating a unique avatar:
<!-- returns the string "c8b5b0e33d408246e30f53e32b8f7627a7a649d4" -->
```
+## Internationalization
+
+### i18n
+
+This translates a piece of content based on your `i18n/en-US.yaml`
+(and friends) files. You can use the
+[go-i18n](https://github.com/nicksnyder/go-i18n) tools to manage your
+translations. The translations can exist in both the theme and at the
+root of your repository.
+
+e.g.: `{{ i18n "translation_id" }}`
+
+
+### T
+
+`T` is an alias to `i18n`. E.g. `{{ T "translation_id" }}`.
+>>>>>>> Add multilingual support in Hugo
## Times
@@ -763,7 +786,6 @@ This can be useful if you want to use Gravatar for generating a unique avatar:
* `{{ (time "2016-05-28").YearDay }}` → 149
* `{{ mul 1000 (time "2016-05-28T10:30:00.00+10:00").Unix }}` → 1464395400000 (Unix time in milliseconds)
-
## URLs
### absURL, relURL
diff --git a/docs/content/templates/terms.md b/docs/content/templates/terms.md
index 3e6b43878..3711fd8aa 100644
--- a/docs/content/templates/terms.md
+++ b/docs/content/templates/terms.md
@@ -89,7 +89,7 @@ content tagged with each tag.
<ul>
{{ $data := .Data }}
{{ range $key, $value := .Data.Terms }}
- <li><a href="{{ $data.Plural }}/{{ $key | urlize }}">{{ $key }}</a> {{ len $value }}</li>
+ <li><a href="{{ .Site.LanguagePrefix }}/{{ $data.Plural }}/{{ $key | urlize }}">{{ $key }}</a> {{ len $value }}</li>
{{ end }}
</ul>
</div>
@@ -109,7 +109,7 @@ Another example listing the content for each term (ordered by Date):
{{ $data := .Data }}
{{ range $key,$value := .Data.Terms.ByCount }}
- <h2><a href="{{ $data.Plural }}/{{ $value.Name | urlize }}">{{ $value.Name }}</a> {{ $value.Count }}</h2>
+ <h2><a href="{{ .Site.LanguagePrefix }}/{{ $data.Plural }}/{{ $value.Name | urlize }}">{{ $value.Name }}</a> {{ $value.Count }}</h2>
<ul>
{{ range $value.Pages.ByDate }}
<li><a href="{{ .Permalink }}">{{ .Title }}</a></li>
@@ -140,7 +140,7 @@ Hugo can order the meta data in two different ways. It can be ordered:
<ul>
{{ $data := .Data }}
{{ range $key, $value := .Data.Terms.Alphabetical }}
- <li><a href="{{ $data.Plural }}/{{ $value.Name | urlize }}">{{ $value.Name }}</a> {{ $value.Count }}</li>
+ <li><a href="{{ .Site.LanguagePrefix }}/{{ $data.Plural }}/{{ $value.Name | urlize }}">{{ $value.Name }}</a> {{ $value.Count }}</li>
{{ end }}
</ul>
</div>
@@ -158,7 +158,7 @@ Hugo can order the meta data in two different ways. It can be ordered:
<ul>
{{ $data := .Data }}
{{ range $key, $value := .Data.Terms.ByCount }}
- <li><a href="{{ $data.Plural }}/{{ $value.Name | urlize }}">{{ $value.Name }}</a> {{ $value.Count }}</li>
+ <li><a href="{{ .Site.LanguagePrefix }}/{{ $data.Plural }}/{{ $value.Name | urlize }}">{{ $value.Name }}</a> {{ $value.Count }}</li>
{{ end }}
</ul>
</div>
diff --git a/docs/content/templates/variables.md b/docs/content/templates/variables.md
index 953ba5cb6..8f37dd19f 100644
--- a/docs/content/templates/variables.md
+++ b/docs/content/templates/variables.md
@@ -58,6 +58,8 @@ matter, content or derived from file location.
**.IsPage** Always true for page.<br>
**.Site** See [Site Variables]({{< relref "#site-variables" >}}) below.<br>
**.Hugo** See [Hugo Variables]({{< relref "#hugo-variables" >}}) below.<br>
+**.Translations** A map to other pages with the same filename, but with a different language-extension (like `post.fr.md`). Populated only if `Multilingual` is enabled in your site config.
+**.Lang** Taken from the language extension notation. Populated only if `Multilingual` is enabled for your site config.
## Page Params
@@ -119,9 +121,9 @@ includes taxonomies, lists and the homepage.
**.Site** See [Site Variables]({{< relref "#site-variables" >}}) below.<br>
**.Hugo** See [Hugo Variables]({{< relref "#hugo-variables" >}}) below.<br>
-### Taxonomy Term Variables
+### Taxonomy Terms Node Variables
-[Taxonomy Terms](/templates/terms/) pages are of the type "node" and have the following additional variables.
+[Taxonomy Terms](/templates/terms/) pages are of the type "node" and have the following additional variables. These are available in `layouts/_defaults/terms.html` for example.
**.Data.Singular** The singular name of the taxonomy<br>
**.Data.Plural** The plural name of the taxonomy<br>
@@ -132,14 +134,25 @@ includes taxonomies, lists and the homepage.
The last two can also be reversed: **.Data.Terms.Alphabetical.Reverse**, **.Data.Terms.ByCount.Reverse**.
+### Taxonomies elsewhere
+
+The **.Site.Taxonomies** variable holds all taxonomies defines site-wide. It is a map of the taxonomy name to a list of its values. For example: "tags" -> ["tag1", "tag2", "tag3"]. Each value, though, is not a string but rather a [Taxonomy variable](#the-taxonomy-variable).
+
+#### The Taxonomy variable
+
+The Taxonomy variable, available as **.Site.Taxonomies.tags** for example, contains the list of tags (values) and, for each of those, their corresponding content pages.
+
+
+
## Site Variables
Also available is `.Site` which has the following:
**.Site.BaseURL** The base URL for the site as defined in the site configuration file.<br>
**.Site.RSSLink** The URL for the site RSS.<br>
-**.Site.Taxonomies** The [taxonomies](/taxonomies/usage/) for the entire site. Replaces the now-obsolete `.Site.Indexes` since v0.11.<br>
-**.Site.Pages** Array of all content ordered by Date, newest first. Replaces the now-deprecated `.Site.Recent` starting v0.13.<br>
+**.Site.Taxonomies** The [taxonomies](/taxonomies/usage/) for the entire site. Replaces the now-obsolete `.Site.Indexes` since v0.11. Also see section [Taxonomies elsewhere](#taxonomies-elsewhere).<br>
+**.Site.Pages** Array of all content ordered by Date, newest first. Replaces the now-deprecated `.Site.Recent` starting v0.13. This array contains only the pages in the current language.<br>
+**.Site.AllPages** Array of all pages regardless of their translation.<br>
**.Site.Params** A container holding the values from the `params` section of your site configuration file. For example, a TOML config file might look like this:
baseurl = "http://yoursite.example.com/"
@@ -152,7 +165,7 @@ Also available is `.Site` which has the following:
**.Site.Menus** All of the menus in the site.<br>
**.Site.Title** A string representing the title of the site.<br>
**.Site.Author** A map of the authors as defined in the site configuration.<br>
-**.Site.LanguageCode** A string representing the language as defined in the site configuration.<br>
+**.Site.LanguageCode** A string representing the language as defined in the site configuration. This is mostly used to populate the RSS feeds with the right language code.<br>
**.Site.DisqusShortname** A string representing the shortname of the Disqus shortcode as defined in the site configuration.<br>
**.Site.GoogleAnalytics** A string representing your tracking code for Google Analytics as defined in the site configuration.<br>
**.Site.Copyright** A string representing the copyright of your web site as defined in the site configuration.<br>
@@ -160,6 +173,10 @@ Also available is `.Site` which has the following:
**.Site.Permalinks** A string to override the default permalink format. Defined in the site configuration.<br>
**.Site.BuildDrafts** A boolean (Default: false) to indicate whether to build drafts. Defined in the site configuration.<br>
**.Site.Data** Custom data, see [Data Files](/extras/datafiles/).<br>
+**.Site.Multilingual** Whether the site supports internationalization of the content. With this mode enabled, all your posts' URLs will be prefixed with the language (ex: `/en/2016/01/01/my-post`)<br>
+**.Site.CurrentLanguage** This indicates which language you are currently rendering the website for. When using `Multilingual` mode, will render the site in this language. You can then run `hugo` again with a second `config` file, with the other languages. When using `i18n` and `T` template functions, it will use the `i18n/*.yaml` files (in either `/themes/[yourtheme]/i18n` or the `/i18n`, translations in the latter having precedence).<br>
+**.Site.LanguagePrefix** When `Multilingual` is enabled, this will hold `/{{ .Site.CurrentLanguage}}`, otherwise will be an empty string. Using this to prefix taxonomies or other hard-coded links ensures your keep your theme compatible with Multilingual configurations.
+**.Site.Languages** An ordered list of languages when Multilingual is enabled. Used in your templates to iterate through and create links to different languages.<br>
## File Variables
diff --git a/helpers/path.go b/helpers/path.go
index 0c18e5397..a8a50aab3 100644
--- a/helpers/path.go
+++ b/helpers/path.go
@@ -182,6 +182,12 @@ func GetThemeDataDirPath() (string, error) {
return getThemeDirPath("data")
}
+// GetThemeI18nDirPath returns the theme's i18n dir path if theme is set.
+// If theme is set and the i18n dir doesn't exist, an error is returned.
+func GetThemeI18nDirPath() (string, error) {
+ return getThemeDirPath("i18n")
+}
+
func getThemeDirPath(path string) (string, error) {
if !ThemeSet() {
return "", errors.New("No theme set")
diff --git a/hugolib/embedded_shortcodes_test.go b/hugolib/embedded_shortcodes_test.go
index 360ec1209..18f807fbf 100644
--- a/hugolib/embedded_shortcodes_test.go
+++ b/hugolib/embedded_shortcodes_test.go
@@ -56,8 +56,8 @@ func doTestShortcodeCrossrefs(t *testing.T, relative bool) {
templ := tpl.New()
p, _ := pageFromString(simplePageWithURL, path)
p.Node.Site = &SiteInfo{
- Pages: &(Pages{p}),
- BaseURL: template.URL(helpers.SanitizeURLKeepTrailingSlash(baseURL)),
+ AllPages: &(Pages{p}),
+ BaseURL: template.URL(helpers.SanitizeURLKeepTrailingSlash(baseURL)),
}
output, err := HandleShortcodes(in, p, templ)
diff --git a/hugolib/i18n.go b/hugolib/i18n.go
new file mode 100644
index 000000000..cb5061501
--- /dev/null
+++ b/hugolib/i18n.go
@@ -0,0 +1,36 @@
+// Copyright 2016 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
+
+import (
+ "github.com/nicksnyder/go-i18n/i18n/bundle"
+ "github.com/spf13/hugo/source"
+ "github.com/spf13/hugo/tpl"
+)
+
+func loadI18n(sources []source.Input, lang string) (err error) {
+ i18nBundle := bundle.New()
+ for _, currentSource := range sources {
+ for _, r := range currentSource.Files() {
+ err = i18nBundle.ParseTranslationFileBytes(r.LogicalName(), r.Bytes())
+ if err != nil {
+ return
+ }
+ }
+ }
+
+ tpl.SetI18nTfunc(lang, i18nBundle)
+
+ return nil
+}
diff --git a/hugolib/menu_test.go b/hugolib/menu_test.go
index df6ccf629..602775a4b 100644
--- a/hugolib/menu_test.go
+++ b/hugolib/menu_test.go
@@ -691,13 +691,7 @@ func testSiteSetup(s *Site, t *testing.T) {
s.Menus = Menus{}
s.initializeSiteInfo()
- if err := s.createPages(); err != nil {
- t.Fatalf("Unable to create pages: %s", err)
- }
-
- if err := s.buildSiteMeta(); err != nil {
- t.Fatalf("Unable to build site metadata: %s", err)
- }
+ createPagesAndMeta(t, s)
}
func tomlToMap(s string) (map[string]interface{}, error) {
diff --git a/hugolib/multilingual.go b/hugolib/multilingual.go
new file mode 100644
index 000000000..2552bad4c
--- /dev/null
+++ b/hugolib/multilingual.go
@@ -0,0 +1,48 @@
+package hugolib
+
+import (
+ "github.com/spf13/cast"
+ "github.com/spf13/viper"
+)
+
+type Multilingual struct {
+ enabled bool
+ config *viper.Viper
+
+ Languages []string
+}
+
+func (ml *Multilingual) GetString(key string) string { return cast.ToString(ml.Get(key)) }
+func (ml *Multilingual) GetStringMap(key string) map[string]interface{} {
+ return cast.ToStringMap(ml.Get(key))
+}
+
+func (ml *Multilingual) GetStringMapString(key string) map[string]string {
+ return cast.ToStringMapString(ml.Get(key))
+}
+
+func (ml *Multilingual) Get(key string) interface{} {
+ if ml != nil && ml.config != nil && ml.config.IsSet(key) {
+ return ml.config.Get(key)
+ }
+ return viper.Get(key)
+}
+
+func (s *Site) SetMultilingualConfig(currentLang string, orderedLanguages []string, langConfigs map[string]interface{}) {
+ conf := viper.New()
+ for k, val := range cast.ToStringMap(langConfigs[currentLang]) {
+ conf.Set(k, val)
+ }
+ conf.Set("CurrentLanguage", currentLang)
+ ml := &Multilingual{
+ enabled: len(langConfigs) > 0,
+ config: conf,
+ Languages: orderedLanguages,
+ }
+ viper.Set("Multilingual", ml.enabled)
+ s.Multilingual = ml
+}
+
+func (s *Site) multilingualEnabled() bool {
+ return s.Multilingual != nil && s.Multilingual.enabled
+}
diff --git a/hugolib/page.go b/hugolib/page.go
index f64cdf95f..5c281bb96 100644
--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -61,8 +61,10 @@ type Page struct {
PublishDate time.Time
ExpiryDate time.Time
Markup string
+ Translations Translations
extension string
contentType string
+ lang string
renderable bool
Layout string
layoutsCalculated []string
@@ -300,9 +302,11 @@ func (p *Page) getRenderingConfig() *helpers.Blackfriday {
func newPage(filename string) *Page {
page := Page{contentType: "",
- Source: Source{File: *source.NewFile(filename)},
- Node: Node{Keywords: []string{}, Sitemap: Sitemap{Priority: -1}},
- Params: make(map[string]interface{})}
+ Source: Source{File: *source.NewFile(filename)},
+ Node: Node{Keywords: []string{}, Sitemap: Sitemap{Priority: -1}},
+ Params: make(map[string]interface{}),
+ Translations: make(Translations),
+ }
jww.DEBUG.Println("Reading from", page.File.Path())
return &page
@@ -445,11 +449,13 @@ func (p *Page) permalink() (*url.URL, error) {
if len(pSlug) > 0 {
permalink = helpers.URLPrep(viper.GetBool("UglyURLs"), path.Join(dir, p.Slug+"."+p.Extension()))
} else {
- _, t := filepath.Split(p.Source.LogicalName())
+ t := p.Source.TranslationBaseName()
permalink = helpers.URLPrep(viper.GetBool("UglyURLs"), path.Join(dir, helpers.ReplaceExtension(strings.TrimSpace(t), p.Extension())))
}
}
+ permalink = p.addMultilingualWebPrefix(permalink)
+
return helpers.MakePermalink(baseURL, permalink), nil
}
@@ -460,6 +466,10 @@ func (p *Page) Extension() string {
return viper.GetString("DefaultExtension")
}
+func (p *Page) Lang() string {
+ return p.lang
+}
+
func (p *Page) LinkTitle() string {
if len(p.linkTitle) > 0 {
return p.linkTitle
@@ -699,29 +709,29 @@ func (p *Page) getParam(key string, stringToLower bool) interface{} {
return nil
}
- switch v.(type) {
+ switch val := v.(type) {
case bool:
- return v
- case time.Time:
- return v
+ return val
+ case string:
+ if stringToLower {
+ return strings.ToLower(val)
+ }
+ return val
case int64, int32, int16, int8, int:
return cast.ToInt(v)
case float64, float32:
return cast.ToFloat64(v)
- case map[string]interface{}: // JSON and TOML
- return v
- case map[interface{}]interface{}: // YAML
- return v
- case string:
- if stringToLower {
- return strings.ToLower(v.(string))
- }
- return v
+ case time.Time:
+ return val
case []string:
if stringToLower {
- return helpers.SliceToLower(v.([]string))
+ return helpers.SliceToLower(val)
}
return v
+ case map[string]interface{}: // JSON and TOML
+ return v
+ case map[interface{}]interface{}: // YAML
+ return v
}
jww.ERROR.Printf("GetParam(\"%s\"): Unknown type %s\n", key, reflect.TypeOf(v))
@@ -851,6 +861,7 @@ func (p *Page) parse(reader io.Reader) error {
p.renderable = psr.IsRenderable()
p.frontmatter = psr.FrontMatter()
p.rawContent = psr.Content()
+ p.lang = p.Source.File.Lang()
meta, err := psr.Metadata()
if meta != nil {
@@ -975,7 +986,6 @@ func (p *Page) FullFilePath() string {
}
func (p *Page) TargetPath() (outfile string) {
-
// Always use URL if it's specified
if len(strings.TrimSpace(p.URL)) > 2 {
outfile = strings.TrimSpace(p.URL)
@@ -997,6 +1007,7 @@ func (p *Page) TargetPath() (outfile string) {
outfile += "index.html"
}
outfile = filepath.FromSlash(outfile)
+ outfile = p.addMultilingualFilesystemPrefix(outfile)
return
}
}
@@ -1005,8 +1016,22 @@ func (p *Page) TargetPath() (outfile string) {
outfile = strings.TrimSpace(p.Slug) + "." + p.Extension()
} else {
// Fall back to filename
- outfile = helpers.ReplaceExtension(p.Source.LogicalName(), p.Extension())
+ outfile = helpers.ReplaceExtension(p.Source.TranslationBaseName(), p.Extension())
}
- return filepath.Join(strings.ToLower(helpers.MakePath(p.Source.Dir())), strings.TrimSpace(outfile))
+ return p.addMultilingualFilesystemPrefix(filepath.Join(strings.ToLower(helpers.MakePath(p.Source.Dir())), strings.TrimSpace(outfile)))
+}
+
+func (p *Page) addMultilingualWebPrefix(outfile string) string {
+ if p.Lang() == "" {
+ return outfile
+ }
+ return "/" + path.Join(p.Lang(), outfile)
+}
+
+func (p *Page) addMultilingualFilesystemPrefix(outfile string) string {
+ if p.Lang() == "" {
+ return outfile
+ }
+ return string(filepath.Separator) + filepath.Join(p.Lang(), outfile)
}
diff --git a/hugolib/permalinks.go b/hugolib/permalinks.go
index 497fc10b2..581f8740f 100644
--- a/hugolib/permalinks.go
+++ b/hugolib/permalinks.go
@@ -159,7 +159,7 @@ func pageToPermalinkTitle(p *Page, _ string) (string, error) {
func pageToPermalinkFilename(p *Page, _ string) (string, error) {
//var extension = p.Source.Ext
//var name = p.Source.Path()[0 : len(p.Source.Path())-len(extension)]
- return helpers.URLize(p.Source.BaseFileName()), nil
+ return helpers.URLize(p.Source.TranslationBaseName()), nil
}
// if the page has a slug, return the slug, else return the title
diff --git a/hugolib/planner.go b/hugolib/planner.go
index 22dd2586b..6086718c0 100644
--- a/hugolib/planner.go
+++ b/hugolib/planner.go
@@ -25,7 +25,7 @@ func (s *Site) ShowPlan(out io.Writer) (err error) {
fmt.Fprintf(out, "No source files provided.\n")
}
- for _, p := range s.Pages {
+ for _, p := range s.AllPages {
fmt.Fprintf(out, "%s", p.Source.Path())
if p.IsRenderable() {
fmt.Fprintf(out, " (renderer: markdown)")
diff --git a/hugolib/robotstxt_test.go b/hugolib/robotstxt_test.go
index 776ff3aca..461d04e5d 100644
--- a/hugolib/robotstxt_test.go
+++ b/hugolib/robotstxt_test.go
@@ -46,13 +46,7 @@ func TestRobotsTXTOutput(t *testing.T) {
s.prepTemplates("robots.txt", robotTxtTemplate)
- if err := s.createPages(); err != nil {
- t.Fatalf("Unable to create pages: %s", err)
- }
-
- if err := s.buildSiteMeta(); err != nil {
- t.Fatalf("Unable to build site metadata: %s", err)
- }
+ createPagesAndMeta(t, s)
if err := s.renderHomePage(); err != nil {
t.Fatalf("Unable to RenderHomePage: %s", err)
diff --git a/hugolib/rss_test.go b/hugolib/rss_test.go
index 5eca01143..ec6ee87bd 100644
--- a/hugolib/rss_test.go
+++ b/hugolib/rss_test.go
@@ -59,13 +59,7 @@ func TestRSSOutput(t *testing.T) {
s.initializeSiteInfo()
s.prepTemplates("rss.xml", rssTemplate)
- if err := s.createPages(); err != nil {
- t.Fatalf("Unable to create pages: %s", err)
- }
-
- if err := s.buildSiteMeta(); err != nil {
- t.Fatalf("Unable to build site metadata: %s", err)
- }
+ createPagesAndMeta(t, s)
if err := s.renderHomePage(); err != nil {
t.Fatalf("Unable to RenderHomePage: %s", err)
diff --git a/hugolib/site.go b/hugolib/site.go
index c5851e813..308e35c90 100644
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -20,6 +20,7 @@ import (
"io"
"net/url"
"os"
+ "path"
"path/filepath"
"sort"
"strconv"
@@ -29,8 +30,6 @@ import (
"sync/atomic"
- "path"
-
"github.com/bep/inflect"
"github.com/fsnotify/fsnotify"
"github.com/spf13/afero"
@@ -76,6 +75,7 @@ var distinctErrorLogger = helpers.NewDistinctErrorLogger()
// 5. The entire collection of files is written to disk.
type Site struct {
Pages Pages
+ AllPages Pages
Files []*source.File
Tmpl tpl.Template
Taxonomies TaxonomyList
@@ -87,6 +87,7 @@ type Site struct {
targets targetList
targetListInit sync.Once
RunMode runmode
+ Multilingual *Multilingual
draftCount int
futureCount int
expiredCount int
@@ -106,7 +107,8 @@ type SiteInfo struct {
Authors AuthorList
Social SiteSocial
Sections Taxonomy
- Pages *Pages
+ Pages *Pages // Includes only pages in this language
+ AllPages *Pages // Includes other translated pages, excluding those in this language.
Files *[]*source.File
Menus *Menus
Hugo *HugoInfo
@@ -125,6 +127,11 @@ type SiteInfo struct {
preserveTaxonomyNames bool
paginationPageCount uint64
Data *map[string]interface{}
+
+ Multilingual bool
+ CurrentLanguage string
+ LanguagePrefix string
+ Languages []string
}
// SiteSocial is a place to put social details on a site level. These are the
@@ -150,17 +157,17 @@ func (s *SiteInfo) GetParam(key string) interface{} {
return nil
}
- switch v.(type) {
+ switch val := v.(type) {
case bool:
- return cast.ToBool(v)
+ return val
case string:
- return cast.ToString(v)
+ return val
case int64, int32, int16, int8, int:
return cast.ToInt(v)
case float64, float32:
return cast.ToFloat64(v)
case time.Time:
- return cast.ToTime(v)
+ return val
case []string:
return v
}
@@ -181,7 +188,7 @@ func (s *SiteInfo) refLink(ref string, page *Page, relative bool) (string, error
var link string
if refURL.Path != "" {
- for _, page := range []*Page(*s.Pages) {
+ for _, page := range []*Page(*s.AllPages) {
refPath := filepath.FromSlash(refURL.Path)
if page.Source.Path() == refPath || page.Source.LogicalName() == refPath {
target = page
@@ -256,7 +263,7 @@ func (s *SiteInfo) SourceRelativeLink(ref string, currentPage *Page) (string, er
}
}
- for _, page := range []*Page(*s.Pages) {
+ for _, page := range []*Page(*s.AllPages) {
if page.Source.Path() == refPath {
target = page
break
@@ -265,14 +272,14 @@ func (s *SiteInfo) SourceRelativeLink(ref string, currentPage *Page) (string, er
// need to exhaust the test, then try with the others :/
// if the refPath doesn't end in a filename with extension `.md`, then try with `.md` , and then `/index.md`
mdPath := strings.TrimSuffix(refPath, string(os.PathSeparator)) + ".md"
- for _, page := range []*Page(*s.Pages) {
+ for _, page := range []*Page(*s.AllPages) {
if page.Source.Path() == mdPath {
target = page
break
}
}
indexPath := filepath.Join(refPath, "index.md")
- for _, page := range []*Page(*s.Pages) {
+ for _, page := range []*Page(*s.AllPages) {
if page.Source.Path() == indexPath {
target = page
break
@@ -443,7 +450,7 @@ func (s *Site) ReBuild(events []fsnotify.Event) error {
// If a content file changes, we need to reload only it and re-render the entire site.
- // First step is to read the changed files and (re)place them in site.Pages
+ // First step is to read the changed files and (re)place them in site.AllPages
// This includes processing any meta-data for that content
// The second step is to convert the content into HTML
@@ -479,7 +486,7 @@ func (s *Site) ReBuild(events []fsnotify.Event) error {
if len(tmplChanged) > 0 || len(dataChanged) > 0 {
// Do not need to read the files again, but they need conversion
// for shortocde re-rendering.
- for _, p := range s.Pages {
+ for _, p := range s.AllPages {
pageChan <- p
}
}
@@ -538,6 +545,9 @@ func (s *Site) ReBuild(events []fsnotify.Event) error {
s.timerStep("read & convert pages from source")
+ // FIXME: does this go inside the next `if` statement ?
+ s.setupTranslations()
+
if len(sourceChanged) > 0 {
s.setupPrevNext()
if err = s.buildSiteMeta(); err != nil {
@@ -665,9 +675,9 @@ func (s *Site) readDataFromSourceFS() error {
dataSources = append(dataSources, &source.Filesystem{Base: s.absDataDir()})
// have to be last - duplicate keys in earlier entries will win
- themeStaticDir, err := helpers.GetThemeDataDirPath()
+ themeDataDir, err := helpers.GetThemeDataDirPath()
if err == nil {
- dataSources = append(dataSources, &source.Filesystem{Base: themeStaticDir})
+ dataSources = append(dataSources, &source.Filesystem{Base: themeDataDir})
}
err = s.loadData(dataSources)
@@ -688,10 +698,25 @@ func (s *Site) Process() (err error) {
return
}
+ i18nSources := []source.Input{&source.Filesystem{Base: s.absI18nDir()}}
+
+ themeI18nDir, err := helpers.GetThemeI18nDirPath()
+ if err == nil {
+ i18nSources = []source.Input{&source.Filesystem{Base: themeI18nDir}, i18nSources[0]}
+ }
+
+ if err = loadI18n(i18nSources, s.Multilingual.GetString("CurrentLanguage")); err != nil {
+ return
+ }
+ s.timerStep("load i18n")
+
if err = s.createPages(); err != nil {
return
}
+
+ s.setupTranslations()
s.setupPrevNext()
+
if err = s.buildSiteMeta(); err != nil {
return
}
@@ -711,6 +736,27 @@ func (s *Site) setupPrevNext() {
}
}
+func (s *Site) setupTranslations() {
+ if !s.multilingualEnabled() {
+ s.Pages = s.AllPages
+ return
+ }
+
+ currentLang := s.Multilingual.GetString("CurrentLanguage")
+
+ allTranslations := pagesToTranslationsMap(s.AllPages)
+ assignTranslationsToPages(allTranslations, s.AllPages)
+
+ var currentLangPages []*Page
+ for _, p := range s.AllPages {
+ if p.Lang() == "" || strings.HasPrefix(currentLang, p.lang) {
+ currentLangPages = append(currentLangPages, p)
+ }
+ }
+
+ s.Pages = currentLangPages
+}
+
func (s *Site) Render() (err error) {
if err = s.renderAliases(); err != nil {
return
@@ -771,32 +817,47 @@ func (s *Site) initialize() (err error) {
}
func (s *Site) initializeSiteInfo() {
- params := viper.GetStringMap("Params")
+ params := s.Multilingual.GetStringMap("Params")
permalinks := make(PermalinkOverrides)
for k, v := range viper.GetStringMapString("Permalinks") {
permalinks[k] = pathPattern(v)
}
+ languagePrefix := ""
+ if s.multilingualEnabled() {
+ languagePrefix = "/" + s.Multilingual.GetString("CurrentLanguage")
+ }
+
+ languages := []string{}
+ if s.Multilingual != nil {
+ languages = s.Multilingual.Languages
+ }
+
s.Info = SiteInfo{
BaseURL: template.URL(helpers.SanitizeURLKeepTrailingSlash(viper.GetString("BaseURL"))),
- Title: viper.GetString("Title"),
- Author: viper.GetStringMap("author"),
- Social: viper.GetStringMapString("social"),
- LanguageCode: viper.GetString("languagecode"),
- Copyright: viper.GetString("copyright"),
- DisqusShortname: viper.GetString("DisqusShortname"),
+ Title: s.Multilingual.GetString("Title"),
+ Author: s.Multilingual.GetStringMap("author"),
+ Social: s.Multilingual.GetStringMapString("social"),
+ LanguageCode: s.Multilingual.GetString("languagecode"),
+ Copyright: s.Multilingual.GetString("copyright"),
+ DisqusShortname: s.Multilingual.GetString("DisqusShortname"),
+ Multilingual: s.multilingualEnabled(),
+ CurrentLanguage: s.Multilingual.GetString("CurrentLanguage"),
+ LanguagePrefix: languagePrefix,
+ Languages: languages,
GoogleAnalytics: viper.GetString("GoogleAnalytics"),
RSSLink: s.permalinkStr(viper.GetString("RSSUri")),
BuildDrafts: viper.GetBool("BuildDrafts"),
canonifyURLs: viper.GetBool("CanonifyURLs"),
preserveTaxonomyNames: viper.GetBool("PreserveTaxonomyNames"),
- Pages: &s.Pages,
- Files: &s.Files,
- Menus: &s.Menus,
- Params: params,
- Permalinks: permalinks,
- Data: &s.Data,
+ AllPages: &s.AllPages,
+ Pages: &s.Pages,
+ Files: &s.Files,
+ Menus: &s.Menus,
+ Params: params,
+ Permalinks: permalinks,
+ Data: &s.Data,
}
}
@@ -808,6 +869,10 @@ func (s *Site) absDataDir() string {
return helpers.AbsPathify(viper.GetString("DataDir"))
}
+func (s *Site) absI18nDir() string {
+ return helpers.AbsPathify(viper.GetString("I18nDir"))
+}
+
func (s *Site) absThemeDir() string {
return helpers.AbsPathify(viper.GetString("themesDir") + "/" + viper.GetString("theme"))
}
@@ -903,7 +968,7 @@ func (s *Site) convertSource() chan error {
go converterCollator(s, results, errs)
- for _, p := range s.Pages {
+ for _, p := range s.AllPages {
pageChan <- p
}
@@ -997,7 +1062,7 @@ func converterCollator(s *Site, results <-chan HandledResult, errs chan<- error)
func (s *Site) addPage(page *Page) {
if page.shouldBuild() {
- s.Pages = append(s.Pages, page)
+ s.AllPages = append(s.AllPages, page)
}
if page.IsDraft() {
@@ -1014,8 +1079,8 @@ func (s *Site) addPage(page *Page) {
}
func (s *Site) removePageByPath(path string) {
- if i := s.Pages.FindPagePosByFilePath(path); i >= 0 {
- page := s.Pages[i]
+ if i := s.AllPages.FindPagePosByFilePath(path); i >= 0 {
+ page := s.AllPages[i]
if page.IsDraft() {
s.draftCount--
@@ -1029,12 +1094,12 @@ func (s *Site) removePageByPath(path string) {
s.expiredCount--
}
- s.Pages = append(s.Pages[:i], s.Pages[i+1:]...)
+ s.AllPages = append(s.AllPages[:i], s.AllPages[i+1:]...)
}
}
func (s *Site) removePage(page *Page) {
- if i := s.Pages.FindPagePos(page); i >= 0 {
+ if i := s.AllPages.FindPagePos(page); i >= 0 {
if page.IsDraft() {
s.draftCount--
}
@@ -1047,7 +1112,7 @@ func (s *Site) removePage(page *Page) {
s.expiredCount--
}
- s.Pages = append(s.Pages[:i], s.Pages[i+1:]...)
+ s.AllPages = append(s.AllPages[:i], s.AllPages[i+1:]...)
}
}
@@ -1086,7 +1151,7 @@ func incrementalReadCollator(s *Site, results <-chan HandledResult, pageChan cha
}
}
- s.Pages.Sort()
+ s.AllPages.Sort()
close(coordinator)
if len(errMsgs) == 0 {
@@ -1112,7 +1177,7 @@ func readCollator(s *Site, results <-chan HandledResult, errs chan<- error) {
}
}
- s.Pages.Sort()
+ s.AllPages.Sort()
if len(errMsgs) == 0 {
errs <- nil
return
@@ -1298,9 +1363,8 @@ func (s *Site) resetPageBuildState() {
s.Info.paginationPageCount = 0
- for _, p := range s.Pages {
+ for _, p := range s.AllPages {
p.scratch = newScratch()
-
}
}
@@ -1326,17 +1390,6 @@ func (s *Site) assembleSections() {
}
}
-func (s *Site) possibleTaxonomies() (taxonomies []string) {
- for _, p := range s.Pages {
- for k := range p.Params {
- if !helpers.InStringArray(taxonomies, k) {
- taxonomies = append(taxonomies, k)
- }
- }
- }
- return
-}
-
// renderAliases renders shell pages that simply have a redirect in the header.
func (s *Site) renderAliases() error {
for _, p := range s.Pages {
@@ -1536,6 +1589,19 @@ func (s *Site) newTaxonomyNode(t taxRenderInfo) (*Node, string) {
return n, base
}
+// addMultilingualPrefix adds the `en/` prefix to the path passed as parameter.
+// `basePath` must not start with http://
+func (s *Site) addMultilingualPrefix(basePath string) string {
+ hadPrefix := strings.HasPrefix(basePath, "/")
+ if s.multilingualEnabled() {
+ basePath = path.Join(s.Multilingual.GetString("CurrentLanguage"), basePath)
+ if hadPrefix {
+ basePath = "/" + basePath
+ }
+ }
+ return basePath
+}
+
func taxonomyRenderer(s *Site, taxes <-chan taxRenderInfo, results chan<- error, wg *sync.WaitGroup) {
defer wg.Done()
@@ -1549,6 +1615,8 @@ func taxonomyRenderer(s *Site, taxes <-chan taxRenderInfo, results chan<- error,
n, base = s.newTaxonomyNode(t)
+ base = s.addMultilingualPrefix(base)
+
dest := base
if viper.GetBool("UglyURLs") {
dest = helpers.Uglify(base + ".html")
@@ -1623,7 +1691,7 @@ func (s *Site) renderListsOfTaxonomyTerms() (err error) {
layouts := []string{"taxonomy/" + singular + ".terms.html", "_default/terms.html", "indexes/indexes.html"}
layouts = s.appendThemeTemplates(layouts)
if s.layoutExists(layouts...) {
- if err := s.renderAndWritePage("taxonomy terms for "+singular, plural+"/index.html", n, layouts...); err != nil {
+ if err := s.renderAndWritePage("taxonomy terms for "+singular, s.addMultilingualPrefix(plural+"/index.html"), n, layouts...); err != nil {
return err
}
}
@@ -1664,8 +1732,10 @@ func (s *Site) renderSectionLists() error {
section = helpers.MakePathSanitized(section)
}
+ base := s.addMultilingualPrefix(section)
+
n := s.newSectionListNode(sectionName, section, data)
- if err := s.renderAndWritePage(fmt.Sprintf("section %s", section), section, n, s.appendThemeTemplates(layouts)...); err != nil {
+ if err := s.renderAndWritePage(fmt.Sprintf("section %s", section), base, n, s.appendThemeTemplates(layouts)...); err != nil {
return err
}
@@ -1674,7 +1744,7 @@ func (s *Site) renderSectionLists() error {
paginatePath := viper.GetString("paginatePath")
// write alias for page 1
- s.writeDestAlias(helpers.PaginateAliasPath(section, 1), s.permalink(section))
+ s.writeDestAlias(helpers.PaginateAliasPath(base, 1), s.permalink(base))
pagers := n.paginator.Pagers()
@@ -1692,7 +1762,7 @@ func (s *Site) renderSectionLists() error {
sectionPagerNode.Lastmod = first.Lastmod
}
pageNumber := i + 1
- htmlBase := fmt.Sprintf("/%s/%s/%d", section, paginatePath, pageNumber)
+ htmlBase := fmt.Sprintf("/%s/%s/%d", base, paginatePath, pageNumber)
if err := s.renderAndWritePage(fmt.Sprintf("section %s", section), filepath.FromSlash(htmlBase), sectionPagerNode, layouts...); err != nil {
return err
}
@@ -1702,10 +1772,10 @@ func (s *Site) renderSectionLists() error {
if !viper.GetBool("DisableRSS") && section != "" {
// XML Feed
rssuri := viper.GetString("RSSUri")
- n.URL = s.permalinkStr(section + "/" + rssuri)
- n.Permalink = s.permalink(section)
+ n.URL = s.permalinkStr(base + "/" + rssuri)
+ n.Permalink = s.permalink(base)
rssLayouts := []string{"section/" + section + ".rss.xml", "_default/rss.xml", "rss.xml", "_internal/_default/rss.xml"}
- if err := s.renderAndWriteXML("section "+section+" rss", section+"/"+rssuri, n, s.appendThemeTemplates(rssLayouts)...); err != nil {
+ if err := s.renderAndWriteXML("section "+section+" rss", base+"/"+rssuri, n, s.appendThemeTemplates(rssLayouts)...); err != nil {
return err
}
}
@@ -1713,24 +1783,11 @@ func (s *Site) renderSectionLists() error {
return nil
}
-func (s *Site) newHomeNode() *Node {
- n := s.newNode()
- n.Title = n.Site.Title
- n.IsHome = true
- s.setURLs(n, "/")
- n.Data["Pages"] = s.Pages
- if len(s.Pages) != 0 {
- n.Date = s.Pages[0].Date
- n.Lastmod = s.Pages[0].Lastmod
- }
- return n
-}
-
func (s *Site) renderHomePage() error {
n := s.newHomeNode()
layouts := s.appendThemeTemplates([]string{"index.html", "_default/list.html"})
- if err := s.renderAndWritePage("homepage", helpers.FilePathSeparator, n, layouts...); err != nil {
+ if err := s.renderAndWritePage("homepage", s.addMultilingualPrefix(helpers.FilePathSeparator), n, layouts...); err != nil {
return err
}
@@ -1739,7 +1796,7 @@ func (s *Site) renderHomePage() error {
paginatePath := viper.GetString("paginatePath")
// write alias for page 1
- s.writeDestAlias(helpers.PaginateAliasPath("", 1), s.permalink("/"))
+ s.writeDestAlias(s.addMultilingualPrefix(helpers.PaginateAliasPath("", 1)), s.permalink("/"))
pagers := n.paginator.Pagers()
@@ -1758,6 +1815,7 @@ func (s *Site) renderHomePage() error {
}
pageNumber := i + 1
htmlBase := fmt.Sprintf("/%s/%d", paginatePath, pageNumber)
+ htmlBase = s.addMultilingualPrefix(htmlBase)
if err := s.renderAndWritePage(fmt.Sprintf("homepage"), filepath.FromSlash(htmlBase), homePagerNode, layouts...); err != nil {
return err
}
@@ -1780,7 +1838,7 @@ func (s *Site) renderHomePage() error {
rssLayouts := []string{"rss.xml", "_default/rss.xml", "_internal/_default/rss.xml"}
- if err := s.renderAndWriteXML("homepage rss", viper.GetString("RSSUri"), n, s.appendThemeTemplates(rssLayouts)...); err != nil {
+ if err := s.renderAndWriteXML("homepage rss", s.addMultilingualPrefix(viper.GetString("RSSUri")), n, s.appendThemeTemplates(rssLayouts)...); err != nil {
return err
}
}
@@ -1804,6 +1862,19 @@ func (s *Site) renderHomePage() error {
return nil
}
+func (s *Site) newHomeNode() *Node {
+ n := s.newNode()
+ n.Title = n.Site.Title
+ n.IsHome = true
+ s.setURLs(n, "/")
+ n.Data["Pages"] = s.Pages
+ if len(s.Pages) != 0 {
+ n.Date = s.Pages[0].Date
+ n.Lastmod = s.Pages[0].Lastmod
+ }
+ return n
+}
+
func (s *Site) renderSitemap() error {
if viper.GetBool("DisableSitemap") {
return nil
@@ -1845,7 +1916,7 @@ func (s *Site) renderSitemap() error {
smLayouts := []string{"sitemap.xml", "_default/sitemap.xml", "_internal/_default/sitemap.xml"}
- if err := s.renderAndWriteXML("sitemap", page.Sitemap.Filename, n, s.appendThemeTemplates(smLayouts)...); err != nil {
+ if err := s.renderAndWriteXML("sitemap", s.addMultilingualPrefix(page.Sitemap.Filename), n, s.appendThemeTemplates(smLayouts)...); err != nil {
return err
}
@@ -1874,7 +1945,7 @@ func (s *Site) renderRobotsTXT() error {
// Stats prints Hugo builds stats to the console.
// This is what you see after a successful hugo build.
-func (s *Site) Stats() {
+func (s *Site) Stats(lang string, t0 time.Time) {
jww.FEEDBACK.Println(s.draftStats())
jww.FEEDBACK.Println(s.futureStats())
jww.FEEDBACK.Println(s.expiredStats())
@@ -1886,9 +1957,14 @@ func (s *Site) Stats() {
for _, pl := range taxonomies {
jww.FEEDBACK.Printf("%d %s created\n", len(s.Taxonomies[pl]), pl)
}
+
+ if lang != "" {
+ jww.FEEDBACK.Printf("rendered lang %q in %v ms\n", lang, int(1000*time.Since(t0).Seconds()))
+ }
}
func (s *Site) setURLs(n *Node, in string) {
+ in = s.addMultilingualPrefix(in)
n.URL = helpers.URLizeAndPrep(in)
n.Permalink = s.permalink(n.URL)
n.RSSLink = template.HTML(s.permalink(in + ".xml"))
diff --git a/hugolib/site_test.go b/hugolib/site_test.go
index f06c9de81..2f03dec97 100644
--- a/hugolib/site_test.go
+++ b/hugolib/site_test.go
@@ -18,6 +18,7 @@ import (
"fmt"
"html/template"
"io"
+ "io/ioutil"
"path/filepath"
"strings"
"testing"
@@ -92,16 +93,27 @@ func TestReadPagesFromSourceWithEmptySource(t *testing.T) {
}
func createAndRenderPages(t *testing.T, s *Site) {
- if err := s.createPages(); err != nil {
- t.Fatalf("Unable to create pages: %s", err)
+ createPagesAndMeta(t, s)
+
+ if err := s.renderPages(); err != nil {
+ t.Fatalf("Unable to render pages. %s", err)
}
+}
+
+func createPagesAndMeta(t *testing.T, s *Site) {
+ createPages(t, s)
+
+ s.setupTranslations()
+ s.setupPrevNext()
if err := s.buildSiteMeta(); err != nil {
t.Fatalf("Unable to build site metadata: %s", err)
}
+}
- if err := s.renderPages(); err != nil {
- t.Fatalf("Unable to render pages. %s", err)
+func createPages(t *testing.T, s *Site) {
+ if err := s.createPages(); err != nil {
+ t.Fatalf("Unable to create pages: %s", err)
}
}
@@ -254,9 +266,8 @@ func TestDraftAndFutureRender(t *testing.T) {
s.initializeSiteInfo()
- if err := s.createPages(); err != nil {
- t.Fatalf("Unable to create pages: %s", err)
- }
+ createPages(t, s)
+
return s
}
@@ -264,14 +275,14 @@ func TestDraftAndFutureRender(t *testing.T) {
// Testing Defaults.. Only draft:true and publishDate in the past should be rendered
s := siteSetup()
- if len(s.Pages) != 1 {
+ if len(s.AllPages) != 1 {
t.Fatal("Draft or Future dated content published unexpectedly")
}
// only publishDate in the past should be rendered
viper.Set("BuildDrafts", true)
s = siteSetup()
- if len(s.Pages) != 2 {
+ if len(s.AllPages) != 2 {
t.Fatal("Future Dated Posts published unexpectedly")
}
@@ -279,7 +290,7 @@ func TestDraftAndFutureRender(t *testing.T) {
viper.Set("BuildDrafts", false)
viper.Set("BuildFuture", true)
s = siteSetup()
- if len(s.Pages) != 2 {
+ if len(s.AllPages) != 2 {
t.Fatal("Draft posts published unexpectedly")
}
@@ -287,7 +298,7 @@ func TestDraftAndFutureRender(t *testing.T) {
viper.Set("BuildDrafts", true)
viper.Set("BuildFuture", true)
s = siteSetup()
- if len(s.Pages) != 4 {
+ if len(s.AllPages) != 4 {
t.Fatal("Drafts or Future posts not included as expected")
}
@@ -313,9 +324,8 @@ func TestFutureExpirationRender(t *testing.T) {
s.initializeSiteInfo()
- if err := s.createPages(); err != nil {
- t.Fatalf("Unable to create pages: %s", err)
- }
+ createPages(t, s)
+
return s
}
@@ -323,17 +333,17 @@ func TestFutureExpirationRender(t *testing.T) {
s := siteSetup()
- if len(s.Pages) != 1 {
- if len(s.Pages) > 1 {
+ if len(s.AllPages) != 1 {
+ if len(s.AllPages) > 1 {
t.Fatal("Expired content published unexpectedly")
}
- if len(s.Pages) < 1 {
+ if len(s.AllPages) < 1 {
t.Fatal("Valid content expired unexpectedly")
}
}
- if s.Pages[0].Title == "doc2" {
+ if s.AllPages[0].Title == "doc2" {
t.Fatal("Expired content published unexpectedly")
}
}
@@ -689,17 +699,7 @@ func TestAbsURLify(t *testing.T) {
s.prepTemplates("blue/single.html", templateWithURLAbs)
- if err := s.createPages(); err != nil {
- t.Fatalf("Unable to create pages: %s", err)
- }
-
- if err := s.buildSiteMeta(); err != nil {
- t.Fatalf("Unable to build site metadata: %s", err)
- }
-
- if err := s.renderPages(); err != nil {
- t.Fatalf("Unable to render pages. %s", err)
- }
+ createAndRenderPages(t, s)
tests := []struct {
file, expected string
@@ -791,13 +791,7 @@ func TestOrderedPages(t *testing.T) {
}
s.initializeSiteInfo()
- if err := s.createPages(); err != nil {
- t.Fatalf("Unable to create pages: %s", err)
- }
-
- if err := s.buildSiteMeta(); err != nil {
- t.Fatalf("Unable to build site metadata: %s", err)
- }
+ createPagesAndMeta(t, s)
if s.Sections["sect"][0].Weight != 2 || s.Sections["sect"][3].Weight != 6 {
t.Errorf("Pages in unexpected order. First should be '%d', got '%d'", 2, s.Sections["sect"][0].Weight)
@@ -865,13 +859,7 @@ func TestGroupedPages(t *testing.T) {
}
s.initializeSiteInfo()
- if err := s.createPages(); err != nil {
- t.Fatalf("Unable to create pages: %s", err)
- }
-
- if err := s.buildSiteMeta(); err != nil {
- t.Fatalf("Unable to build site metadata: %s", err)
- }
+ createPagesAndMeta(t, s)
rbysection, err := s.Pages.GroupBy("Section", "desc")
if err != nil {
@@ -1055,13 +1043,7 @@ func TestWeightedTaxonomies(t *testing.T) {
}
s.initializeSiteInfo()
- if err := s.createPages(); err != nil {
- t.Fatalf("Unable to create pages: %s", err)
- }
-
- if err := s.buildSiteMeta(); err != nil {
- t.Fatalf("Unable to build site metadata: %s", err)
- }
+ createPagesAndMeta(t, s)
if s.Taxonomies["tags"]["a"][0].Page.Title != "foo" {
t.Errorf("Pages in unexpected order, 'foo' expected first, got '%v'", s.Taxonomies["tags"]["a"][0].Page.Title)
@@ -1129,9 +1111,7 @@ func setupLinkingMockSite(t *testing.T) *Site {
site.initializeSiteInfo()
- if err := site.createPages(); err != nil {
- t.Fatalf("Unable to create pages: %s", err)
- }
+ createPagesAndMeta(t, site)
return site
}
@@ -1341,3 +1321,159 @@ func TestSourceRelativeLinkFileing(t *testing.T) {
}
}
}
+
+func TestMultilingualSwitch(t *testing.T) {
+ // General settings
+ viper.Set("DefaultExtension", "html")
+ viper.Set("baseurl", "http://example.com/blog")
+ viper.Set("DisableSitemap", false)
+ viper.Set("DisableRSS", false)
+ viper.Set("RSSUri", "index.xml")
+ viper.Set("Taxonomies", map[string]string{"tag": "tags"})
+ viper.Set("Permalinks", map[string]string{"other": "/somewhere/else/:filename"})
+
+ // Sources
+ sources := []source.ByteSource{
+ {filepath.FromSlash("sect/doc1.en.md"), []byte(`---
+title: doc1
+slug: doc1-slug
+tags:
+ - tag1
+publishdate: "2000-01-01"
+---
+# doc1
+*some content*
+NOTE: slug should be used as URL
+`)},
+ {filepath.FromSlash("sect/doc1.fr.md"), []byte(`---
+title: doc1
+tags:
+ - tag1
+ - tag2
+publishdate: "2000-01-04"
+---
+# doc1
+*quelque contenu*
+NOTE: should be in the 'en' Page's 'Translations' field.
+NOTE: date is after "doc3"
+`)},
+ {filepath.FromSlash("sect/doc2.en.md"), []byte(`---
+title: doc2
+publishdate: "2000-01-02"
+---
+# doc2
+*some content*
+NOTE: without slug, "doc2" should be used, without ".en" as URL
+`)},
+ {filepath.FromSlash("sect/doc3.en.md"), []byte(`---
+title: doc3
+publishdate: "2000-01-03"
+tags:
+ - tag2
+url: /superbob
+---
+# doc3
+*some content*
+NOTE: third 'en' doc, should trigger pagination on home page.
+`)},
+ {filepath.FromSlash("sect/doc4.md"), []byte(`---
+title: doc4
+tags:
+ - tag1
+publishdate: "2000-01-05"
+---
+# doc4
+*du contenu francophone*
+NOTE: should use the DefaultContentLanguage and mark this doc as 'fr'.
+NOTE: doesn't have any corresponding translation in 'en'
+`)},
+ {filepath.FromSlash("other/doc5.fr.md"), []byte(`---
+title: doc5
+publishdate: "2000-01-06"
+---
+# doc5
+*autre contenu francophone*
+NOTE: should use the "permalinks" configuration with :filename
+`)},
+ }
+
+ hugofs.InitMemFs()
+
+ s := &Site{
+ Source: &source.InMemorySource{ByteSource: sources},
+ Multilingual: &Multilingual{
+ config: viper.New(),
+ enabled: true,
+ },
+ }
+ // Multilingual settings
+ viper.Set("Multilingual", true)
+ s.Multilingual.config.Set("CurrentLanguage", "en")
+ viper.Set("DefaultContentLanguage", "fr")
+ viper.Set("paginate", "2")
+
+ s.prepTemplates()
+ s.initializeSiteInfo()
+
+ createPagesAndMeta(t, s)
+
+ assert.Len(t, s.Source.Files(), 6, "should have 6 source files")
+ assert.Len(t, s.Pages, 3, "should have 3 pages")
+ assert.Len(t, s.AllPages, 6, "should have 6 total pages (including translations)")
+
+ doc1en := s.Pages[0]
+ permalink, err := doc1en.Permalink()
+ assert.NoError(t, err, "permalink call failed")
+ assert.Equal(t, "http://example.com/blog/en/sect/doc1-slug", permalink, "invalid doc1.en permalink")
+ assert.Len(t, doc1en.Translations, 1, "doc1-en should have one translation, excluding itself")
+
+ doc2 := s.Pages[1]
+ permalink, err = doc2.Permalink()
+ assert.NoError(t, err, "permalink call failed")
+ assert.Equal(t, "http://example.com/blog/en/sect/doc2", permalink, "invalid doc2 permalink")
+
+ doc3 := s.Pages[2]
+ permalink, err = doc3.Permalink()
+ assert.NoError(t, err, "permalink call failed")
+ assert.Equal(t, "http://example.com/blog/superbob", permalink, "invalid doc3 permalink")
+ assert.Equal(t, "/superbob", doc3.URL, "invalid url, was specified on doc3")
+
+ assert.Equal(t, doc2.Next, doc3, "doc3 should follow doc2, in .Next")
+
+ doc1fr := doc1en.Translations["fr"]
+ permalink, err = doc1fr.Permalink()
+ assert.NoError(t, err, "permalink call failed")
+ assert.Equal(t, "http://example.com/blog/fr/sect/doc1", permalink, "invalid doc1fr permalink")
+
+ assert.Equal(t, doc1en.Translations["fr"], doc1fr, "doc1-en should have doc1-fr as translation")
+ assert.Equal(t, doc1fr.Translations["en"], doc1en, "doc1-fr should have doc1-en as translation")
+
+ doc4 := s.AllPages[4]
+ permalink, err = doc4.Permalink()
+ assert.NoError(t, err, "permalink call failed")
+ assert.Equal(t, "http://example.com/blog/fr/sect/doc4", permalink, "invalid doc4 permalink")
+ assert.Len(t, doc4.Translations, 0, "found translations for doc4")
+
+ doc5 := s.AllPages[5]
+ permalink, err = doc5.Permalink()
+ assert.NoError(t, err, "permalink call failed")
+ assert.Equal(t, "http://example.com/blog/fr/somewhere/else/doc5", permalink, "invalid doc5 permalink")
+
+ // Taxonomies and their URLs
+ assert.Len(t, s.Taxonomies, 1, "should have 1 taxonomy")
+ tags := s.Taxonomies["tags"]
+ assert.Len(t, tags, 2, "should have 2 different tags")
+ assert.Equal(t, tags["tag1"][0].Page, doc1en, "first tag1 page should be doc1")
+
+ // Expect the tags locations to be in certain places, with the /en/ prefixes, etc..
+}
+
+func assertFileContent(t *testing.T, path string, content string) {
+ fl, err := hugofs.Destination().Open(path)
+ assert.NoError(t, err, "file content not found when asserting on content of %s", path)
+
+ cnt, err := ioutil.ReadAll(fl)
+ assert.NoError(t, err, "cannot read file content when asserting on content of %s", path)
+
+ assert.Equal(t, content, string(cnt))
+}
diff --git a/hugolib/site_url_test.go b/hugolib/site_url_test.go
index c910c918d..6e011a500 100644
--- a/hugolib/site_url_test.go
+++ b/hugolib/site_url_test.go
@@ -97,12 +97,7 @@ func TestPageCount(t *testing.T) {
s.initializeSiteInfo()
s.prepTemplates("indexes/blue.html", indexTemplate)
- if err := s.createPages(); err != nil {
- t.Errorf("Unable to create pages: %s", err)
- }
- if err := s.buildSiteMeta(); err != nil {
- t.Errorf("Unable to build site metadata: %s", err)
- }
+ createPagesAndMeta(t, s)
if err := s.renderSectionLists(); err != nil {
t.Errorf("Unable to render section lists: %s", err)
diff --git a/hugolib/sitemap_test.go b/hugolib/sitemap_test.go
index 2ce842085..b9270ba58 100644
--- a/hugolib/sitemap_test.go
+++ b/hugolib/sitemap_test.go
@@ -51,13 +51,7 @@ func TestSitemapOutput(t *testing.T) {
s.prepTemplates("sitemap.xml", SITEMAP_TEMPLATE)
- if err := s.createPages(); err != nil {
- t.Fatalf("Unable to create pages: %s", err)
- }
-
- if err := s.buildSiteMeta(); err != nil {
- t.Fatalf("Unable to build site metadata: %s", err)
- }
+ createPagesAndMeta(t, s)
if err := s.renderHomePage(); err != nil {
t.Fatalf("Unable to RenderHomePage: %s", err)
diff --git a/hugolib/taxonomy_test.go b/hugolib/taxonomy_test.go
index ab9df1080..03d567aa7 100644
--- a/hugolib/taxonomy_test.go
+++ b/hugolib/taxonomy_test.go
@@ -20,18 +20,6 @@ import (
"github.com/spf13/viper"
)
-func TestSitePossibleTaxonomies(t *testing.T) {
- site := new(Site)
- page, _ := NewPageFrom(strings.NewReader(pageYamlWithTaxonomiesA), "path/to/page")
- site.Pages = append(site.Pages, page)
- taxonomies := site.possibleTaxonomies()
- if !compareStringSlice(taxonomies, []string{"tags", "categories"}) {
- if !compareStringSlice(taxonomies, []string{"categories", "tags"}) {
- t.Fatalf("possible taxonomies do not match [tags categories]. Got: %s", taxonomies)
- }
- }
-}
-
func TestByCountOrderOfTaxonomies(t *testing.T) {
viper.Reset()
defer viper.Reset()
diff --git a/hugolib/translations.go b/hugolib/translations.go
new file mode 100644
index 000000000..b503071d8
--- /dev/null
+++ b/hugolib/translations.go
@@ -0,0 +1,59 @@
+// Copyright 2016 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
+
+// Translations represent the other translations for a given page. The
+// string here is the language code, as affected by the `post.LANG.md`
+// filename.
+type Translations map[string]*Page
+
+func pagesToTranslationsMap(pages []*Page) map[string]Translations {
+ out := make(map[string]Translations)
+
+ for _, page := range pages {
+ base := page.TranslationBaseName()
+
+ pageTranslation, present := out[base]
+ if !present {
+ pageTranslation = make(Translations)
+ }
+
+ pageLang := page.Lang()
+ if pageLang == "" {
+ continue
+ }
+
+ pageTranslation[pageLang] = page
+ out[base] = pageTranslation
+ }
+
+ return out
+}
+
+func assignTranslationsToPages(allTranslations map[string]Translations, pages []*Page) {
+ for _, page := range pages {
+ base := page.TranslationBaseName()
+ trans, exist := allTranslations[base]
+ if !exist {
+ continue
+ }
+
+ for lang, translatedPage := range trans {
+ if translatedPage == page {
+ continue
+ }
+ page.Translations[lang] = translatedPage
+ }
+ }
+}
diff --git a/source/file.go b/source/file.go
index 469c35eee..9012b91c4 100644
--- a/source/file.go
+++ b/source/file.go
@@ -19,6 +19,7 @@ import (
"strings"
"github.com/spf13/hugo/helpers"
+ "github.com/spf13/viper"
)
// File represents a source content file.
@@ -26,11 +27,15 @@ import (
type File struct {
relpath string // Original relative path, e.g. content/foo.txt
logicalName string // foo.txt
+ baseName string // `post` for `post.md`, also `post.en` for `post.en.md`
Contents io.Reader
section string // The first directory
dir string // The relative directory Path (minus file name)
ext string // Just the ext (eg txt)
uniqueID string // MD5 of the filename
+
+ translationBaseName string // `post` for `post.es.md` (if `Multilingual` is enabled.)
+ lang string // The language code if `Multilingual` is enabled
}
// UniqueID is the MD5 hash of the filename and is for most practical applications,
@@ -51,7 +56,17 @@ func (f *File) Bytes() []byte {
// BaseFileName Filename without extension.
func (f *File) BaseFileName() string {
- return helpers.Filename(f.LogicalName())
+ return f.baseName
+}
+
+// Filename with no extension, not even the optional language extension part.
+func (f *File) TranslationBaseName() string {
+ return f.translationBaseName
+}
+
+// Lang for this page, if `Multilingual` is enabled on your site.
+func (f *File) Lang() string {
+ return f.lang
}
// Section is first directory below the content root.
@@ -108,6 +123,17 @@ func NewFile(relpath string) *File {
f.dir, f.logicalName = filepath.Split(f.relpath)
f.ext = strings.TrimPrefix(filepath.Ext(f.LogicalName()), ".")
+ f.baseName = helpers.Filename(f.LogicalName())
+ if viper.GetBool("Multilingual") {
+ f.lang = strings.TrimPrefix(filepath.Ext(f.baseName), ".")
+ if f.lang == "" {
+ f.lang = viper.GetString("DefaultContentLanguage")
+ }
+ f.translationBaseName = helpers.Filename(f.baseName)
+ } else {
+ f.translationBaseName = f.baseName
+ }
+
f.section = helpers.GuessSection(f.Dir())
f.uniqueID = helpers.Md5String(f.LogicalName())
diff --git a/tpl/template_funcs.go b/tpl/template_funcs.go
index 09191f41a..ed931c44b 100644
--- a/tpl/template_funcs.go
+++ b/tpl/template_funcs.go
@@ -1920,5 +1920,7 @@ func init() {
"upper": func(a string) string { return strings.ToUpper(a) },
"urlize": helpers.URLize,
"where": where,
+ "i18n": I18nTranslate,
+ "T": I18nTranslate,
}
}
diff --git a/tpl/template_i18n.go b/tpl/template_i18n.go
new file mode 100644
index 000000000..3b0f0cb63
--- /dev/null
+++ b/tpl/template_i18n.go
@@ -0,0 +1,47 @@
+// Copyright 2015 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 tpl
+
+import (
+ "fmt"
+
+ "github.com/nicksnyder/go-i18n/i18n/bundle"
+ jww "github.com/spf13/jwalterweatherman"
+)
+
+var i18nTfunc bundle.TranslateFunc
+
+func SetI18nTfunc(lang string, bndl *bundle.Bundle) {
+ tFunc, err := bndl.Tfunc(lang)
+ if err == nil {
+ i18nTfunc = tFunc
+ return
+ }
+
+ jww.WARN.Printf("could not load translations for language %q (%s), will not translate!\n", lang, err.Error())
+ i18nTfunc = bundle.TranslateFunc(func(id string, args ...interface{}) string {
+ // TODO: depending on the site mode, we might want to fall back on the default
+ // language's translation.
+ // TODO: eventually, we could add --i18n-warnings and print something when
+ // such things happen.
+ return fmt.Sprintf("[i18n: %s]", id)
+ })
+}
+
+func I18nTranslate(id string, args ...interface{}) (string, error) {
+ if i18nTfunc == nil {
+ return "", fmt.Errorf("i18n not initialized, have you configured everything properly?")
+ }
+ return i18nTfunc(id, args...), nil
+}