URI: 
       Add multilingual support in Hugo - hugo - [fork] hugo port for 9front
  HTML git clone git@git.drkhsh.at/hugo.git
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
   DIR README
   DIR LICENSE
       ---
   DIR commit ec33732fbe84f67c1164fb713d6cb738609f2e2e
   DIR parent faa3472fa299adb287d575e6d404d4ddcddbff4e
  HTML Author: Alexandre Bourget <alex@bourget.cc>
       Date:   Sat, 14 May 2016 00:35:16 -0400
       
       Add multilingual support in Hugo
       
       Implements:
       * support to render:
         * content/post/whatever.en.md to /en/2015/12/22/whatever/index.html
         * content/post/whatever.fr.md to /fr/2015/12/22/whatever/index.html
       * gets enabled when `Multilingual:` is specified in config.
       * support having language switchers in templates, that know
         where the translated page is (with .Page.Translations)
         (when you're on /en/about/, you can have a "Francais" link pointing to
          /fr/a-propos/)
         * all translations are in the `.Page.Translations` map, including the current one.
       * easily tweak themes to support Multilingual mode
       * renders in a single swift, no need for two config files.
       
       Adds a couple of variables useful for multilingual sites
       
       Adds documentation (content/multilingual.md)
       
       Added language prefixing for all URL generation/permalinking see in the
       code base.
       
       Implements i18n. Leverages the great github.com/nicksnyder/go-i18n lib.. thanks Nick.
       * Adds "i18n" and "T" template functions..
       
       Diffstat:
         M commands/benchmark.go               |       4 ++--
         M commands/hugo.go                    |      73 ++++++++++++++++++++++---------
         M commands/list.go                    |       6 +++---
         A commands/multilingual.go            |      41 +++++++++++++++++++++++++++++++
         A docs/content/content/multilingual.… |     238 +++++++++++++++++++++++++++++++
         M docs/content/taxonomies/displaying… |       8 ++++----
         M docs/content/taxonomies/ordering.md |       4 ++--
         M docs/content/templates/functions.md |      26 ++++++++++++++++++++++++--
         M docs/content/templates/terms.md     |       8 ++++----
         M docs/content/templates/variables.md |      27 ++++++++++++++++++++++-----
         M helpers/path.go                     |       6 ++++++
         M hugolib/embedded_shortcodes_test.go |       4 ++--
         A hugolib/i18n.go                     |      36 +++++++++++++++++++++++++++++++
         M hugolib/menu_test.go                |       8 +-------
         A hugolib/multilingual.go             |      48 +++++++++++++++++++++++++++++++
         M hugolib/page.go                     |      67 +++++++++++++++++++++----------
         M hugolib/permalinks.go               |       2 +-
         M hugolib/planner.go                  |       2 +-
         M hugolib/robotstxt_test.go           |       8 +-------
         M hugolib/rss_test.go                 |       8 +-------
         M hugolib/site.go                     |     226 +++++++++++++++++++++----------
         M hugolib/site_test.go                |     242 ++++++++++++++++++++++++-------
         M hugolib/site_url_test.go            |       7 +------
         M hugolib/sitemap_test.go             |       8 +-------
         M hugolib/taxonomy_test.go            |      12 ------------
         A hugolib/translations.go             |      59 +++++++++++++++++++++++++++++++
         M source/file.go                      |      28 +++++++++++++++++++++++++++-
         M tpl/template_funcs.go               |       2 ++
         A tpl/template_i18n.go                |      47 +++++++++++++++++++++++++++++++
       
       29 files changed, 1013 insertions(+), 242 deletions(-)
       ---
   DIR diff --git 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()
                        }
                }
   DIR diff --git 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
        }
   DIR diff --git 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()))
                                }
   DIR diff --git a/commands/multilingual.go 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"])
       +}
   DIR diff --git a/docs/content/content/multilingual.md 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.
   DIR diff --git 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>
       -
   DIR diff --git 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>
        
   DIR diff --git 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
   DIR diff --git 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>
   DIR diff --git 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
        
   DIR diff --git 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")
   DIR diff --git 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)
   DIR diff --git a/hugolib/i18n.go 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
       +}
   DIR diff --git 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) {
   DIR diff --git a/hugolib/multilingual.go 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
       +}
   DIR diff --git 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)
        }
   DIR diff --git 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
   DIR diff --git 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)")
   DIR diff --git 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)
   DIR diff --git 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)
   DIR diff --git 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"))
   DIR diff --git 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))
       +}
   DIR diff --git 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)
   DIR diff --git 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)
   DIR diff --git 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()
   DIR diff --git a/hugolib/translations.go 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
       +                }
       +        }
       +}
   DIR diff --git 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())
        
   DIR diff --git 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,
                }
        }
   DIR diff --git a/tpl/template_i18n.go 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
       +}