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..
This commit is contained in:
Alexandre Bourget 2016-05-14 00:35:16 -04:00 committed by Bjørn Erik Pedersen
parent faa3472fa2
commit ec33732fbe
29 changed files with 1014 additions and 243 deletions

View file

@ -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()
}
}

View file

@ -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)
t0 := time.Now()
if MainSites == nil {
MainSites = make(map[string]*hugolib.Site)
}
if len(watching) > 0 && watching[0] {
MainSite.RunMode.Watching = true
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)
}
err = MainSite.Build()
if err != nil {
return err
}
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
}

View file

@ -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()))
}

41
commands/multilingual.go Normal file
View file

@ -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"])
}

View file

@ -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.

View file

@ -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>

View file

@ -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>

View file

@ -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

View file

@ -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>

View file

@ -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

View file

@ -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")

View file

@ -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)

36
hugolib/i18n.go Normal file
View file

@ -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
}

View file

@ -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) {

48
hugolib/multilingual.go Normal file
View file

@ -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
}

View file

@ -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 time.Time:
return val
case []string:
if stringToLower {
return helpers.SliceToLower(val)
}
return 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 []string:
if stringToLower {
return helpers.SliceToLower(v.([]string))
}
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)
}

View file

@ -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

View file

@ -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)")

View file

@ -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)

View file

@ -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)

View file

@ -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"))

View file

@ -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))
}

View file

@ -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)

View file

@ -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)

View file

@ -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()

59
hugolib/translations.go Normal file
View file

@ -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
}
}
}

View file

@ -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())

View file

@ -1920,5 +1920,7 @@ func init() {
"upper": func(a string) string { return strings.ToUpper(a) },
"urlize": helpers.URLize,
"where": where,
"i18n": I18nTranslate,
"T": I18nTranslate,
}
}

47
tpl/template_i18n.go Normal file
View file

@ -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
}