mirror of
https://github.com/gohugoio/hugo.git
synced 2025-04-27 22:21:07 +03:00
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:
parent
faa3472fa2
commit
ec33732fbe
29 changed files with 1014 additions and 243 deletions
|
@ -57,7 +57,7 @@ func benchmark(cmd *cobra.Command, args []string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for i := 0; i < benchmarkTimes; i++ {
|
for i := 0; i < benchmarkTimes; i++ {
|
||||||
MainSite = nil
|
MainSites = nil
|
||||||
_ = buildSite()
|
_ = buildSite()
|
||||||
}
|
}
|
||||||
pprof.WriteHeapProfile(f)
|
pprof.WriteHeapProfile(f)
|
||||||
|
@ -76,7 +76,7 @@ func benchmark(cmd *cobra.Command, args []string) error {
|
||||||
pprof.StartCPUProfile(f)
|
pprof.StartCPUProfile(f)
|
||||||
defer pprof.StopCPUProfile()
|
defer pprof.StopCPUProfile()
|
||||||
for i := 0; i < benchmarkTimes; i++ {
|
for i := 0; i < benchmarkTimes; i++ {
|
||||||
MainSite = nil
|
MainSites = nil
|
||||||
_ = buildSite()
|
_ = buildSite()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,10 +46,10 @@ import (
|
||||||
"github.com/spf13/viper"
|
"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
|
// 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.
|
// 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
|
// Reset resets Hugo ready for a new full build. This is mainly only useful
|
||||||
// for benchmark testing etc. via the CLI commands.
|
// for benchmark testing etc. via the CLI commands.
|
||||||
|
@ -287,6 +287,7 @@ func loadDefaultSettings() {
|
||||||
viper.SetDefault("ArchetypeDir", "archetypes")
|
viper.SetDefault("ArchetypeDir", "archetypes")
|
||||||
viper.SetDefault("PublishDir", "public")
|
viper.SetDefault("PublishDir", "public")
|
||||||
viper.SetDefault("DataDir", "data")
|
viper.SetDefault("DataDir", "data")
|
||||||
|
viper.SetDefault("I18nDir", "i18n")
|
||||||
viper.SetDefault("ThemesDir", "themes")
|
viper.SetDefault("ThemesDir", "themes")
|
||||||
viper.SetDefault("DefaultLayout", "post")
|
viper.SetDefault("DefaultLayout", "post")
|
||||||
viper.SetDefault("BuildDrafts", false)
|
viper.SetDefault("BuildDrafts", false)
|
||||||
|
@ -323,6 +324,8 @@ func loadDefaultSettings() {
|
||||||
viper.SetDefault("EnableEmoji", false)
|
viper.SetDefault("EnableEmoji", false)
|
||||||
viper.SetDefault("PygmentsCodeFencesGuessSyntax", false)
|
viper.SetDefault("PygmentsCodeFencesGuessSyntax", false)
|
||||||
viper.SetDefault("UseModTimeAsFallback", false)
|
viper.SetDefault("UseModTimeAsFallback", false)
|
||||||
|
viper.SetDefault("Multilingual", false)
|
||||||
|
viper.SetDefault("DefaultContentLanguage", "en")
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitializeConfig initializes a config file with sensible default configuration flags.
|
// InitializeConfig initializes a config file with sensible default configuration flags.
|
||||||
|
@ -490,6 +493,8 @@ func InitializeConfig(subCmdVs ...*cobra.Command) error {
|
||||||
helpers.HugoReleaseVersion(), minVersion)
|
helpers.HugoReleaseVersion(), minVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readMultilingualConfiguration()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -506,7 +511,7 @@ func watchConfig() {
|
||||||
viper.OnConfigChange(func(e fsnotify.Event) {
|
viper.OnConfigChange(func(e fsnotify.Event) {
|
||||||
fmt.Println("Config file changed:", e.Name)
|
fmt.Println("Config file changed:", e.Name)
|
||||||
// Force a full rebuild
|
// Force a full rebuild
|
||||||
MainSite = nil
|
MainSites = nil
|
||||||
utils.CheckErr(buildSite(true))
|
utils.CheckErr(buildSite(true))
|
||||||
if !viper.GetBool("DisableLiveReload") {
|
if !viper.GetBool("DisableLiveReload") {
|
||||||
// Will block forever trying to write to a channel that nobody is reading if livereload isn't initialized
|
// 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 {
|
func getDirList() []string {
|
||||||
var a []string
|
var a []string
|
||||||
dataDir := helpers.AbsPathify(viper.GetString("DataDir"))
|
dataDir := helpers.AbsPathify(viper.GetString("DataDir"))
|
||||||
|
i18nDir := helpers.AbsPathify(viper.GetString("I18nDir"))
|
||||||
layoutDir := helpers.AbsPathify(viper.GetString("LayoutDir"))
|
layoutDir := helpers.AbsPathify(viper.GetString("LayoutDir"))
|
||||||
staticDir := helpers.AbsPathify(viper.GetString("StaticDir"))
|
staticDir := helpers.AbsPathify(viper.GetString("StaticDir"))
|
||||||
walker := func(path string, fi os.FileInfo, err error) error {
|
walker := func(path string, fi os.FileInfo, err error) error {
|
||||||
|
@ -639,8 +645,13 @@ func getDirList() []string {
|
||||||
if path == dataDir && os.IsNotExist(err) {
|
if path == dataDir && os.IsNotExist(err) {
|
||||||
jww.WARN.Println("Skip DataDir:", err)
|
jww.WARN.Println("Skip DataDir:", err)
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if path == i18nDir && os.IsNotExist(err) {
|
||||||
|
jww.WARN.Println("Skip I18nDir:", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if path == layoutDir && os.IsNotExist(err) {
|
if path == layoutDir && os.IsNotExist(err) {
|
||||||
jww.WARN.Println("Skip LayoutDir:", err)
|
jww.WARN.Println("Skip LayoutDir:", err)
|
||||||
return nil
|
return nil
|
||||||
|
@ -684,6 +695,7 @@ func getDirList() []string {
|
||||||
|
|
||||||
helpers.SymbolicWalk(hugofs.Source(), dataDir, walker)
|
helpers.SymbolicWalk(hugofs.Source(), dataDir, walker)
|
||||||
helpers.SymbolicWalk(hugofs.Source(), helpers.AbsPathify(viper.GetString("ContentDir")), 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(), helpers.AbsPathify(viper.GetString("LayoutDir")), walker)
|
||||||
helpers.SymbolicWalk(hugofs.Source(), staticDir, walker)
|
helpers.SymbolicWalk(hugofs.Source(), staticDir, walker)
|
||||||
if helpers.ThemeSet() {
|
if helpers.ThemeSet() {
|
||||||
|
@ -695,31 +707,52 @@ func getDirList() []string {
|
||||||
|
|
||||||
func buildSite(watching ...bool) (err error) {
|
func buildSite(watching ...bool) (err error) {
|
||||||
fmt.Println("Started building site")
|
fmt.Println("Started building site")
|
||||||
startTime := time.Now()
|
t0 := time.Now()
|
||||||
if MainSite == nil {
|
|
||||||
MainSite = new(hugolib.Site)
|
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 {
|
jww.FEEDBACK.Printf("total in %v ms\n", int(1000*time.Since(t0).Seconds()))
|
||||||
return err
|
|
||||||
}
|
|
||||||
MainSite.Stats()
|
|
||||||
jww.FEEDBACK.Printf("in %v ms\n", int(1000*time.Since(startTime).Seconds()))
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func rebuildSite(events []fsnotify.Event) error {
|
func rebuildSite(events []fsnotify.Event) error {
|
||||||
startTime := time.Now()
|
t0 := time.Now()
|
||||||
err := MainSite.ReBuild(events)
|
|
||||||
if err != nil {
|
for _, lang := range langConfigsList {
|
||||||
return err
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,7 @@ var listDraftsCmd = &cobra.Command{
|
||||||
return newSystemError("Error Processing Source Content", err)
|
return newSystemError("Error Processing Source Content", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, p := range site.Pages {
|
for _, p := range site.AllPages {
|
||||||
if p.IsDraft() {
|
if p.IsDraft() {
|
||||||
fmt.Println(filepath.Join(p.File.Dir(), p.File.LogicalName()))
|
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)
|
return newSystemError("Error Processing Source Content", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, p := range site.Pages {
|
for _, p := range site.AllPages {
|
||||||
if p.IsFuture() {
|
if p.IsFuture() {
|
||||||
fmt.Println(filepath.Join(p.File.Dir(), p.File.LogicalName()))
|
fmt.Println(filepath.Join(p.File.Dir(), p.File.LogicalName()))
|
||||||
}
|
}
|
||||||
|
@ -119,7 +119,7 @@ expired.`,
|
||||||
return newSystemError("Error Processing Source Content", err)
|
return newSystemError("Error Processing Source Content", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, p := range site.Pages {
|
for _, p := range site.AllPages {
|
||||||
if p.IsExpired() {
|
if p.IsExpired() {
|
||||||
fmt.Println(filepath.Join(p.File.Dir(), p.File.LogicalName()))
|
fmt.Println(filepath.Join(p.File.Dir(), p.File.LogicalName()))
|
||||||
}
|
}
|
||||||
|
|
41
commands/multilingual.go
Normal file
41
commands/multilingual.go
Normal 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"])
|
||||||
|
}
|
238
docs/content/content/multilingual.md
Normal file
238
docs/content/content/multilingual.md
Normal 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.
|
|
@ -38,7 +38,7 @@ each content piece are located in the usual place
|
||||||
|
|
||||||
<ul id="tags">
|
<ul id="tags">
|
||||||
{{ range .Params.tags }}
|
{{ range .Params.tags }}
|
||||||
<li><a href="{{ "/tags/" | relURL }}{{ . | urlize }}">{{ . }}</a> </li>
|
<li><a href="{{ "/tags/" | relLangURL }}{{ . | urlize }}">{{ . }}</a> </li>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
@ -110,7 +110,8 @@ The following example displays all tag keys:
|
||||||
|
|
||||||
<ul id="all-tags">
|
<ul id="all-tags">
|
||||||
{{ range $name, $taxonomy := .Site.Taxonomies.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 }}
|
{{ end }}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
@ -120,7 +121,7 @@ This example will list all taxonomies, each of their keys and all the content as
|
||||||
<section>
|
<section>
|
||||||
<ul>
|
<ul>
|
||||||
{{ range $taxonomyname, $taxonomy := .Site.Taxonomies }}
|
{{ range $taxonomyname, $taxonomy := .Site.Taxonomies }}
|
||||||
<li><a href="{{ "/" | relURL}}{{ $taxonomyname | urlize }}">{{ $taxonomyname }}</a>
|
<li><a href="{{ "/" | relLangURL}}{{ $taxonomyname | urlize }}">{{ $taxonomyname }}</a>
|
||||||
<ul>
|
<ul>
|
||||||
{{ range $key, $value := $taxonomy }}
|
{{ range $key, $value := $taxonomy }}
|
||||||
<li> {{ $key }} </li>
|
<li> {{ $key }} </li>
|
||||||
|
@ -135,4 +136,3 @@ This example will list all taxonomies, each of their keys and all the content as
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,7 @@ Taxonomies can be ordered by either alphabetical key or by the number of content
|
||||||
<ul>
|
<ul>
|
||||||
{{ $data := .Data }}
|
{{ $data := .Data }}
|
||||||
{{ range $key, $value := .Data.Taxonomy.Alphabetical }}
|
{{ 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 }}
|
{{ end }}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ Taxonomies can be ordered by either alphabetical key or by the number of content
|
||||||
<ul>
|
<ul>
|
||||||
{{ $data := .Data }}
|
{{ $data := .Data }}
|
||||||
{{ range $key, $value := .Data.Taxonomy.ByCount }}
|
{{ 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 }}
|
{{ end }}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|
|
@ -435,6 +435,13 @@ e.g.
|
||||||
|
|
||||||
## Strings
|
## 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
|
### chomp
|
||||||
Removes any trailing newline characters. Useful in a pipeline to remove newlines added by other processing (including `markdownify`).
|
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. -->
|
<!-- outputs a content length of 8 runes. -->
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### md5
|
### md5
|
||||||
|
|
||||||
`md5` hashes the given input and returns its MD5 checksum.
|
`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" -->
|
<!-- 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
|
## 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
|
* `{{ (time "2016-05-28").YearDay }}` → 149
|
||||||
* `{{ mul 1000 (time "2016-05-28T10:30:00.00+10:00").Unix }}` → 1464395400000 (Unix time in milliseconds)
|
* `{{ mul 1000 (time "2016-05-28T10:30:00.00+10:00").Unix }}` → 1464395400000 (Unix time in milliseconds)
|
||||||
|
|
||||||
|
|
||||||
## URLs
|
## URLs
|
||||||
|
|
||||||
### absURL, relURL
|
### absURL, relURL
|
||||||
|
|
|
@ -89,7 +89,7 @@ content tagged with each tag.
|
||||||
<ul>
|
<ul>
|
||||||
{{ $data := .Data }}
|
{{ $data := .Data }}
|
||||||
{{ range $key, $value := .Data.Terms }}
|
{{ 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 }}
|
{{ end }}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -109,7 +109,7 @@ Another example listing the content for each term (ordered by Date):
|
||||||
|
|
||||||
{{ $data := .Data }}
|
{{ $data := .Data }}
|
||||||
{{ range $key,$value := .Data.Terms.ByCount }}
|
{{ 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>
|
<ul>
|
||||||
{{ range $value.Pages.ByDate }}
|
{{ range $value.Pages.ByDate }}
|
||||||
<li><a href="{{ .Permalink }}">{{ .Title }}</a></li>
|
<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>
|
<ul>
|
||||||
{{ $data := .Data }}
|
{{ $data := .Data }}
|
||||||
{{ range $key, $value := .Data.Terms.Alphabetical }}
|
{{ 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 }}
|
{{ end }}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -158,7 +158,7 @@ Hugo can order the meta data in two different ways. It can be ordered:
|
||||||
<ul>
|
<ul>
|
||||||
{{ $data := .Data }}
|
{{ $data := .Data }}
|
||||||
{{ range $key, $value := .Data.Terms.ByCount }}
|
{{ 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 }}
|
{{ end }}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -58,6 +58,8 @@ matter, content or derived from file location.
|
||||||
**.IsPage** Always true for page.<br>
|
**.IsPage** Always true for page.<br>
|
||||||
**.Site** See [Site Variables]({{< relref "#site-variables" >}}) below.<br>
|
**.Site** See [Site Variables]({{< relref "#site-variables" >}}) below.<br>
|
||||||
**.Hugo** See [Hugo Variables]({{< relref "#hugo-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
|
## Page Params
|
||||||
|
|
||||||
|
@ -119,9 +121,9 @@ includes taxonomies, lists and the homepage.
|
||||||
**.Site** See [Site Variables]({{< relref "#site-variables" >}}) below.<br>
|
**.Site** See [Site Variables]({{< relref "#site-variables" >}}) below.<br>
|
||||||
**.Hugo** See [Hugo Variables]({{< relref "#hugo-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.Singular** The singular name of the taxonomy<br>
|
||||||
**.Data.Plural** The plural 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**.
|
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
|
## Site Variables
|
||||||
|
|
||||||
Also available is `.Site` which has the following:
|
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.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.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.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.<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:
|
**.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/"
|
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.Menus** All of the menus in the site.<br>
|
||||||
**.Site.Title** A string representing the title of 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.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.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.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>
|
**.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.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.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.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
|
## File Variables
|
||||||
|
|
||||||
|
|
|
@ -182,6 +182,12 @@ func GetThemeDataDirPath() (string, error) {
|
||||||
return getThemeDirPath("data")
|
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) {
|
func getThemeDirPath(path string) (string, error) {
|
||||||
if !ThemeSet() {
|
if !ThemeSet() {
|
||||||
return "", errors.New("No theme set")
|
return "", errors.New("No theme set")
|
||||||
|
|
|
@ -56,8 +56,8 @@ func doTestShortcodeCrossrefs(t *testing.T, relative bool) {
|
||||||
templ := tpl.New()
|
templ := tpl.New()
|
||||||
p, _ := pageFromString(simplePageWithURL, path)
|
p, _ := pageFromString(simplePageWithURL, path)
|
||||||
p.Node.Site = &SiteInfo{
|
p.Node.Site = &SiteInfo{
|
||||||
Pages: &(Pages{p}),
|
AllPages: &(Pages{p}),
|
||||||
BaseURL: template.URL(helpers.SanitizeURLKeepTrailingSlash(baseURL)),
|
BaseURL: template.URL(helpers.SanitizeURLKeepTrailingSlash(baseURL)),
|
||||||
}
|
}
|
||||||
|
|
||||||
output, err := HandleShortcodes(in, p, templ)
|
output, err := HandleShortcodes(in, p, templ)
|
||||||
|
|
36
hugolib/i18n.go
Normal file
36
hugolib/i18n.go
Normal 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
|
||||||
|
}
|
|
@ -691,13 +691,7 @@ func testSiteSetup(s *Site, t *testing.T) {
|
||||||
s.Menus = Menus{}
|
s.Menus = Menus{}
|
||||||
s.initializeSiteInfo()
|
s.initializeSiteInfo()
|
||||||
|
|
||||||
if err := s.createPages(); err != nil {
|
createPagesAndMeta(t, s)
|
||||||
t.Fatalf("Unable to create pages: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.buildSiteMeta(); err != nil {
|
|
||||||
t.Fatalf("Unable to build site metadata: %s", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func tomlToMap(s string) (map[string]interface{}, error) {
|
func tomlToMap(s string) (map[string]interface{}, error) {
|
||||||
|
|
48
hugolib/multilingual.go
Normal file
48
hugolib/multilingual.go
Normal 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
|
||||||
|
}
|
|
@ -61,8 +61,10 @@ type Page struct {
|
||||||
PublishDate time.Time
|
PublishDate time.Time
|
||||||
ExpiryDate time.Time
|
ExpiryDate time.Time
|
||||||
Markup string
|
Markup string
|
||||||
|
Translations Translations
|
||||||
extension string
|
extension string
|
||||||
contentType string
|
contentType string
|
||||||
|
lang string
|
||||||
renderable bool
|
renderable bool
|
||||||
Layout string
|
Layout string
|
||||||
layoutsCalculated []string
|
layoutsCalculated []string
|
||||||
|
@ -300,9 +302,11 @@ func (p *Page) getRenderingConfig() *helpers.Blackfriday {
|
||||||
|
|
||||||
func newPage(filename string) *Page {
|
func newPage(filename string) *Page {
|
||||||
page := Page{contentType: "",
|
page := Page{contentType: "",
|
||||||
Source: Source{File: *source.NewFile(filename)},
|
Source: Source{File: *source.NewFile(filename)},
|
||||||
Node: Node{Keywords: []string{}, Sitemap: Sitemap{Priority: -1}},
|
Node: Node{Keywords: []string{}, Sitemap: Sitemap{Priority: -1}},
|
||||||
Params: make(map[string]interface{})}
|
Params: make(map[string]interface{}),
|
||||||
|
Translations: make(Translations),
|
||||||
|
}
|
||||||
|
|
||||||
jww.DEBUG.Println("Reading from", page.File.Path())
|
jww.DEBUG.Println("Reading from", page.File.Path())
|
||||||
return &page
|
return &page
|
||||||
|
@ -445,11 +449,13 @@ func (p *Page) permalink() (*url.URL, error) {
|
||||||
if len(pSlug) > 0 {
|
if len(pSlug) > 0 {
|
||||||
permalink = helpers.URLPrep(viper.GetBool("UglyURLs"), path.Join(dir, p.Slug+"."+p.Extension()))
|
permalink = helpers.URLPrep(viper.GetBool("UglyURLs"), path.Join(dir, p.Slug+"."+p.Extension()))
|
||||||
} else {
|
} 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 = 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
|
return helpers.MakePermalink(baseURL, permalink), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -460,6 +466,10 @@ func (p *Page) Extension() string {
|
||||||
return viper.GetString("DefaultExtension")
|
return viper.GetString("DefaultExtension")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Page) Lang() string {
|
||||||
|
return p.lang
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Page) LinkTitle() string {
|
func (p *Page) LinkTitle() string {
|
||||||
if len(p.linkTitle) > 0 {
|
if len(p.linkTitle) > 0 {
|
||||||
return p.linkTitle
|
return p.linkTitle
|
||||||
|
@ -699,29 +709,29 @@ func (p *Page) getParam(key string, stringToLower bool) interface{} {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
switch v.(type) {
|
switch val := v.(type) {
|
||||||
case bool:
|
case bool:
|
||||||
return v
|
return val
|
||||||
case time.Time:
|
case string:
|
||||||
return v
|
if stringToLower {
|
||||||
|
return strings.ToLower(val)
|
||||||
|
}
|
||||||
|
return val
|
||||||
case int64, int32, int16, int8, int:
|
case int64, int32, int16, int8, int:
|
||||||
return cast.ToInt(v)
|
return cast.ToInt(v)
|
||||||
case float64, float32:
|
case float64, float32:
|
||||||
return cast.ToFloat64(v)
|
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
|
case map[string]interface{}: // JSON and TOML
|
||||||
return v
|
return v
|
||||||
case map[interface{}]interface{}: // YAML
|
case map[interface{}]interface{}: // YAML
|
||||||
return v
|
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))
|
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.renderable = psr.IsRenderable()
|
||||||
p.frontmatter = psr.FrontMatter()
|
p.frontmatter = psr.FrontMatter()
|
||||||
p.rawContent = psr.Content()
|
p.rawContent = psr.Content()
|
||||||
|
p.lang = p.Source.File.Lang()
|
||||||
|
|
||||||
meta, err := psr.Metadata()
|
meta, err := psr.Metadata()
|
||||||
if meta != nil {
|
if meta != nil {
|
||||||
|
@ -975,7 +986,6 @@ func (p *Page) FullFilePath() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Page) TargetPath() (outfile string) {
|
func (p *Page) TargetPath() (outfile string) {
|
||||||
|
|
||||||
// Always use URL if it's specified
|
// Always use URL if it's specified
|
||||||
if len(strings.TrimSpace(p.URL)) > 2 {
|
if len(strings.TrimSpace(p.URL)) > 2 {
|
||||||
outfile = strings.TrimSpace(p.URL)
|
outfile = strings.TrimSpace(p.URL)
|
||||||
|
@ -997,6 +1007,7 @@ func (p *Page) TargetPath() (outfile string) {
|
||||||
outfile += "index.html"
|
outfile += "index.html"
|
||||||
}
|
}
|
||||||
outfile = filepath.FromSlash(outfile)
|
outfile = filepath.FromSlash(outfile)
|
||||||
|
outfile = p.addMultilingualFilesystemPrefix(outfile)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1005,8 +1016,22 @@ func (p *Page) TargetPath() (outfile string) {
|
||||||
outfile = strings.TrimSpace(p.Slug) + "." + p.Extension()
|
outfile = strings.TrimSpace(p.Slug) + "." + p.Extension()
|
||||||
} else {
|
} else {
|
||||||
// Fall back to filename
|
// 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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -159,7 +159,7 @@ func pageToPermalinkTitle(p *Page, _ string) (string, error) {
|
||||||
func pageToPermalinkFilename(p *Page, _ string) (string, error) {
|
func pageToPermalinkFilename(p *Page, _ string) (string, error) {
|
||||||
//var extension = p.Source.Ext
|
//var extension = p.Source.Ext
|
||||||
//var name = p.Source.Path()[0 : len(p.Source.Path())-len(extension)]
|
//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
|
// if the page has a slug, return the slug, else return the title
|
||||||
|
|
|
@ -25,7 +25,7 @@ func (s *Site) ShowPlan(out io.Writer) (err error) {
|
||||||
fmt.Fprintf(out, "No source files provided.\n")
|
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())
|
fmt.Fprintf(out, "%s", p.Source.Path())
|
||||||
if p.IsRenderable() {
|
if p.IsRenderable() {
|
||||||
fmt.Fprintf(out, " (renderer: markdown)")
|
fmt.Fprintf(out, " (renderer: markdown)")
|
||||||
|
|
|
@ -46,13 +46,7 @@ func TestRobotsTXTOutput(t *testing.T) {
|
||||||
|
|
||||||
s.prepTemplates("robots.txt", robotTxtTemplate)
|
s.prepTemplates("robots.txt", robotTxtTemplate)
|
||||||
|
|
||||||
if err := s.createPages(); err != nil {
|
createPagesAndMeta(t, s)
|
||||||
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.renderHomePage(); err != nil {
|
if err := s.renderHomePage(); err != nil {
|
||||||
t.Fatalf("Unable to RenderHomePage: %s", err)
|
t.Fatalf("Unable to RenderHomePage: %s", err)
|
||||||
|
|
|
@ -59,13 +59,7 @@ func TestRSSOutput(t *testing.T) {
|
||||||
s.initializeSiteInfo()
|
s.initializeSiteInfo()
|
||||||
s.prepTemplates("rss.xml", rssTemplate)
|
s.prepTemplates("rss.xml", rssTemplate)
|
||||||
|
|
||||||
if err := s.createPages(); err != nil {
|
createPagesAndMeta(t, s)
|
||||||
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.renderHomePage(); err != nil {
|
if err := s.renderHomePage(); err != nil {
|
||||||
t.Fatalf("Unable to RenderHomePage: %s", err)
|
t.Fatalf("Unable to RenderHomePage: %s", err)
|
||||||
|
|
226
hugolib/site.go
226
hugolib/site.go
|
@ -20,6 +20,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -29,8 +30,6 @@ import (
|
||||||
|
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"github.com/bep/inflect"
|
"github.com/bep/inflect"
|
||||||
"github.com/fsnotify/fsnotify"
|
"github.com/fsnotify/fsnotify"
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
|
@ -76,6 +75,7 @@ var distinctErrorLogger = helpers.NewDistinctErrorLogger()
|
||||||
// 5. The entire collection of files is written to disk.
|
// 5. The entire collection of files is written to disk.
|
||||||
type Site struct {
|
type Site struct {
|
||||||
Pages Pages
|
Pages Pages
|
||||||
|
AllPages Pages
|
||||||
Files []*source.File
|
Files []*source.File
|
||||||
Tmpl tpl.Template
|
Tmpl tpl.Template
|
||||||
Taxonomies TaxonomyList
|
Taxonomies TaxonomyList
|
||||||
|
@ -87,6 +87,7 @@ type Site struct {
|
||||||
targets targetList
|
targets targetList
|
||||||
targetListInit sync.Once
|
targetListInit sync.Once
|
||||||
RunMode runmode
|
RunMode runmode
|
||||||
|
Multilingual *Multilingual
|
||||||
draftCount int
|
draftCount int
|
||||||
futureCount int
|
futureCount int
|
||||||
expiredCount int
|
expiredCount int
|
||||||
|
@ -106,7 +107,8 @@ type SiteInfo struct {
|
||||||
Authors AuthorList
|
Authors AuthorList
|
||||||
Social SiteSocial
|
Social SiteSocial
|
||||||
Sections Taxonomy
|
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
|
Files *[]*source.File
|
||||||
Menus *Menus
|
Menus *Menus
|
||||||
Hugo *HugoInfo
|
Hugo *HugoInfo
|
||||||
|
@ -125,6 +127,11 @@ type SiteInfo struct {
|
||||||
preserveTaxonomyNames bool
|
preserveTaxonomyNames bool
|
||||||
paginationPageCount uint64
|
paginationPageCount uint64
|
||||||
Data *map[string]interface{}
|
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
|
// 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
switch v.(type) {
|
switch val := v.(type) {
|
||||||
case bool:
|
case bool:
|
||||||
return cast.ToBool(v)
|
return val
|
||||||
case string:
|
case string:
|
||||||
return cast.ToString(v)
|
return val
|
||||||
case int64, int32, int16, int8, int:
|
case int64, int32, int16, int8, int:
|
||||||
return cast.ToInt(v)
|
return cast.ToInt(v)
|
||||||
case float64, float32:
|
case float64, float32:
|
||||||
return cast.ToFloat64(v)
|
return cast.ToFloat64(v)
|
||||||
case time.Time:
|
case time.Time:
|
||||||
return cast.ToTime(v)
|
return val
|
||||||
case []string:
|
case []string:
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
@ -181,7 +188,7 @@ func (s *SiteInfo) refLink(ref string, page *Page, relative bool) (string, error
|
||||||
var link string
|
var link string
|
||||||
|
|
||||||
if refURL.Path != "" {
|
if refURL.Path != "" {
|
||||||
for _, page := range []*Page(*s.Pages) {
|
for _, page := range []*Page(*s.AllPages) {
|
||||||
refPath := filepath.FromSlash(refURL.Path)
|
refPath := filepath.FromSlash(refURL.Path)
|
||||||
if page.Source.Path() == refPath || page.Source.LogicalName() == refPath {
|
if page.Source.Path() == refPath || page.Source.LogicalName() == refPath {
|
||||||
target = page
|
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 {
|
if page.Source.Path() == refPath {
|
||||||
target = page
|
target = page
|
||||||
break
|
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 :/
|
// 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`
|
// 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"
|
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 {
|
if page.Source.Path() == mdPath {
|
||||||
target = page
|
target = page
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
indexPath := filepath.Join(refPath, "index.md")
|
indexPath := filepath.Join(refPath, "index.md")
|
||||||
for _, page := range []*Page(*s.Pages) {
|
for _, page := range []*Page(*s.AllPages) {
|
||||||
if page.Source.Path() == indexPath {
|
if page.Source.Path() == indexPath {
|
||||||
target = page
|
target = page
|
||||||
break
|
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.
|
// 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
|
// This includes processing any meta-data for that content
|
||||||
|
|
||||||
// The second step is to convert the content into HTML
|
// 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 {
|
if len(tmplChanged) > 0 || len(dataChanged) > 0 {
|
||||||
// Do not need to read the files again, but they need conversion
|
// Do not need to read the files again, but they need conversion
|
||||||
// for shortocde re-rendering.
|
// for shortocde re-rendering.
|
||||||
for _, p := range s.Pages {
|
for _, p := range s.AllPages {
|
||||||
pageChan <- p
|
pageChan <- p
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -538,6 +545,9 @@ func (s *Site) ReBuild(events []fsnotify.Event) error {
|
||||||
|
|
||||||
s.timerStep("read & convert pages from source")
|
s.timerStep("read & convert pages from source")
|
||||||
|
|
||||||
|
// FIXME: does this go inside the next `if` statement ?
|
||||||
|
s.setupTranslations()
|
||||||
|
|
||||||
if len(sourceChanged) > 0 {
|
if len(sourceChanged) > 0 {
|
||||||
s.setupPrevNext()
|
s.setupPrevNext()
|
||||||
if err = s.buildSiteMeta(); err != nil {
|
if err = s.buildSiteMeta(); err != nil {
|
||||||
|
@ -665,9 +675,9 @@ func (s *Site) readDataFromSourceFS() error {
|
||||||
dataSources = append(dataSources, &source.Filesystem{Base: s.absDataDir()})
|
dataSources = append(dataSources, &source.Filesystem{Base: s.absDataDir()})
|
||||||
|
|
||||||
// have to be last - duplicate keys in earlier entries will win
|
// have to be last - duplicate keys in earlier entries will win
|
||||||
themeStaticDir, err := helpers.GetThemeDataDirPath()
|
themeDataDir, err := helpers.GetThemeDataDirPath()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
dataSources = append(dataSources, &source.Filesystem{Base: themeStaticDir})
|
dataSources = append(dataSources, &source.Filesystem{Base: themeDataDir})
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.loadData(dataSources)
|
err = s.loadData(dataSources)
|
||||||
|
@ -688,10 +698,25 @@ func (s *Site) Process() (err error) {
|
||||||
return
|
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 {
|
if err = s.createPages(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.setupTranslations()
|
||||||
s.setupPrevNext()
|
s.setupPrevNext()
|
||||||
|
|
||||||
if err = s.buildSiteMeta(); err != nil {
|
if err = s.buildSiteMeta(); err != nil {
|
||||||
return
|
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) {
|
func (s *Site) Render() (err error) {
|
||||||
if err = s.renderAliases(); err != nil {
|
if err = s.renderAliases(); err != nil {
|
||||||
return
|
return
|
||||||
|
@ -771,32 +817,47 @@ func (s *Site) initialize() (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Site) initializeSiteInfo() {
|
func (s *Site) initializeSiteInfo() {
|
||||||
params := viper.GetStringMap("Params")
|
params := s.Multilingual.GetStringMap("Params")
|
||||||
|
|
||||||
permalinks := make(PermalinkOverrides)
|
permalinks := make(PermalinkOverrides)
|
||||||
for k, v := range viper.GetStringMapString("Permalinks") {
|
for k, v := range viper.GetStringMapString("Permalinks") {
|
||||||
permalinks[k] = pathPattern(v)
|
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{
|
s.Info = SiteInfo{
|
||||||
BaseURL: template.URL(helpers.SanitizeURLKeepTrailingSlash(viper.GetString("BaseURL"))),
|
BaseURL: template.URL(helpers.SanitizeURLKeepTrailingSlash(viper.GetString("BaseURL"))),
|
||||||
Title: viper.GetString("Title"),
|
Title: s.Multilingual.GetString("Title"),
|
||||||
Author: viper.GetStringMap("author"),
|
Author: s.Multilingual.GetStringMap("author"),
|
||||||
Social: viper.GetStringMapString("social"),
|
Social: s.Multilingual.GetStringMapString("social"),
|
||||||
LanguageCode: viper.GetString("languagecode"),
|
LanguageCode: s.Multilingual.GetString("languagecode"),
|
||||||
Copyright: viper.GetString("copyright"),
|
Copyright: s.Multilingual.GetString("copyright"),
|
||||||
DisqusShortname: viper.GetString("DisqusShortname"),
|
DisqusShortname: s.Multilingual.GetString("DisqusShortname"),
|
||||||
|
Multilingual: s.multilingualEnabled(),
|
||||||
|
CurrentLanguage: s.Multilingual.GetString("CurrentLanguage"),
|
||||||
|
LanguagePrefix: languagePrefix,
|
||||||
|
Languages: languages,
|
||||||
GoogleAnalytics: viper.GetString("GoogleAnalytics"),
|
GoogleAnalytics: viper.GetString("GoogleAnalytics"),
|
||||||
RSSLink: s.permalinkStr(viper.GetString("RSSUri")),
|
RSSLink: s.permalinkStr(viper.GetString("RSSUri")),
|
||||||
BuildDrafts: viper.GetBool("BuildDrafts"),
|
BuildDrafts: viper.GetBool("BuildDrafts"),
|
||||||
canonifyURLs: viper.GetBool("CanonifyURLs"),
|
canonifyURLs: viper.GetBool("CanonifyURLs"),
|
||||||
preserveTaxonomyNames: viper.GetBool("PreserveTaxonomyNames"),
|
preserveTaxonomyNames: viper.GetBool("PreserveTaxonomyNames"),
|
||||||
Pages: &s.Pages,
|
AllPages: &s.AllPages,
|
||||||
Files: &s.Files,
|
Pages: &s.Pages,
|
||||||
Menus: &s.Menus,
|
Files: &s.Files,
|
||||||
Params: params,
|
Menus: &s.Menus,
|
||||||
Permalinks: permalinks,
|
Params: params,
|
||||||
Data: &s.Data,
|
Permalinks: permalinks,
|
||||||
|
Data: &s.Data,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -808,6 +869,10 @@ func (s *Site) absDataDir() string {
|
||||||
return helpers.AbsPathify(viper.GetString("DataDir"))
|
return helpers.AbsPathify(viper.GetString("DataDir"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Site) absI18nDir() string {
|
||||||
|
return helpers.AbsPathify(viper.GetString("I18nDir"))
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Site) absThemeDir() string {
|
func (s *Site) absThemeDir() string {
|
||||||
return helpers.AbsPathify(viper.GetString("themesDir") + "/" + viper.GetString("theme"))
|
return helpers.AbsPathify(viper.GetString("themesDir") + "/" + viper.GetString("theme"))
|
||||||
}
|
}
|
||||||
|
@ -903,7 +968,7 @@ func (s *Site) convertSource() chan error {
|
||||||
|
|
||||||
go converterCollator(s, results, errs)
|
go converterCollator(s, results, errs)
|
||||||
|
|
||||||
for _, p := range s.Pages {
|
for _, p := range s.AllPages {
|
||||||
pageChan <- p
|
pageChan <- p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -997,7 +1062,7 @@ func converterCollator(s *Site, results <-chan HandledResult, errs chan<- error)
|
||||||
|
|
||||||
func (s *Site) addPage(page *Page) {
|
func (s *Site) addPage(page *Page) {
|
||||||
if page.shouldBuild() {
|
if page.shouldBuild() {
|
||||||
s.Pages = append(s.Pages, page)
|
s.AllPages = append(s.AllPages, page)
|
||||||
}
|
}
|
||||||
|
|
||||||
if page.IsDraft() {
|
if page.IsDraft() {
|
||||||
|
@ -1014,8 +1079,8 @@ func (s *Site) addPage(page *Page) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Site) removePageByPath(path string) {
|
func (s *Site) removePageByPath(path string) {
|
||||||
if i := s.Pages.FindPagePosByFilePath(path); i >= 0 {
|
if i := s.AllPages.FindPagePosByFilePath(path); i >= 0 {
|
||||||
page := s.Pages[i]
|
page := s.AllPages[i]
|
||||||
|
|
||||||
if page.IsDraft() {
|
if page.IsDraft() {
|
||||||
s.draftCount--
|
s.draftCount--
|
||||||
|
@ -1029,12 +1094,12 @@ func (s *Site) removePageByPath(path string) {
|
||||||
s.expiredCount--
|
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) {
|
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() {
|
if page.IsDraft() {
|
||||||
s.draftCount--
|
s.draftCount--
|
||||||
}
|
}
|
||||||
|
@ -1047,7 +1112,7 @@ func (s *Site) removePage(page *Page) {
|
||||||
s.expiredCount--
|
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)
|
close(coordinator)
|
||||||
|
|
||||||
if len(errMsgs) == 0 {
|
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 {
|
if len(errMsgs) == 0 {
|
||||||
errs <- nil
|
errs <- nil
|
||||||
return
|
return
|
||||||
|
@ -1298,9 +1363,8 @@ func (s *Site) resetPageBuildState() {
|
||||||
|
|
||||||
s.Info.paginationPageCount = 0
|
s.Info.paginationPageCount = 0
|
||||||
|
|
||||||
for _, p := range s.Pages {
|
for _, p := range s.AllPages {
|
||||||
p.scratch = newScratch()
|
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.
|
// renderAliases renders shell pages that simply have a redirect in the header.
|
||||||
func (s *Site) renderAliases() error {
|
func (s *Site) renderAliases() error {
|
||||||
for _, p := range s.Pages {
|
for _, p := range s.Pages {
|
||||||
|
@ -1536,6 +1589,19 @@ func (s *Site) newTaxonomyNode(t taxRenderInfo) (*Node, string) {
|
||||||
return n, base
|
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) {
|
func taxonomyRenderer(s *Site, taxes <-chan taxRenderInfo, results chan<- error, wg *sync.WaitGroup) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
|
@ -1549,6 +1615,8 @@ func taxonomyRenderer(s *Site, taxes <-chan taxRenderInfo, results chan<- error,
|
||||||
|
|
||||||
n, base = s.newTaxonomyNode(t)
|
n, base = s.newTaxonomyNode(t)
|
||||||
|
|
||||||
|
base = s.addMultilingualPrefix(base)
|
||||||
|
|
||||||
dest := base
|
dest := base
|
||||||
if viper.GetBool("UglyURLs") {
|
if viper.GetBool("UglyURLs") {
|
||||||
dest = helpers.Uglify(base + ".html")
|
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 := []string{"taxonomy/" + singular + ".terms.html", "_default/terms.html", "indexes/indexes.html"}
|
||||||
layouts = s.appendThemeTemplates(layouts)
|
layouts = s.appendThemeTemplates(layouts)
|
||||||
if s.layoutExists(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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1664,8 +1732,10 @@ func (s *Site) renderSectionLists() error {
|
||||||
section = helpers.MakePathSanitized(section)
|
section = helpers.MakePathSanitized(section)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
base := s.addMultilingualPrefix(section)
|
||||||
|
|
||||||
n := s.newSectionListNode(sectionName, section, data)
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1674,7 +1744,7 @@ func (s *Site) renderSectionLists() error {
|
||||||
paginatePath := viper.GetString("paginatePath")
|
paginatePath := viper.GetString("paginatePath")
|
||||||
|
|
||||||
// write alias for page 1
|
// 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()
|
pagers := n.paginator.Pagers()
|
||||||
|
|
||||||
|
@ -1692,7 +1762,7 @@ func (s *Site) renderSectionLists() error {
|
||||||
sectionPagerNode.Lastmod = first.Lastmod
|
sectionPagerNode.Lastmod = first.Lastmod
|
||||||
}
|
}
|
||||||
pageNumber := i + 1
|
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 {
|
if err := s.renderAndWritePage(fmt.Sprintf("section %s", section), filepath.FromSlash(htmlBase), sectionPagerNode, layouts...); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -1702,10 +1772,10 @@ func (s *Site) renderSectionLists() error {
|
||||||
if !viper.GetBool("DisableRSS") && section != "" {
|
if !viper.GetBool("DisableRSS") && section != "" {
|
||||||
// XML Feed
|
// XML Feed
|
||||||
rssuri := viper.GetString("RSSUri")
|
rssuri := viper.GetString("RSSUri")
|
||||||
n.URL = s.permalinkStr(section + "/" + rssuri)
|
n.URL = s.permalinkStr(base + "/" + rssuri)
|
||||||
n.Permalink = s.permalink(section)
|
n.Permalink = s.permalink(base)
|
||||||
rssLayouts := []string{"section/" + section + ".rss.xml", "_default/rss.xml", "rss.xml", "_internal/_default/rss.xml"}
|
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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1713,24 +1783,11 @@ func (s *Site) renderSectionLists() error {
|
||||||
return nil
|
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 {
|
func (s *Site) renderHomePage() error {
|
||||||
n := s.newHomeNode()
|
n := s.newHomeNode()
|
||||||
layouts := s.appendThemeTemplates([]string{"index.html", "_default/list.html"})
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1739,7 +1796,7 @@ func (s *Site) renderHomePage() error {
|
||||||
paginatePath := viper.GetString("paginatePath")
|
paginatePath := viper.GetString("paginatePath")
|
||||||
|
|
||||||
// write alias for page 1
|
// 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()
|
pagers := n.paginator.Pagers()
|
||||||
|
|
||||||
|
@ -1758,6 +1815,7 @@ func (s *Site) renderHomePage() error {
|
||||||
}
|
}
|
||||||
pageNumber := i + 1
|
pageNumber := i + 1
|
||||||
htmlBase := fmt.Sprintf("/%s/%d", paginatePath, pageNumber)
|
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 {
|
if err := s.renderAndWritePage(fmt.Sprintf("homepage"), filepath.FromSlash(htmlBase), homePagerNode, layouts...); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -1780,7 +1838,7 @@ func (s *Site) renderHomePage() error {
|
||||||
|
|
||||||
rssLayouts := []string{"rss.xml", "_default/rss.xml", "_internal/_default/rss.xml"}
|
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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1804,6 +1862,19 @@ func (s *Site) renderHomePage() error {
|
||||||
return nil
|
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 {
|
func (s *Site) renderSitemap() error {
|
||||||
if viper.GetBool("DisableSitemap") {
|
if viper.GetBool("DisableSitemap") {
|
||||||
return nil
|
return nil
|
||||||
|
@ -1845,7 +1916,7 @@ func (s *Site) renderSitemap() error {
|
||||||
|
|
||||||
smLayouts := []string{"sitemap.xml", "_default/sitemap.xml", "_internal/_default/sitemap.xml"}
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1874,7 +1945,7 @@ func (s *Site) renderRobotsTXT() error {
|
||||||
|
|
||||||
// Stats prints Hugo builds stats to the console.
|
// Stats prints Hugo builds stats to the console.
|
||||||
// This is what you see after a successful hugo build.
|
// 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.draftStats())
|
||||||
jww.FEEDBACK.Println(s.futureStats())
|
jww.FEEDBACK.Println(s.futureStats())
|
||||||
jww.FEEDBACK.Println(s.expiredStats())
|
jww.FEEDBACK.Println(s.expiredStats())
|
||||||
|
@ -1886,9 +1957,14 @@ func (s *Site) Stats() {
|
||||||
for _, pl := range taxonomies {
|
for _, pl := range taxonomies {
|
||||||
jww.FEEDBACK.Printf("%d %s created\n", len(s.Taxonomies[pl]), pl)
|
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) {
|
func (s *Site) setURLs(n *Node, in string) {
|
||||||
|
in = s.addMultilingualPrefix(in)
|
||||||
n.URL = helpers.URLizeAndPrep(in)
|
n.URL = helpers.URLizeAndPrep(in)
|
||||||
n.Permalink = s.permalink(n.URL)
|
n.Permalink = s.permalink(n.URL)
|
||||||
n.RSSLink = template.HTML(s.permalink(in + ".xml"))
|
n.RSSLink = template.HTML(s.permalink(in + ".xml"))
|
||||||
|
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -92,16 +93,27 @@ func TestReadPagesFromSourceWithEmptySource(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func createAndRenderPages(t *testing.T, s *Site) {
|
func createAndRenderPages(t *testing.T, s *Site) {
|
||||||
if err := s.createPages(); err != nil {
|
createPagesAndMeta(t, s)
|
||||||
t.Fatalf("Unable to create pages: %s", err)
|
|
||||||
|
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 {
|
if err := s.buildSiteMeta(); err != nil {
|
||||||
t.Fatalf("Unable to build site metadata: %s", err)
|
t.Fatalf("Unable to build site metadata: %s", err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := s.renderPages(); err != nil {
|
func createPages(t *testing.T, s *Site) {
|
||||||
t.Fatalf("Unable to render pages. %s", err)
|
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()
|
s.initializeSiteInfo()
|
||||||
|
|
||||||
if err := s.createPages(); err != nil {
|
createPages(t, s)
|
||||||
t.Fatalf("Unable to create pages: %s", err)
|
|
||||||
}
|
|
||||||
return 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
|
// Testing Defaults.. Only draft:true and publishDate in the past should be rendered
|
||||||
s := siteSetup()
|
s := siteSetup()
|
||||||
if len(s.Pages) != 1 {
|
if len(s.AllPages) != 1 {
|
||||||
t.Fatal("Draft or Future dated content published unexpectedly")
|
t.Fatal("Draft or Future dated content published unexpectedly")
|
||||||
}
|
}
|
||||||
|
|
||||||
// only publishDate in the past should be rendered
|
// only publishDate in the past should be rendered
|
||||||
viper.Set("BuildDrafts", true)
|
viper.Set("BuildDrafts", true)
|
||||||
s = siteSetup()
|
s = siteSetup()
|
||||||
if len(s.Pages) != 2 {
|
if len(s.AllPages) != 2 {
|
||||||
t.Fatal("Future Dated Posts published unexpectedly")
|
t.Fatal("Future Dated Posts published unexpectedly")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -279,7 +290,7 @@ func TestDraftAndFutureRender(t *testing.T) {
|
||||||
viper.Set("BuildDrafts", false)
|
viper.Set("BuildDrafts", false)
|
||||||
viper.Set("BuildFuture", true)
|
viper.Set("BuildFuture", true)
|
||||||
s = siteSetup()
|
s = siteSetup()
|
||||||
if len(s.Pages) != 2 {
|
if len(s.AllPages) != 2 {
|
||||||
t.Fatal("Draft posts published unexpectedly")
|
t.Fatal("Draft posts published unexpectedly")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -287,7 +298,7 @@ func TestDraftAndFutureRender(t *testing.T) {
|
||||||
viper.Set("BuildDrafts", true)
|
viper.Set("BuildDrafts", true)
|
||||||
viper.Set("BuildFuture", true)
|
viper.Set("BuildFuture", true)
|
||||||
s = siteSetup()
|
s = siteSetup()
|
||||||
if len(s.Pages) != 4 {
|
if len(s.AllPages) != 4 {
|
||||||
t.Fatal("Drafts or Future posts not included as expected")
|
t.Fatal("Drafts or Future posts not included as expected")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -313,9 +324,8 @@ func TestFutureExpirationRender(t *testing.T) {
|
||||||
|
|
||||||
s.initializeSiteInfo()
|
s.initializeSiteInfo()
|
||||||
|
|
||||||
if err := s.createPages(); err != nil {
|
createPages(t, s)
|
||||||
t.Fatalf("Unable to create pages: %s", err)
|
|
||||||
}
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -323,17 +333,17 @@ func TestFutureExpirationRender(t *testing.T) {
|
||||||
|
|
||||||
s := siteSetup()
|
s := siteSetup()
|
||||||
|
|
||||||
if len(s.Pages) != 1 {
|
if len(s.AllPages) != 1 {
|
||||||
if len(s.Pages) > 1 {
|
if len(s.AllPages) > 1 {
|
||||||
t.Fatal("Expired content published unexpectedly")
|
t.Fatal("Expired content published unexpectedly")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(s.Pages) < 1 {
|
if len(s.AllPages) < 1 {
|
||||||
t.Fatal("Valid content expired unexpectedly")
|
t.Fatal("Valid content expired unexpectedly")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.Pages[0].Title == "doc2" {
|
if s.AllPages[0].Title == "doc2" {
|
||||||
t.Fatal("Expired content published unexpectedly")
|
t.Fatal("Expired content published unexpectedly")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -689,17 +699,7 @@ func TestAbsURLify(t *testing.T) {
|
||||||
|
|
||||||
s.prepTemplates("blue/single.html", templateWithURLAbs)
|
s.prepTemplates("blue/single.html", templateWithURLAbs)
|
||||||
|
|
||||||
if err := s.createPages(); err != nil {
|
createAndRenderPages(t, s)
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
file, expected string
|
file, expected string
|
||||||
|
@ -791,13 +791,7 @@ func TestOrderedPages(t *testing.T) {
|
||||||
}
|
}
|
||||||
s.initializeSiteInfo()
|
s.initializeSiteInfo()
|
||||||
|
|
||||||
if err := s.createPages(); err != nil {
|
createPagesAndMeta(t, s)
|
||||||
t.Fatalf("Unable to create pages: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.buildSiteMeta(); err != nil {
|
|
||||||
t.Fatalf("Unable to build site metadata: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.Sections["sect"][0].Weight != 2 || s.Sections["sect"][3].Weight != 6 {
|
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)
|
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()
|
s.initializeSiteInfo()
|
||||||
|
|
||||||
if err := s.createPages(); err != nil {
|
createPagesAndMeta(t, s)
|
||||||
t.Fatalf("Unable to create pages: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.buildSiteMeta(); err != nil {
|
|
||||||
t.Fatalf("Unable to build site metadata: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
rbysection, err := s.Pages.GroupBy("Section", "desc")
|
rbysection, err := s.Pages.GroupBy("Section", "desc")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1055,13 +1043,7 @@ func TestWeightedTaxonomies(t *testing.T) {
|
||||||
}
|
}
|
||||||
s.initializeSiteInfo()
|
s.initializeSiteInfo()
|
||||||
|
|
||||||
if err := s.createPages(); err != nil {
|
createPagesAndMeta(t, s)
|
||||||
t.Fatalf("Unable to create pages: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.buildSiteMeta(); err != nil {
|
|
||||||
t.Fatalf("Unable to build site metadata: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.Taxonomies["tags"]["a"][0].Page.Title != "foo" {
|
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)
|
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()
|
site.initializeSiteInfo()
|
||||||
|
|
||||||
if err := site.createPages(); err != nil {
|
createPagesAndMeta(t, site)
|
||||||
t.Fatalf("Unable to create pages: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return 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))
|
||||||
|
}
|
||||||
|
|
|
@ -97,12 +97,7 @@ func TestPageCount(t *testing.T) {
|
||||||
s.initializeSiteInfo()
|
s.initializeSiteInfo()
|
||||||
s.prepTemplates("indexes/blue.html", indexTemplate)
|
s.prepTemplates("indexes/blue.html", indexTemplate)
|
||||||
|
|
||||||
if err := s.createPages(); err != nil {
|
createPagesAndMeta(t, s)
|
||||||
t.Errorf("Unable to create pages: %s", err)
|
|
||||||
}
|
|
||||||
if err := s.buildSiteMeta(); err != nil {
|
|
||||||
t.Errorf("Unable to build site metadata: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.renderSectionLists(); err != nil {
|
if err := s.renderSectionLists(); err != nil {
|
||||||
t.Errorf("Unable to render section lists: %s", err)
|
t.Errorf("Unable to render section lists: %s", err)
|
||||||
|
|
|
@ -51,13 +51,7 @@ func TestSitemapOutput(t *testing.T) {
|
||||||
|
|
||||||
s.prepTemplates("sitemap.xml", SITEMAP_TEMPLATE)
|
s.prepTemplates("sitemap.xml", SITEMAP_TEMPLATE)
|
||||||
|
|
||||||
if err := s.createPages(); err != nil {
|
createPagesAndMeta(t, s)
|
||||||
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.renderHomePage(); err != nil {
|
if err := s.renderHomePage(); err != nil {
|
||||||
t.Fatalf("Unable to RenderHomePage: %s", err)
|
t.Fatalf("Unable to RenderHomePage: %s", err)
|
||||||
|
|
|
@ -20,18 +20,6 @@ import (
|
||||||
"github.com/spf13/viper"
|
"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) {
|
func TestByCountOrderOfTaxonomies(t *testing.T) {
|
||||||
viper.Reset()
|
viper.Reset()
|
||||||
defer viper.Reset()
|
defer viper.Reset()
|
||||||
|
|
59
hugolib/translations.go
Normal file
59
hugolib/translations.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/hugo/helpers"
|
"github.com/spf13/hugo/helpers"
|
||||||
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
// File represents a source content file.
|
// File represents a source content file.
|
||||||
|
@ -26,11 +27,15 @@ import (
|
||||||
type File struct {
|
type File struct {
|
||||||
relpath string // Original relative path, e.g. content/foo.txt
|
relpath string // Original relative path, e.g. content/foo.txt
|
||||||
logicalName string // foo.txt
|
logicalName string // foo.txt
|
||||||
|
baseName string // `post` for `post.md`, also `post.en` for `post.en.md`
|
||||||
Contents io.Reader
|
Contents io.Reader
|
||||||
section string // The first directory
|
section string // The first directory
|
||||||
dir string // The relative directory Path (minus file name)
|
dir string // The relative directory Path (minus file name)
|
||||||
ext string // Just the ext (eg txt)
|
ext string // Just the ext (eg txt)
|
||||||
uniqueID string // MD5 of the filename
|
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,
|
// 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.
|
// BaseFileName Filename without extension.
|
||||||
func (f *File) BaseFileName() string {
|
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.
|
// 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.dir, f.logicalName = filepath.Split(f.relpath)
|
||||||
f.ext = strings.TrimPrefix(filepath.Ext(f.LogicalName()), ".")
|
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.section = helpers.GuessSection(f.Dir())
|
||||||
f.uniqueID = helpers.Md5String(f.LogicalName())
|
f.uniqueID = helpers.Md5String(f.LogicalName())
|
||||||
|
|
||||||
|
|
|
@ -1920,5 +1920,7 @@ func init() {
|
||||||
"upper": func(a string) string { return strings.ToUpper(a) },
|
"upper": func(a string) string { return strings.ToUpper(a) },
|
||||||
"urlize": helpers.URLize,
|
"urlize": helpers.URLize,
|
||||||
"where": where,
|
"where": where,
|
||||||
|
"i18n": I18nTranslate,
|
||||||
|
"T": I18nTranslate,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
47
tpl/template_i18n.go
Normal file
47
tpl/template_i18n.go
Normal 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
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue