mirror of
https://github.com/gohugoio/hugo.git
synced 2025-04-17 16:39:44 +00: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
|
||||
}
|
||||
for i := 0; i < benchmarkTimes; i++ {
|
||||
MainSite = nil
|
||||
MainSites = nil
|
||||
_ = buildSite()
|
||||
}
|
||||
pprof.WriteHeapProfile(f)
|
||||
|
@ -76,7 +76,7 @@ func benchmark(cmd *cobra.Command, args []string) error {
|
|||
pprof.StartCPUProfile(f)
|
||||
defer pprof.StopCPUProfile()
|
||||
for i := 0; i < benchmarkTimes; i++ {
|
||||
MainSite = nil
|
||||
MainSites = nil
|
||||
_ = buildSite()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,10 +46,10 @@ import (
|
|||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// MainSite represents the Hugo site to build. This variable is exported as it
|
||||
// MainSites represents the Hugo sites to build. This variable is exported as it
|
||||
// is used by at least one external library (the Hugo caddy plugin). We should
|
||||
// provide a cleaner external API, but until then, this is it.
|
||||
var MainSite *hugolib.Site
|
||||
var MainSites map[string]*hugolib.Site
|
||||
|
||||
// Reset resets Hugo ready for a new full build. This is mainly only useful
|
||||
// for benchmark testing etc. via the CLI commands.
|
||||
|
@ -287,6 +287,7 @@ func loadDefaultSettings() {
|
|||
viper.SetDefault("ArchetypeDir", "archetypes")
|
||||
viper.SetDefault("PublishDir", "public")
|
||||
viper.SetDefault("DataDir", "data")
|
||||
viper.SetDefault("I18nDir", "i18n")
|
||||
viper.SetDefault("ThemesDir", "themes")
|
||||
viper.SetDefault("DefaultLayout", "post")
|
||||
viper.SetDefault("BuildDrafts", false)
|
||||
|
@ -323,6 +324,8 @@ func loadDefaultSettings() {
|
|||
viper.SetDefault("EnableEmoji", false)
|
||||
viper.SetDefault("PygmentsCodeFencesGuessSyntax", false)
|
||||
viper.SetDefault("UseModTimeAsFallback", false)
|
||||
viper.SetDefault("Multilingual", false)
|
||||
viper.SetDefault("DefaultContentLanguage", "en")
|
||||
}
|
||||
|
||||
// InitializeConfig initializes a config file with sensible default configuration flags.
|
||||
|
@ -490,6 +493,8 @@ func InitializeConfig(subCmdVs ...*cobra.Command) error {
|
|||
helpers.HugoReleaseVersion(), minVersion)
|
||||
}
|
||||
|
||||
readMultilingualConfiguration()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -506,7 +511,7 @@ func watchConfig() {
|
|||
viper.OnConfigChange(func(e fsnotify.Event) {
|
||||
fmt.Println("Config file changed:", e.Name)
|
||||
// Force a full rebuild
|
||||
MainSite = nil
|
||||
MainSites = nil
|
||||
utils.CheckErr(buildSite(true))
|
||||
if !viper.GetBool("DisableLiveReload") {
|
||||
// Will block forever trying to write to a channel that nobody is reading if livereload isn't initialized
|
||||
|
@ -632,6 +637,7 @@ func copyStatic() error {
|
|||
func getDirList() []string {
|
||||
var a []string
|
||||
dataDir := helpers.AbsPathify(viper.GetString("DataDir"))
|
||||
i18nDir := helpers.AbsPathify(viper.GetString("I18nDir"))
|
||||
layoutDir := helpers.AbsPathify(viper.GetString("LayoutDir"))
|
||||
staticDir := helpers.AbsPathify(viper.GetString("StaticDir"))
|
||||
walker := func(path string, fi os.FileInfo, err error) error {
|
||||
|
@ -639,8 +645,13 @@ func getDirList() []string {
|
|||
if path == dataDir && os.IsNotExist(err) {
|
||||
jww.WARN.Println("Skip DataDir:", err)
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
if path == i18nDir && os.IsNotExist(err) {
|
||||
jww.WARN.Println("Skip I18nDir:", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if path == layoutDir && os.IsNotExist(err) {
|
||||
jww.WARN.Println("Skip LayoutDir:", err)
|
||||
return nil
|
||||
|
@ -684,6 +695,7 @@ func getDirList() []string {
|
|||
|
||||
helpers.SymbolicWalk(hugofs.Source(), dataDir, walker)
|
||||
helpers.SymbolicWalk(hugofs.Source(), helpers.AbsPathify(viper.GetString("ContentDir")), walker)
|
||||
helpers.SymbolicWalk(hugofs.Source(), i18nDir, walker)
|
||||
helpers.SymbolicWalk(hugofs.Source(), helpers.AbsPathify(viper.GetString("LayoutDir")), walker)
|
||||
helpers.SymbolicWalk(hugofs.Source(), staticDir, walker)
|
||||
if helpers.ThemeSet() {
|
||||
|
@ -695,31 +707,52 @@ func getDirList() []string {
|
|||
|
||||
func buildSite(watching ...bool) (err error) {
|
||||
fmt.Println("Started building site")
|
||||
startTime := time.Now()
|
||||
if MainSite == nil {
|
||||
MainSite = new(hugolib.Site)
|
||||
t0 := time.Now()
|
||||
|
||||
if MainSites == nil {
|
||||
MainSites = make(map[string]*hugolib.Site)
|
||||
}
|
||||
if len(watching) > 0 && watching[0] {
|
||||
MainSite.RunMode.Watching = true
|
||||
|
||||
for _, lang := range langConfigsList {
|
||||
t1 := time.Now()
|
||||
mainSite, present := MainSites[lang]
|
||||
if !present {
|
||||
mainSite = new(hugolib.Site)
|
||||
MainSites[lang] = mainSite
|
||||
mainSite.SetMultilingualConfig(lang, langConfigsList, langConfigs)
|
||||
}
|
||||
|
||||
if len(watching) > 0 && watching[0] {
|
||||
mainSite.RunMode.Watching = true
|
||||
}
|
||||
|
||||
if err := mainSite.Build(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mainSite.Stats(lang, t1)
|
||||
}
|
||||
err = MainSite.Build()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
MainSite.Stats()
|
||||
jww.FEEDBACK.Printf("in %v ms\n", int(1000*time.Since(startTime).Seconds()))
|
||||
|
||||
jww.FEEDBACK.Printf("total in %v ms\n", int(1000*time.Since(t0).Seconds()))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func rebuildSite(events []fsnotify.Event) error {
|
||||
startTime := time.Now()
|
||||
err := MainSite.ReBuild(events)
|
||||
if err != nil {
|
||||
return err
|
||||
t0 := time.Now()
|
||||
|
||||
for _, lang := range langConfigsList {
|
||||
t1 := time.Now()
|
||||
mainSite := MainSites[lang]
|
||||
|
||||
if err := mainSite.ReBuild(events); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mainSite.Stats(lang, t1)
|
||||
}
|
||||
MainSite.Stats()
|
||||
jww.FEEDBACK.Printf("in %v ms\n", int(1000*time.Since(startTime).Seconds()))
|
||||
|
||||
jww.FEEDBACK.Printf("total in %v ms\n", int(1000*time.Since(t0).Seconds()))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ var listDraftsCmd = &cobra.Command{
|
|||
return newSystemError("Error Processing Source Content", err)
|
||||
}
|
||||
|
||||
for _, p := range site.Pages {
|
||||
for _, p := range site.AllPages {
|
||||
if p.IsDraft() {
|
||||
fmt.Println(filepath.Join(p.File.Dir(), p.File.LogicalName()))
|
||||
}
|
||||
|
@ -88,7 +88,7 @@ posted in the future.`,
|
|||
return newSystemError("Error Processing Source Content", err)
|
||||
}
|
||||
|
||||
for _, p := range site.Pages {
|
||||
for _, p := range site.AllPages {
|
||||
if p.IsFuture() {
|
||||
fmt.Println(filepath.Join(p.File.Dir(), p.File.LogicalName()))
|
||||
}
|
||||
|
@ -119,7 +119,7 @@ expired.`,
|
|||
return newSystemError("Error Processing Source Content", err)
|
||||
}
|
||||
|
||||
for _, p := range site.Pages {
|
||||
for _, p := range site.AllPages {
|
||||
if p.IsExpired() {
|
||||
fmt.Println(filepath.Join(p.File.Dir(), p.File.LogicalName()))
|
||||
}
|
||||
|
|
41
commands/multilingual.go
Normal file
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">
|
||||
{{ range .Params.tags }}
|
||||
<li><a href="{{ "/tags/" | relURL }}{{ . | urlize }}">{{ . }}</a> </li>
|
||||
<li><a href="{{ "/tags/" | relLangURL }}{{ . | urlize }}">{{ . }}</a> </li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
|
||||
|
@ -110,7 +110,8 @@ The following example displays all tag keys:
|
|||
|
||||
<ul id="all-tags">
|
||||
{{ range $name, $taxonomy := .Site.Taxonomies.tags }}
|
||||
<li><a href="{{ "/tags/" | relURL }}{{ $name | urlize }}">{{ $name }}</a></li>
|
||||
<<<<<<< HEAD
|
||||
<li><a href="{{ "/tags/" | relLangURL }}{{ $name | urlize }}">{{ $name }}</a></li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
|
||||
|
@ -120,7 +121,7 @@ This example will list all taxonomies, each of their keys and all the content as
|
|||
<section>
|
||||
<ul>
|
||||
{{ range $taxonomyname, $taxonomy := .Site.Taxonomies }}
|
||||
<li><a href="{{ "/" | relURL}}{{ $taxonomyname | urlize }}">{{ $taxonomyname }}</a>
|
||||
<li><a href="{{ "/" | relLangURL}}{{ $taxonomyname | urlize }}">{{ $taxonomyname }}</a>
|
||||
<ul>
|
||||
{{ range $key, $value := $taxonomy }}
|
||||
<li> {{ $key }} </li>
|
||||
|
@ -135,4 +136,3 @@ This example will list all taxonomies, each of their keys and all the content as
|
|||
{{ end }}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ Taxonomies can be ordered by either alphabetical key or by the number of content
|
|||
<ul>
|
||||
{{ $data := .Data }}
|
||||
{{ range $key, $value := .Data.Taxonomy.Alphabetical }}
|
||||
<li><a href="{{ $data.Plural }}/{{ $value.Name | urlize }}"> {{ $value.Name }} </a> {{ $value.Count }} </li>
|
||||
<li><a href="{{ .Site.LanguagePrefix }}/{{ $data.Plural }}/{{ $value.Name | urlize }}"> {{ $value.Name }} </a> {{ $value.Count }} </li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
|
||||
|
@ -38,7 +38,7 @@ Taxonomies can be ordered by either alphabetical key or by the number of content
|
|||
<ul>
|
||||
{{ $data := .Data }}
|
||||
{{ range $key, $value := .Data.Taxonomy.ByCount }}
|
||||
<li><a href="{{ $data.Plural }}/{{ $value.Name | urlize }}"> {{ $value.Name }} </a> {{ $value.Count }} </li>
|
||||
<li><a href="{{ .Site.LanguagePrefix }}/{{ $data.Plural }}/{{ $value.Name | urlize }}"> {{ $value.Name }} </a> {{ $value.Count }} </li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
|
||||
|
|
|
@ -435,6 +435,13 @@ e.g.
|
|||
|
||||
## Strings
|
||||
|
||||
### printf
|
||||
|
||||
Format a string using the standard `fmt.Sprintf` function. See [the go
|
||||
doc](https://golang.org/pkg/fmt/) for reference.
|
||||
|
||||
e.g., `{{ i18n ( printf "combined_%s" $var ) }}` or `{{ printf "formatted %.2f" 3.1416 }}`
|
||||
|
||||
### chomp
|
||||
Removes any trailing newline characters. Useful in a pipeline to remove newlines added by other processing (including `markdownify`).
|
||||
|
||||
|
@ -726,7 +733,6 @@ CJK-like languages.
|
|||
<!-- outputs a content length of 8 runes. -->
|
||||
```
|
||||
|
||||
|
||||
### md5
|
||||
|
||||
`md5` hashes the given input and returns its MD5 checksum.
|
||||
|
@ -752,6 +758,23 @@ This can be useful if you want to use Gravatar for generating a unique avatar:
|
|||
<!-- returns the string "c8b5b0e33d408246e30f53e32b8f7627a7a649d4" -->
|
||||
```
|
||||
|
||||
## Internationalization
|
||||
|
||||
### i18n
|
||||
|
||||
This translates a piece of content based on your `i18n/en-US.yaml`
|
||||
(and friends) files. You can use the
|
||||
[go-i18n](https://github.com/nicksnyder/go-i18n) tools to manage your
|
||||
translations. The translations can exist in both the theme and at the
|
||||
root of your repository.
|
||||
|
||||
e.g.: `{{ i18n "translation_id" }}`
|
||||
|
||||
|
||||
### T
|
||||
|
||||
`T` is an alias to `i18n`. E.g. `{{ T "translation_id" }}`.
|
||||
>>>>>>> Add multilingual support in Hugo
|
||||
|
||||
## Times
|
||||
|
||||
|
@ -763,7 +786,6 @@ This can be useful if you want to use Gravatar for generating a unique avatar:
|
|||
* `{{ (time "2016-05-28").YearDay }}` → 149
|
||||
* `{{ mul 1000 (time "2016-05-28T10:30:00.00+10:00").Unix }}` → 1464395400000 (Unix time in milliseconds)
|
||||
|
||||
|
||||
## URLs
|
||||
|
||||
### absURL, relURL
|
||||
|
|
|
@ -89,7 +89,7 @@ content tagged with each tag.
|
|||
<ul>
|
||||
{{ $data := .Data }}
|
||||
{{ range $key, $value := .Data.Terms }}
|
||||
<li><a href="{{ $data.Plural }}/{{ $key | urlize }}">{{ $key }}</a> {{ len $value }}</li>
|
||||
<li><a href="{{ .Site.LanguagePrefix }}/{{ $data.Plural }}/{{ $key | urlize }}">{{ $key }}</a> {{ len $value }}</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -109,7 +109,7 @@ Another example listing the content for each term (ordered by Date):
|
|||
|
||||
{{ $data := .Data }}
|
||||
{{ range $key,$value := .Data.Terms.ByCount }}
|
||||
<h2><a href="{{ $data.Plural }}/{{ $value.Name | urlize }}">{{ $value.Name }}</a> {{ $value.Count }}</h2>
|
||||
<h2><a href="{{ .Site.LanguagePrefix }}/{{ $data.Plural }}/{{ $value.Name | urlize }}">{{ $value.Name }}</a> {{ $value.Count }}</h2>
|
||||
<ul>
|
||||
{{ range $value.Pages.ByDate }}
|
||||
<li><a href="{{ .Permalink }}">{{ .Title }}</a></li>
|
||||
|
@ -140,7 +140,7 @@ Hugo can order the meta data in two different ways. It can be ordered:
|
|||
<ul>
|
||||
{{ $data := .Data }}
|
||||
{{ range $key, $value := .Data.Terms.Alphabetical }}
|
||||
<li><a href="{{ $data.Plural }}/{{ $value.Name | urlize }}">{{ $value.Name }}</a> {{ $value.Count }}</li>
|
||||
<li><a href="{{ .Site.LanguagePrefix }}/{{ $data.Plural }}/{{ $value.Name | urlize }}">{{ $value.Name }}</a> {{ $value.Count }}</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -158,7 +158,7 @@ Hugo can order the meta data in two different ways. It can be ordered:
|
|||
<ul>
|
||||
{{ $data := .Data }}
|
||||
{{ range $key, $value := .Data.Terms.ByCount }}
|
||||
<li><a href="{{ $data.Plural }}/{{ $value.Name | urlize }}">{{ $value.Name }}</a> {{ $value.Count }}</li>
|
||||
<li><a href="{{ .Site.LanguagePrefix }}/{{ $data.Plural }}/{{ $value.Name | urlize }}">{{ $value.Name }}</a> {{ $value.Count }}</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -58,6 +58,8 @@ matter, content or derived from file location.
|
|||
**.IsPage** Always true for page.<br>
|
||||
**.Site** See [Site Variables]({{< relref "#site-variables" >}}) below.<br>
|
||||
**.Hugo** See [Hugo Variables]({{< relref "#hugo-variables" >}}) below.<br>
|
||||
**.Translations** A map to other pages with the same filename, but with a different language-extension (like `post.fr.md`). Populated only if `Multilingual` is enabled in your site config.
|
||||
**.Lang** Taken from the language extension notation. Populated only if `Multilingual` is enabled for your site config.
|
||||
|
||||
## Page Params
|
||||
|
||||
|
@ -119,9 +121,9 @@ includes taxonomies, lists and the homepage.
|
|||
**.Site** See [Site Variables]({{< relref "#site-variables" >}}) below.<br>
|
||||
**.Hugo** See [Hugo Variables]({{< relref "#hugo-variables" >}}) below.<br>
|
||||
|
||||
### Taxonomy Term Variables
|
||||
### Taxonomy Terms Node Variables
|
||||
|
||||
[Taxonomy Terms](/templates/terms/) pages are of the type "node" and have the following additional variables.
|
||||
[Taxonomy Terms](/templates/terms/) pages are of the type "node" and have the following additional variables. These are available in `layouts/_defaults/terms.html` for example.
|
||||
|
||||
**.Data.Singular** The singular name of the taxonomy<br>
|
||||
**.Data.Plural** The plural name of the taxonomy<br>
|
||||
|
@ -132,14 +134,25 @@ includes taxonomies, lists and the homepage.
|
|||
|
||||
The last two can also be reversed: **.Data.Terms.Alphabetical.Reverse**, **.Data.Terms.ByCount.Reverse**.
|
||||
|
||||
### Taxonomies elsewhere
|
||||
|
||||
The **.Site.Taxonomies** variable holds all taxonomies defines site-wide. It is a map of the taxonomy name to a list of its values. For example: "tags" -> ["tag1", "tag2", "tag3"]. Each value, though, is not a string but rather a [Taxonomy variable](#the-taxonomy-variable).
|
||||
|
||||
#### The Taxonomy variable
|
||||
|
||||
The Taxonomy variable, available as **.Site.Taxonomies.tags** for example, contains the list of tags (values) and, for each of those, their corresponding content pages.
|
||||
|
||||
|
||||
|
||||
## Site Variables
|
||||
|
||||
Also available is `.Site` which has the following:
|
||||
|
||||
**.Site.BaseURL** The base URL for the site as defined in the site configuration file.<br>
|
||||
**.Site.RSSLink** The URL for the site RSS.<br>
|
||||
**.Site.Taxonomies** The [taxonomies](/taxonomies/usage/) for the entire site. Replaces the now-obsolete `.Site.Indexes` since v0.11.<br>
|
||||
**.Site.Pages** Array of all content ordered by Date, newest first. Replaces the now-deprecated `.Site.Recent` starting v0.13.<br>
|
||||
**.Site.Taxonomies** The [taxonomies](/taxonomies/usage/) for the entire site. Replaces the now-obsolete `.Site.Indexes` since v0.11. Also see section [Taxonomies elsewhere](#taxonomies-elsewhere).<br>
|
||||
**.Site.Pages** Array of all content ordered by Date, newest first. Replaces the now-deprecated `.Site.Recent` starting v0.13. This array contains only the pages in the current language.<br>
|
||||
**.Site.AllPages** Array of all pages regardless of their translation.<br>
|
||||
**.Site.Params** A container holding the values from the `params` section of your site configuration file. For example, a TOML config file might look like this:
|
||||
|
||||
baseurl = "http://yoursite.example.com/"
|
||||
|
@ -152,7 +165,7 @@ Also available is `.Site` which has the following:
|
|||
**.Site.Menus** All of the menus in the site.<br>
|
||||
**.Site.Title** A string representing the title of the site.<br>
|
||||
**.Site.Author** A map of the authors as defined in the site configuration.<br>
|
||||
**.Site.LanguageCode** A string representing the language as defined in the site configuration.<br>
|
||||
**.Site.LanguageCode** A string representing the language as defined in the site configuration. This is mostly used to populate the RSS feeds with the right language code.<br>
|
||||
**.Site.DisqusShortname** A string representing the shortname of the Disqus shortcode as defined in the site configuration.<br>
|
||||
**.Site.GoogleAnalytics** A string representing your tracking code for Google Analytics as defined in the site configuration.<br>
|
||||
**.Site.Copyright** A string representing the copyright of your web site as defined in the site configuration.<br>
|
||||
|
@ -160,6 +173,10 @@ Also available is `.Site` which has the following:
|
|||
**.Site.Permalinks** A string to override the default permalink format. Defined in the site configuration.<br>
|
||||
**.Site.BuildDrafts** A boolean (Default: false) to indicate whether to build drafts. Defined in the site configuration.<br>
|
||||
**.Site.Data** Custom data, see [Data Files](/extras/datafiles/).<br>
|
||||
**.Site.Multilingual** Whether the site supports internationalization of the content. With this mode enabled, all your posts' URLs will be prefixed with the language (ex: `/en/2016/01/01/my-post`)<br>
|
||||
**.Site.CurrentLanguage** This indicates which language you are currently rendering the website for. When using `Multilingual` mode, will render the site in this language. You can then run `hugo` again with a second `config` file, with the other languages. When using `i18n` and `T` template functions, it will use the `i18n/*.yaml` files (in either `/themes/[yourtheme]/i18n` or the `/i18n`, translations in the latter having precedence).<br>
|
||||
**.Site.LanguagePrefix** When `Multilingual` is enabled, this will hold `/{{ .Site.CurrentLanguage}}`, otherwise will be an empty string. Using this to prefix taxonomies or other hard-coded links ensures your keep your theme compatible with Multilingual configurations.
|
||||
**.Site.Languages** An ordered list of languages when Multilingual is enabled. Used in your templates to iterate through and create links to different languages.<br>
|
||||
|
||||
## File Variables
|
||||
|
||||
|
|
|
@ -182,6 +182,12 @@ func GetThemeDataDirPath() (string, error) {
|
|||
return getThemeDirPath("data")
|
||||
}
|
||||
|
||||
// GetThemeI18nDirPath returns the theme's i18n dir path if theme is set.
|
||||
// If theme is set and the i18n dir doesn't exist, an error is returned.
|
||||
func GetThemeI18nDirPath() (string, error) {
|
||||
return getThemeDirPath("i18n")
|
||||
}
|
||||
|
||||
func getThemeDirPath(path string) (string, error) {
|
||||
if !ThemeSet() {
|
||||
return "", errors.New("No theme set")
|
||||
|
|
|
@ -56,8 +56,8 @@ func doTestShortcodeCrossrefs(t *testing.T, relative bool) {
|
|||
templ := tpl.New()
|
||||
p, _ := pageFromString(simplePageWithURL, path)
|
||||
p.Node.Site = &SiteInfo{
|
||||
Pages: &(Pages{p}),
|
||||
BaseURL: template.URL(helpers.SanitizeURLKeepTrailingSlash(baseURL)),
|
||||
AllPages: &(Pages{p}),
|
||||
BaseURL: template.URL(helpers.SanitizeURLKeepTrailingSlash(baseURL)),
|
||||
}
|
||||
|
||||
output, err := HandleShortcodes(in, p, templ)
|
||||
|
|
36
hugolib/i18n.go
Normal file
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.initializeSiteInfo()
|
||||
|
||||
if err := s.createPages(); err != nil {
|
||||
t.Fatalf("Unable to create pages: %s", err)
|
||||
}
|
||||
|
||||
if err := s.buildSiteMeta(); err != nil {
|
||||
t.Fatalf("Unable to build site metadata: %s", err)
|
||||
}
|
||||
createPagesAndMeta(t, s)
|
||||
}
|
||||
|
||||
func tomlToMap(s string) (map[string]interface{}, error) {
|
||||
|
|
48
hugolib/multilingual.go
Normal file
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
|
||||
ExpiryDate time.Time
|
||||
Markup string
|
||||
Translations Translations
|
||||
extension string
|
||||
contentType string
|
||||
lang string
|
||||
renderable bool
|
||||
Layout string
|
||||
layoutsCalculated []string
|
||||
|
@ -300,9 +302,11 @@ func (p *Page) getRenderingConfig() *helpers.Blackfriday {
|
|||
|
||||
func newPage(filename string) *Page {
|
||||
page := Page{contentType: "",
|
||||
Source: Source{File: *source.NewFile(filename)},
|
||||
Node: Node{Keywords: []string{}, Sitemap: Sitemap{Priority: -1}},
|
||||
Params: make(map[string]interface{})}
|
||||
Source: Source{File: *source.NewFile(filename)},
|
||||
Node: Node{Keywords: []string{}, Sitemap: Sitemap{Priority: -1}},
|
||||
Params: make(map[string]interface{}),
|
||||
Translations: make(Translations),
|
||||
}
|
||||
|
||||
jww.DEBUG.Println("Reading from", page.File.Path())
|
||||
return &page
|
||||
|
@ -445,11 +449,13 @@ func (p *Page) permalink() (*url.URL, error) {
|
|||
if len(pSlug) > 0 {
|
||||
permalink = helpers.URLPrep(viper.GetBool("UglyURLs"), path.Join(dir, p.Slug+"."+p.Extension()))
|
||||
} else {
|
||||
_, t := filepath.Split(p.Source.LogicalName())
|
||||
t := p.Source.TranslationBaseName()
|
||||
permalink = helpers.URLPrep(viper.GetBool("UglyURLs"), path.Join(dir, helpers.ReplaceExtension(strings.TrimSpace(t), p.Extension())))
|
||||
}
|
||||
}
|
||||
|
||||
permalink = p.addMultilingualWebPrefix(permalink)
|
||||
|
||||
return helpers.MakePermalink(baseURL, permalink), nil
|
||||
}
|
||||
|
||||
|
@ -460,6 +466,10 @@ func (p *Page) Extension() string {
|
|||
return viper.GetString("DefaultExtension")
|
||||
}
|
||||
|
||||
func (p *Page) Lang() string {
|
||||
return p.lang
|
||||
}
|
||||
|
||||
func (p *Page) LinkTitle() string {
|
||||
if len(p.linkTitle) > 0 {
|
||||
return p.linkTitle
|
||||
|
@ -699,29 +709,29 @@ func (p *Page) getParam(key string, stringToLower bool) interface{} {
|
|||
return nil
|
||||
}
|
||||
|
||||
switch v.(type) {
|
||||
switch val := v.(type) {
|
||||
case bool:
|
||||
return v
|
||||
case time.Time:
|
||||
return v
|
||||
return val
|
||||
case string:
|
||||
if stringToLower {
|
||||
return strings.ToLower(val)
|
||||
}
|
||||
return val
|
||||
case int64, int32, int16, int8, int:
|
||||
return cast.ToInt(v)
|
||||
case float64, float32:
|
||||
return cast.ToFloat64(v)
|
||||
case time.Time:
|
||||
return val
|
||||
case []string:
|
||||
if stringToLower {
|
||||
return helpers.SliceToLower(val)
|
||||
}
|
||||
return v
|
||||
case map[string]interface{}: // JSON and TOML
|
||||
return v
|
||||
case map[interface{}]interface{}: // YAML
|
||||
return v
|
||||
case string:
|
||||
if stringToLower {
|
||||
return strings.ToLower(v.(string))
|
||||
}
|
||||
return v
|
||||
case []string:
|
||||
if stringToLower {
|
||||
return helpers.SliceToLower(v.([]string))
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
jww.ERROR.Printf("GetParam(\"%s\"): Unknown type %s\n", key, reflect.TypeOf(v))
|
||||
|
@ -851,6 +861,7 @@ func (p *Page) parse(reader io.Reader) error {
|
|||
p.renderable = psr.IsRenderable()
|
||||
p.frontmatter = psr.FrontMatter()
|
||||
p.rawContent = psr.Content()
|
||||
p.lang = p.Source.File.Lang()
|
||||
|
||||
meta, err := psr.Metadata()
|
||||
if meta != nil {
|
||||
|
@ -975,7 +986,6 @@ func (p *Page) FullFilePath() string {
|
|||
}
|
||||
|
||||
func (p *Page) TargetPath() (outfile string) {
|
||||
|
||||
// Always use URL if it's specified
|
||||
if len(strings.TrimSpace(p.URL)) > 2 {
|
||||
outfile = strings.TrimSpace(p.URL)
|
||||
|
@ -997,6 +1007,7 @@ func (p *Page) TargetPath() (outfile string) {
|
|||
outfile += "index.html"
|
||||
}
|
||||
outfile = filepath.FromSlash(outfile)
|
||||
outfile = p.addMultilingualFilesystemPrefix(outfile)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -1005,8 +1016,22 @@ func (p *Page) TargetPath() (outfile string) {
|
|||
outfile = strings.TrimSpace(p.Slug) + "." + p.Extension()
|
||||
} else {
|
||||
// Fall back to filename
|
||||
outfile = helpers.ReplaceExtension(p.Source.LogicalName(), p.Extension())
|
||||
outfile = helpers.ReplaceExtension(p.Source.TranslationBaseName(), p.Extension())
|
||||
}
|
||||
|
||||
return filepath.Join(strings.ToLower(helpers.MakePath(p.Source.Dir())), strings.TrimSpace(outfile))
|
||||
return p.addMultilingualFilesystemPrefix(filepath.Join(strings.ToLower(helpers.MakePath(p.Source.Dir())), strings.TrimSpace(outfile)))
|
||||
}
|
||||
|
||||
func (p *Page) addMultilingualWebPrefix(outfile string) string {
|
||||
if p.Lang() == "" {
|
||||
return outfile
|
||||
}
|
||||
return "/" + path.Join(p.Lang(), outfile)
|
||||
}
|
||||
|
||||
func (p *Page) addMultilingualFilesystemPrefix(outfile string) string {
|
||||
if p.Lang() == "" {
|
||||
return outfile
|
||||
}
|
||||
return string(filepath.Separator) + filepath.Join(p.Lang(), outfile)
|
||||
}
|
||||
|
|
|
@ -159,7 +159,7 @@ func pageToPermalinkTitle(p *Page, _ string) (string, error) {
|
|||
func pageToPermalinkFilename(p *Page, _ string) (string, error) {
|
||||
//var extension = p.Source.Ext
|
||||
//var name = p.Source.Path()[0 : len(p.Source.Path())-len(extension)]
|
||||
return helpers.URLize(p.Source.BaseFileName()), nil
|
||||
return helpers.URLize(p.Source.TranslationBaseName()), nil
|
||||
}
|
||||
|
||||
// if the page has a slug, return the slug, else return the title
|
||||
|
|
|
@ -25,7 +25,7 @@ func (s *Site) ShowPlan(out io.Writer) (err error) {
|
|||
fmt.Fprintf(out, "No source files provided.\n")
|
||||
}
|
||||
|
||||
for _, p := range s.Pages {
|
||||
for _, p := range s.AllPages {
|
||||
fmt.Fprintf(out, "%s", p.Source.Path())
|
||||
if p.IsRenderable() {
|
||||
fmt.Fprintf(out, " (renderer: markdown)")
|
||||
|
|
|
@ -46,13 +46,7 @@ func TestRobotsTXTOutput(t *testing.T) {
|
|||
|
||||
s.prepTemplates("robots.txt", robotTxtTemplate)
|
||||
|
||||
if err := s.createPages(); err != nil {
|
||||
t.Fatalf("Unable to create pages: %s", err)
|
||||
}
|
||||
|
||||
if err := s.buildSiteMeta(); err != nil {
|
||||
t.Fatalf("Unable to build site metadata: %s", err)
|
||||
}
|
||||
createPagesAndMeta(t, s)
|
||||
|
||||
if err := s.renderHomePage(); err != nil {
|
||||
t.Fatalf("Unable to RenderHomePage: %s", err)
|
||||
|
|
|
@ -59,13 +59,7 @@ func TestRSSOutput(t *testing.T) {
|
|||
s.initializeSiteInfo()
|
||||
s.prepTemplates("rss.xml", rssTemplate)
|
||||
|
||||
if err := s.createPages(); err != nil {
|
||||
t.Fatalf("Unable to create pages: %s", err)
|
||||
}
|
||||
|
||||
if err := s.buildSiteMeta(); err != nil {
|
||||
t.Fatalf("Unable to build site metadata: %s", err)
|
||||
}
|
||||
createPagesAndMeta(t, s)
|
||||
|
||||
if err := s.renderHomePage(); err != nil {
|
||||
t.Fatalf("Unable to RenderHomePage: %s", err)
|
||||
|
|
226
hugolib/site.go
226
hugolib/site.go
|
@ -20,6 +20,7 @@ import (
|
|||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
@ -29,8 +30,6 @@ import (
|
|||
|
||||
"sync/atomic"
|
||||
|
||||
"path"
|
||||
|
||||
"github.com/bep/inflect"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/spf13/afero"
|
||||
|
@ -76,6 +75,7 @@ var distinctErrorLogger = helpers.NewDistinctErrorLogger()
|
|||
// 5. The entire collection of files is written to disk.
|
||||
type Site struct {
|
||||
Pages Pages
|
||||
AllPages Pages
|
||||
Files []*source.File
|
||||
Tmpl tpl.Template
|
||||
Taxonomies TaxonomyList
|
||||
|
@ -87,6 +87,7 @@ type Site struct {
|
|||
targets targetList
|
||||
targetListInit sync.Once
|
||||
RunMode runmode
|
||||
Multilingual *Multilingual
|
||||
draftCount int
|
||||
futureCount int
|
||||
expiredCount int
|
||||
|
@ -106,7 +107,8 @@ type SiteInfo struct {
|
|||
Authors AuthorList
|
||||
Social SiteSocial
|
||||
Sections Taxonomy
|
||||
Pages *Pages
|
||||
Pages *Pages // Includes only pages in this language
|
||||
AllPages *Pages // Includes other translated pages, excluding those in this language.
|
||||
Files *[]*source.File
|
||||
Menus *Menus
|
||||
Hugo *HugoInfo
|
||||
|
@ -125,6 +127,11 @@ type SiteInfo struct {
|
|||
preserveTaxonomyNames bool
|
||||
paginationPageCount uint64
|
||||
Data *map[string]interface{}
|
||||
|
||||
Multilingual bool
|
||||
CurrentLanguage string
|
||||
LanguagePrefix string
|
||||
Languages []string
|
||||
}
|
||||
|
||||
// SiteSocial is a place to put social details on a site level. These are the
|
||||
|
@ -150,17 +157,17 @@ func (s *SiteInfo) GetParam(key string) interface{} {
|
|||
return nil
|
||||
}
|
||||
|
||||
switch v.(type) {
|
||||
switch val := v.(type) {
|
||||
case bool:
|
||||
return cast.ToBool(v)
|
||||
return val
|
||||
case string:
|
||||
return cast.ToString(v)
|
||||
return val
|
||||
case int64, int32, int16, int8, int:
|
||||
return cast.ToInt(v)
|
||||
case float64, float32:
|
||||
return cast.ToFloat64(v)
|
||||
case time.Time:
|
||||
return cast.ToTime(v)
|
||||
return val
|
||||
case []string:
|
||||
return v
|
||||
}
|
||||
|
@ -181,7 +188,7 @@ func (s *SiteInfo) refLink(ref string, page *Page, relative bool) (string, error
|
|||
var link string
|
||||
|
||||
if refURL.Path != "" {
|
||||
for _, page := range []*Page(*s.Pages) {
|
||||
for _, page := range []*Page(*s.AllPages) {
|
||||
refPath := filepath.FromSlash(refURL.Path)
|
||||
if page.Source.Path() == refPath || page.Source.LogicalName() == refPath {
|
||||
target = page
|
||||
|
@ -256,7 +263,7 @@ func (s *SiteInfo) SourceRelativeLink(ref string, currentPage *Page) (string, er
|
|||
}
|
||||
}
|
||||
|
||||
for _, page := range []*Page(*s.Pages) {
|
||||
for _, page := range []*Page(*s.AllPages) {
|
||||
if page.Source.Path() == refPath {
|
||||
target = page
|
||||
break
|
||||
|
@ -265,14 +272,14 @@ func (s *SiteInfo) SourceRelativeLink(ref string, currentPage *Page) (string, er
|
|||
// need to exhaust the test, then try with the others :/
|
||||
// if the refPath doesn't end in a filename with extension `.md`, then try with `.md` , and then `/index.md`
|
||||
mdPath := strings.TrimSuffix(refPath, string(os.PathSeparator)) + ".md"
|
||||
for _, page := range []*Page(*s.Pages) {
|
||||
for _, page := range []*Page(*s.AllPages) {
|
||||
if page.Source.Path() == mdPath {
|
||||
target = page
|
||||
break
|
||||
}
|
||||
}
|
||||
indexPath := filepath.Join(refPath, "index.md")
|
||||
for _, page := range []*Page(*s.Pages) {
|
||||
for _, page := range []*Page(*s.AllPages) {
|
||||
if page.Source.Path() == indexPath {
|
||||
target = page
|
||||
break
|
||||
|
@ -443,7 +450,7 @@ func (s *Site) ReBuild(events []fsnotify.Event) error {
|
|||
|
||||
// If a content file changes, we need to reload only it and re-render the entire site.
|
||||
|
||||
// First step is to read the changed files and (re)place them in site.Pages
|
||||
// First step is to read the changed files and (re)place them in site.AllPages
|
||||
// This includes processing any meta-data for that content
|
||||
|
||||
// The second step is to convert the content into HTML
|
||||
|
@ -479,7 +486,7 @@ func (s *Site) ReBuild(events []fsnotify.Event) error {
|
|||
if len(tmplChanged) > 0 || len(dataChanged) > 0 {
|
||||
// Do not need to read the files again, but they need conversion
|
||||
// for shortocde re-rendering.
|
||||
for _, p := range s.Pages {
|
||||
for _, p := range s.AllPages {
|
||||
pageChan <- p
|
||||
}
|
||||
}
|
||||
|
@ -538,6 +545,9 @@ func (s *Site) ReBuild(events []fsnotify.Event) error {
|
|||
|
||||
s.timerStep("read & convert pages from source")
|
||||
|
||||
// FIXME: does this go inside the next `if` statement ?
|
||||
s.setupTranslations()
|
||||
|
||||
if len(sourceChanged) > 0 {
|
||||
s.setupPrevNext()
|
||||
if err = s.buildSiteMeta(); err != nil {
|
||||
|
@ -665,9 +675,9 @@ func (s *Site) readDataFromSourceFS() error {
|
|||
dataSources = append(dataSources, &source.Filesystem{Base: s.absDataDir()})
|
||||
|
||||
// have to be last - duplicate keys in earlier entries will win
|
||||
themeStaticDir, err := helpers.GetThemeDataDirPath()
|
||||
themeDataDir, err := helpers.GetThemeDataDirPath()
|
||||
if err == nil {
|
||||
dataSources = append(dataSources, &source.Filesystem{Base: themeStaticDir})
|
||||
dataSources = append(dataSources, &source.Filesystem{Base: themeDataDir})
|
||||
}
|
||||
|
||||
err = s.loadData(dataSources)
|
||||
|
@ -688,10 +698,25 @@ func (s *Site) Process() (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
i18nSources := []source.Input{&source.Filesystem{Base: s.absI18nDir()}}
|
||||
|
||||
themeI18nDir, err := helpers.GetThemeI18nDirPath()
|
||||
if err == nil {
|
||||
i18nSources = []source.Input{&source.Filesystem{Base: themeI18nDir}, i18nSources[0]}
|
||||
}
|
||||
|
||||
if err = loadI18n(i18nSources, s.Multilingual.GetString("CurrentLanguage")); err != nil {
|
||||
return
|
||||
}
|
||||
s.timerStep("load i18n")
|
||||
|
||||
if err = s.createPages(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
s.setupTranslations()
|
||||
s.setupPrevNext()
|
||||
|
||||
if err = s.buildSiteMeta(); err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -711,6 +736,27 @@ func (s *Site) setupPrevNext() {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *Site) setupTranslations() {
|
||||
if !s.multilingualEnabled() {
|
||||
s.Pages = s.AllPages
|
||||
return
|
||||
}
|
||||
|
||||
currentLang := s.Multilingual.GetString("CurrentLanguage")
|
||||
|
||||
allTranslations := pagesToTranslationsMap(s.AllPages)
|
||||
assignTranslationsToPages(allTranslations, s.AllPages)
|
||||
|
||||
var currentLangPages []*Page
|
||||
for _, p := range s.AllPages {
|
||||
if p.Lang() == "" || strings.HasPrefix(currentLang, p.lang) {
|
||||
currentLangPages = append(currentLangPages, p)
|
||||
}
|
||||
}
|
||||
|
||||
s.Pages = currentLangPages
|
||||
}
|
||||
|
||||
func (s *Site) Render() (err error) {
|
||||
if err = s.renderAliases(); err != nil {
|
||||
return
|
||||
|
@ -771,32 +817,47 @@ func (s *Site) initialize() (err error) {
|
|||
}
|
||||
|
||||
func (s *Site) initializeSiteInfo() {
|
||||
params := viper.GetStringMap("Params")
|
||||
params := s.Multilingual.GetStringMap("Params")
|
||||
|
||||
permalinks := make(PermalinkOverrides)
|
||||
for k, v := range viper.GetStringMapString("Permalinks") {
|
||||
permalinks[k] = pathPattern(v)
|
||||
}
|
||||
|
||||
languagePrefix := ""
|
||||
if s.multilingualEnabled() {
|
||||
languagePrefix = "/" + s.Multilingual.GetString("CurrentLanguage")
|
||||
}
|
||||
|
||||
languages := []string{}
|
||||
if s.Multilingual != nil {
|
||||
languages = s.Multilingual.Languages
|
||||
}
|
||||
|
||||
s.Info = SiteInfo{
|
||||
BaseURL: template.URL(helpers.SanitizeURLKeepTrailingSlash(viper.GetString("BaseURL"))),
|
||||
Title: viper.GetString("Title"),
|
||||
Author: viper.GetStringMap("author"),
|
||||
Social: viper.GetStringMapString("social"),
|
||||
LanguageCode: viper.GetString("languagecode"),
|
||||
Copyright: viper.GetString("copyright"),
|
||||
DisqusShortname: viper.GetString("DisqusShortname"),
|
||||
Title: s.Multilingual.GetString("Title"),
|
||||
Author: s.Multilingual.GetStringMap("author"),
|
||||
Social: s.Multilingual.GetStringMapString("social"),
|
||||
LanguageCode: s.Multilingual.GetString("languagecode"),
|
||||
Copyright: s.Multilingual.GetString("copyright"),
|
||||
DisqusShortname: s.Multilingual.GetString("DisqusShortname"),
|
||||
Multilingual: s.multilingualEnabled(),
|
||||
CurrentLanguage: s.Multilingual.GetString("CurrentLanguage"),
|
||||
LanguagePrefix: languagePrefix,
|
||||
Languages: languages,
|
||||
GoogleAnalytics: viper.GetString("GoogleAnalytics"),
|
||||
RSSLink: s.permalinkStr(viper.GetString("RSSUri")),
|
||||
BuildDrafts: viper.GetBool("BuildDrafts"),
|
||||
canonifyURLs: viper.GetBool("CanonifyURLs"),
|
||||
preserveTaxonomyNames: viper.GetBool("PreserveTaxonomyNames"),
|
||||
Pages: &s.Pages,
|
||||
Files: &s.Files,
|
||||
Menus: &s.Menus,
|
||||
Params: params,
|
||||
Permalinks: permalinks,
|
||||
Data: &s.Data,
|
||||
AllPages: &s.AllPages,
|
||||
Pages: &s.Pages,
|
||||
Files: &s.Files,
|
||||
Menus: &s.Menus,
|
||||
Params: params,
|
||||
Permalinks: permalinks,
|
||||
Data: &s.Data,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -808,6 +869,10 @@ func (s *Site) absDataDir() string {
|
|||
return helpers.AbsPathify(viper.GetString("DataDir"))
|
||||
}
|
||||
|
||||
func (s *Site) absI18nDir() string {
|
||||
return helpers.AbsPathify(viper.GetString("I18nDir"))
|
||||
}
|
||||
|
||||
func (s *Site) absThemeDir() string {
|
||||
return helpers.AbsPathify(viper.GetString("themesDir") + "/" + viper.GetString("theme"))
|
||||
}
|
||||
|
@ -903,7 +968,7 @@ func (s *Site) convertSource() chan error {
|
|||
|
||||
go converterCollator(s, results, errs)
|
||||
|
||||
for _, p := range s.Pages {
|
||||
for _, p := range s.AllPages {
|
||||
pageChan <- p
|
||||
}
|
||||
|
||||
|
@ -997,7 +1062,7 @@ func converterCollator(s *Site, results <-chan HandledResult, errs chan<- error)
|
|||
|
||||
func (s *Site) addPage(page *Page) {
|
||||
if page.shouldBuild() {
|
||||
s.Pages = append(s.Pages, page)
|
||||
s.AllPages = append(s.AllPages, page)
|
||||
}
|
||||
|
||||
if page.IsDraft() {
|
||||
|
@ -1014,8 +1079,8 @@ func (s *Site) addPage(page *Page) {
|
|||
}
|
||||
|
||||
func (s *Site) removePageByPath(path string) {
|
||||
if i := s.Pages.FindPagePosByFilePath(path); i >= 0 {
|
||||
page := s.Pages[i]
|
||||
if i := s.AllPages.FindPagePosByFilePath(path); i >= 0 {
|
||||
page := s.AllPages[i]
|
||||
|
||||
if page.IsDraft() {
|
||||
s.draftCount--
|
||||
|
@ -1029,12 +1094,12 @@ func (s *Site) removePageByPath(path string) {
|
|||
s.expiredCount--
|
||||
}
|
||||
|
||||
s.Pages = append(s.Pages[:i], s.Pages[i+1:]...)
|
||||
s.AllPages = append(s.AllPages[:i], s.AllPages[i+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Site) removePage(page *Page) {
|
||||
if i := s.Pages.FindPagePos(page); i >= 0 {
|
||||
if i := s.AllPages.FindPagePos(page); i >= 0 {
|
||||
if page.IsDraft() {
|
||||
s.draftCount--
|
||||
}
|
||||
|
@ -1047,7 +1112,7 @@ func (s *Site) removePage(page *Page) {
|
|||
s.expiredCount--
|
||||
}
|
||||
|
||||
s.Pages = append(s.Pages[:i], s.Pages[i+1:]...)
|
||||
s.AllPages = append(s.AllPages[:i], s.AllPages[i+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1086,7 +1151,7 @@ func incrementalReadCollator(s *Site, results <-chan HandledResult, pageChan cha
|
|||
}
|
||||
}
|
||||
|
||||
s.Pages.Sort()
|
||||
s.AllPages.Sort()
|
||||
close(coordinator)
|
||||
|
||||
if len(errMsgs) == 0 {
|
||||
|
@ -1112,7 +1177,7 @@ func readCollator(s *Site, results <-chan HandledResult, errs chan<- error) {
|
|||
}
|
||||
}
|
||||
|
||||
s.Pages.Sort()
|
||||
s.AllPages.Sort()
|
||||
if len(errMsgs) == 0 {
|
||||
errs <- nil
|
||||
return
|
||||
|
@ -1298,9 +1363,8 @@ func (s *Site) resetPageBuildState() {
|
|||
|
||||
s.Info.paginationPageCount = 0
|
||||
|
||||
for _, p := range s.Pages {
|
||||
for _, p := range s.AllPages {
|
||||
p.scratch = newScratch()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1326,17 +1390,6 @@ func (s *Site) assembleSections() {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *Site) possibleTaxonomies() (taxonomies []string) {
|
||||
for _, p := range s.Pages {
|
||||
for k := range p.Params {
|
||||
if !helpers.InStringArray(taxonomies, k) {
|
||||
taxonomies = append(taxonomies, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// renderAliases renders shell pages that simply have a redirect in the header.
|
||||
func (s *Site) renderAliases() error {
|
||||
for _, p := range s.Pages {
|
||||
|
@ -1536,6 +1589,19 @@ func (s *Site) newTaxonomyNode(t taxRenderInfo) (*Node, string) {
|
|||
return n, base
|
||||
}
|
||||
|
||||
// addMultilingualPrefix adds the `en/` prefix to the path passed as parameter.
|
||||
// `basePath` must not start with http://
|
||||
func (s *Site) addMultilingualPrefix(basePath string) string {
|
||||
hadPrefix := strings.HasPrefix(basePath, "/")
|
||||
if s.multilingualEnabled() {
|
||||
basePath = path.Join(s.Multilingual.GetString("CurrentLanguage"), basePath)
|
||||
if hadPrefix {
|
||||
basePath = "/" + basePath
|
||||
}
|
||||
}
|
||||
return basePath
|
||||
}
|
||||
|
||||
func taxonomyRenderer(s *Site, taxes <-chan taxRenderInfo, results chan<- error, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
|
@ -1549,6 +1615,8 @@ func taxonomyRenderer(s *Site, taxes <-chan taxRenderInfo, results chan<- error,
|
|||
|
||||
n, base = s.newTaxonomyNode(t)
|
||||
|
||||
base = s.addMultilingualPrefix(base)
|
||||
|
||||
dest := base
|
||||
if viper.GetBool("UglyURLs") {
|
||||
dest = helpers.Uglify(base + ".html")
|
||||
|
@ -1623,7 +1691,7 @@ func (s *Site) renderListsOfTaxonomyTerms() (err error) {
|
|||
layouts := []string{"taxonomy/" + singular + ".terms.html", "_default/terms.html", "indexes/indexes.html"}
|
||||
layouts = s.appendThemeTemplates(layouts)
|
||||
if s.layoutExists(layouts...) {
|
||||
if err := s.renderAndWritePage("taxonomy terms for "+singular, plural+"/index.html", n, layouts...); err != nil {
|
||||
if err := s.renderAndWritePage("taxonomy terms for "+singular, s.addMultilingualPrefix(plural+"/index.html"), n, layouts...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -1664,8 +1732,10 @@ func (s *Site) renderSectionLists() error {
|
|||
section = helpers.MakePathSanitized(section)
|
||||
}
|
||||
|
||||
base := s.addMultilingualPrefix(section)
|
||||
|
||||
n := s.newSectionListNode(sectionName, section, data)
|
||||
if err := s.renderAndWritePage(fmt.Sprintf("section %s", section), section, n, s.appendThemeTemplates(layouts)...); err != nil {
|
||||
if err := s.renderAndWritePage(fmt.Sprintf("section %s", section), base, n, s.appendThemeTemplates(layouts)...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -1674,7 +1744,7 @@ func (s *Site) renderSectionLists() error {
|
|||
paginatePath := viper.GetString("paginatePath")
|
||||
|
||||
// write alias for page 1
|
||||
s.writeDestAlias(helpers.PaginateAliasPath(section, 1), s.permalink(section))
|
||||
s.writeDestAlias(helpers.PaginateAliasPath(base, 1), s.permalink(base))
|
||||
|
||||
pagers := n.paginator.Pagers()
|
||||
|
||||
|
@ -1692,7 +1762,7 @@ func (s *Site) renderSectionLists() error {
|
|||
sectionPagerNode.Lastmod = first.Lastmod
|
||||
}
|
||||
pageNumber := i + 1
|
||||
htmlBase := fmt.Sprintf("/%s/%s/%d", section, paginatePath, pageNumber)
|
||||
htmlBase := fmt.Sprintf("/%s/%s/%d", base, paginatePath, pageNumber)
|
||||
if err := s.renderAndWritePage(fmt.Sprintf("section %s", section), filepath.FromSlash(htmlBase), sectionPagerNode, layouts...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1702,10 +1772,10 @@ func (s *Site) renderSectionLists() error {
|
|||
if !viper.GetBool("DisableRSS") && section != "" {
|
||||
// XML Feed
|
||||
rssuri := viper.GetString("RSSUri")
|
||||
n.URL = s.permalinkStr(section + "/" + rssuri)
|
||||
n.Permalink = s.permalink(section)
|
||||
n.URL = s.permalinkStr(base + "/" + rssuri)
|
||||
n.Permalink = s.permalink(base)
|
||||
rssLayouts := []string{"section/" + section + ".rss.xml", "_default/rss.xml", "rss.xml", "_internal/_default/rss.xml"}
|
||||
if err := s.renderAndWriteXML("section "+section+" rss", section+"/"+rssuri, n, s.appendThemeTemplates(rssLayouts)...); err != nil {
|
||||
if err := s.renderAndWriteXML("section "+section+" rss", base+"/"+rssuri, n, s.appendThemeTemplates(rssLayouts)...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -1713,24 +1783,11 @@ func (s *Site) renderSectionLists() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *Site) newHomeNode() *Node {
|
||||
n := s.newNode()
|
||||
n.Title = n.Site.Title
|
||||
n.IsHome = true
|
||||
s.setURLs(n, "/")
|
||||
n.Data["Pages"] = s.Pages
|
||||
if len(s.Pages) != 0 {
|
||||
n.Date = s.Pages[0].Date
|
||||
n.Lastmod = s.Pages[0].Lastmod
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (s *Site) renderHomePage() error {
|
||||
n := s.newHomeNode()
|
||||
layouts := s.appendThemeTemplates([]string{"index.html", "_default/list.html"})
|
||||
|
||||
if err := s.renderAndWritePage("homepage", helpers.FilePathSeparator, n, layouts...); err != nil {
|
||||
if err := s.renderAndWritePage("homepage", s.addMultilingualPrefix(helpers.FilePathSeparator), n, layouts...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -1739,7 +1796,7 @@ func (s *Site) renderHomePage() error {
|
|||
paginatePath := viper.GetString("paginatePath")
|
||||
|
||||
// write alias for page 1
|
||||
s.writeDestAlias(helpers.PaginateAliasPath("", 1), s.permalink("/"))
|
||||
s.writeDestAlias(s.addMultilingualPrefix(helpers.PaginateAliasPath("", 1)), s.permalink("/"))
|
||||
|
||||
pagers := n.paginator.Pagers()
|
||||
|
||||
|
@ -1758,6 +1815,7 @@ func (s *Site) renderHomePage() error {
|
|||
}
|
||||
pageNumber := i + 1
|
||||
htmlBase := fmt.Sprintf("/%s/%d", paginatePath, pageNumber)
|
||||
htmlBase = s.addMultilingualPrefix(htmlBase)
|
||||
if err := s.renderAndWritePage(fmt.Sprintf("homepage"), filepath.FromSlash(htmlBase), homePagerNode, layouts...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1780,7 +1838,7 @@ func (s *Site) renderHomePage() error {
|
|||
|
||||
rssLayouts := []string{"rss.xml", "_default/rss.xml", "_internal/_default/rss.xml"}
|
||||
|
||||
if err := s.renderAndWriteXML("homepage rss", viper.GetString("RSSUri"), n, s.appendThemeTemplates(rssLayouts)...); err != nil {
|
||||
if err := s.renderAndWriteXML("homepage rss", s.addMultilingualPrefix(viper.GetString("RSSUri")), n, s.appendThemeTemplates(rssLayouts)...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -1804,6 +1862,19 @@ func (s *Site) renderHomePage() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *Site) newHomeNode() *Node {
|
||||
n := s.newNode()
|
||||
n.Title = n.Site.Title
|
||||
n.IsHome = true
|
||||
s.setURLs(n, "/")
|
||||
n.Data["Pages"] = s.Pages
|
||||
if len(s.Pages) != 0 {
|
||||
n.Date = s.Pages[0].Date
|
||||
n.Lastmod = s.Pages[0].Lastmod
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (s *Site) renderSitemap() error {
|
||||
if viper.GetBool("DisableSitemap") {
|
||||
return nil
|
||||
|
@ -1845,7 +1916,7 @@ func (s *Site) renderSitemap() error {
|
|||
|
||||
smLayouts := []string{"sitemap.xml", "_default/sitemap.xml", "_internal/_default/sitemap.xml"}
|
||||
|
||||
if err := s.renderAndWriteXML("sitemap", page.Sitemap.Filename, n, s.appendThemeTemplates(smLayouts)...); err != nil {
|
||||
if err := s.renderAndWriteXML("sitemap", s.addMultilingualPrefix(page.Sitemap.Filename), n, s.appendThemeTemplates(smLayouts)...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -1874,7 +1945,7 @@ func (s *Site) renderRobotsTXT() error {
|
|||
|
||||
// Stats prints Hugo builds stats to the console.
|
||||
// This is what you see after a successful hugo build.
|
||||
func (s *Site) Stats() {
|
||||
func (s *Site) Stats(lang string, t0 time.Time) {
|
||||
jww.FEEDBACK.Println(s.draftStats())
|
||||
jww.FEEDBACK.Println(s.futureStats())
|
||||
jww.FEEDBACK.Println(s.expiredStats())
|
||||
|
@ -1886,9 +1957,14 @@ func (s *Site) Stats() {
|
|||
for _, pl := range taxonomies {
|
||||
jww.FEEDBACK.Printf("%d %s created\n", len(s.Taxonomies[pl]), pl)
|
||||
}
|
||||
|
||||
if lang != "" {
|
||||
jww.FEEDBACK.Printf("rendered lang %q in %v ms\n", lang, int(1000*time.Since(t0).Seconds()))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Site) setURLs(n *Node, in string) {
|
||||
in = s.addMultilingualPrefix(in)
|
||||
n.URL = helpers.URLizeAndPrep(in)
|
||||
n.Permalink = s.permalink(n.URL)
|
||||
n.RSSLink = template.HTML(s.permalink(in + ".xml"))
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
@ -92,16 +93,27 @@ func TestReadPagesFromSourceWithEmptySource(t *testing.T) {
|
|||
}
|
||||
|
||||
func createAndRenderPages(t *testing.T, s *Site) {
|
||||
if err := s.createPages(); err != nil {
|
||||
t.Fatalf("Unable to create pages: %s", err)
|
||||
createPagesAndMeta(t, s)
|
||||
|
||||
if err := s.renderPages(); err != nil {
|
||||
t.Fatalf("Unable to render pages. %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func createPagesAndMeta(t *testing.T, s *Site) {
|
||||
createPages(t, s)
|
||||
|
||||
s.setupTranslations()
|
||||
s.setupPrevNext()
|
||||
|
||||
if err := s.buildSiteMeta(); err != nil {
|
||||
t.Fatalf("Unable to build site metadata: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.renderPages(); err != nil {
|
||||
t.Fatalf("Unable to render pages. %s", err)
|
||||
func createPages(t *testing.T, s *Site) {
|
||||
if err := s.createPages(); err != nil {
|
||||
t.Fatalf("Unable to create pages: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -254,9 +266,8 @@ func TestDraftAndFutureRender(t *testing.T) {
|
|||
|
||||
s.initializeSiteInfo()
|
||||
|
||||
if err := s.createPages(); err != nil {
|
||||
t.Fatalf("Unable to create pages: %s", err)
|
||||
}
|
||||
createPages(t, s)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
|
@ -264,14 +275,14 @@ func TestDraftAndFutureRender(t *testing.T) {
|
|||
|
||||
// Testing Defaults.. Only draft:true and publishDate in the past should be rendered
|
||||
s := siteSetup()
|
||||
if len(s.Pages) != 1 {
|
||||
if len(s.AllPages) != 1 {
|
||||
t.Fatal("Draft or Future dated content published unexpectedly")
|
||||
}
|
||||
|
||||
// only publishDate in the past should be rendered
|
||||
viper.Set("BuildDrafts", true)
|
||||
s = siteSetup()
|
||||
if len(s.Pages) != 2 {
|
||||
if len(s.AllPages) != 2 {
|
||||
t.Fatal("Future Dated Posts published unexpectedly")
|
||||
}
|
||||
|
||||
|
@ -279,7 +290,7 @@ func TestDraftAndFutureRender(t *testing.T) {
|
|||
viper.Set("BuildDrafts", false)
|
||||
viper.Set("BuildFuture", true)
|
||||
s = siteSetup()
|
||||
if len(s.Pages) != 2 {
|
||||
if len(s.AllPages) != 2 {
|
||||
t.Fatal("Draft posts published unexpectedly")
|
||||
}
|
||||
|
||||
|
@ -287,7 +298,7 @@ func TestDraftAndFutureRender(t *testing.T) {
|
|||
viper.Set("BuildDrafts", true)
|
||||
viper.Set("BuildFuture", true)
|
||||
s = siteSetup()
|
||||
if len(s.Pages) != 4 {
|
||||
if len(s.AllPages) != 4 {
|
||||
t.Fatal("Drafts or Future posts not included as expected")
|
||||
}
|
||||
|
||||
|
@ -313,9 +324,8 @@ func TestFutureExpirationRender(t *testing.T) {
|
|||
|
||||
s.initializeSiteInfo()
|
||||
|
||||
if err := s.createPages(); err != nil {
|
||||
t.Fatalf("Unable to create pages: %s", err)
|
||||
}
|
||||
createPages(t, s)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
|
@ -323,17 +333,17 @@ func TestFutureExpirationRender(t *testing.T) {
|
|||
|
||||
s := siteSetup()
|
||||
|
||||
if len(s.Pages) != 1 {
|
||||
if len(s.Pages) > 1 {
|
||||
if len(s.AllPages) != 1 {
|
||||
if len(s.AllPages) > 1 {
|
||||
t.Fatal("Expired content published unexpectedly")
|
||||
}
|
||||
|
||||
if len(s.Pages) < 1 {
|
||||
if len(s.AllPages) < 1 {
|
||||
t.Fatal("Valid content expired unexpectedly")
|
||||
}
|
||||
}
|
||||
|
||||
if s.Pages[0].Title == "doc2" {
|
||||
if s.AllPages[0].Title == "doc2" {
|
||||
t.Fatal("Expired content published unexpectedly")
|
||||
}
|
||||
}
|
||||
|
@ -689,17 +699,7 @@ func TestAbsURLify(t *testing.T) {
|
|||
|
||||
s.prepTemplates("blue/single.html", templateWithURLAbs)
|
||||
|
||||
if err := s.createPages(); err != nil {
|
||||
t.Fatalf("Unable to create pages: %s", err)
|
||||
}
|
||||
|
||||
if err := s.buildSiteMeta(); err != nil {
|
||||
t.Fatalf("Unable to build site metadata: %s", err)
|
||||
}
|
||||
|
||||
if err := s.renderPages(); err != nil {
|
||||
t.Fatalf("Unable to render pages. %s", err)
|
||||
}
|
||||
createAndRenderPages(t, s)
|
||||
|
||||
tests := []struct {
|
||||
file, expected string
|
||||
|
@ -791,13 +791,7 @@ func TestOrderedPages(t *testing.T) {
|
|||
}
|
||||
s.initializeSiteInfo()
|
||||
|
||||
if err := s.createPages(); err != nil {
|
||||
t.Fatalf("Unable to create pages: %s", err)
|
||||
}
|
||||
|
||||
if err := s.buildSiteMeta(); err != nil {
|
||||
t.Fatalf("Unable to build site metadata: %s", err)
|
||||
}
|
||||
createPagesAndMeta(t, s)
|
||||
|
||||
if s.Sections["sect"][0].Weight != 2 || s.Sections["sect"][3].Weight != 6 {
|
||||
t.Errorf("Pages in unexpected order. First should be '%d', got '%d'", 2, s.Sections["sect"][0].Weight)
|
||||
|
@ -865,13 +859,7 @@ func TestGroupedPages(t *testing.T) {
|
|||
}
|
||||
s.initializeSiteInfo()
|
||||
|
||||
if err := s.createPages(); err != nil {
|
||||
t.Fatalf("Unable to create pages: %s", err)
|
||||
}
|
||||
|
||||
if err := s.buildSiteMeta(); err != nil {
|
||||
t.Fatalf("Unable to build site metadata: %s", err)
|
||||
}
|
||||
createPagesAndMeta(t, s)
|
||||
|
||||
rbysection, err := s.Pages.GroupBy("Section", "desc")
|
||||
if err != nil {
|
||||
|
@ -1055,13 +1043,7 @@ func TestWeightedTaxonomies(t *testing.T) {
|
|||
}
|
||||
s.initializeSiteInfo()
|
||||
|
||||
if err := s.createPages(); err != nil {
|
||||
t.Fatalf("Unable to create pages: %s", err)
|
||||
}
|
||||
|
||||
if err := s.buildSiteMeta(); err != nil {
|
||||
t.Fatalf("Unable to build site metadata: %s", err)
|
||||
}
|
||||
createPagesAndMeta(t, s)
|
||||
|
||||
if s.Taxonomies["tags"]["a"][0].Page.Title != "foo" {
|
||||
t.Errorf("Pages in unexpected order, 'foo' expected first, got '%v'", s.Taxonomies["tags"]["a"][0].Page.Title)
|
||||
|
@ -1129,9 +1111,7 @@ func setupLinkingMockSite(t *testing.T) *Site {
|
|||
|
||||
site.initializeSiteInfo()
|
||||
|
||||
if err := site.createPages(); err != nil {
|
||||
t.Fatalf("Unable to create pages: %s", err)
|
||||
}
|
||||
createPagesAndMeta(t, site)
|
||||
|
||||
return site
|
||||
}
|
||||
|
@ -1341,3 +1321,159 @@ func TestSourceRelativeLinkFileing(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultilingualSwitch(t *testing.T) {
|
||||
// General settings
|
||||
viper.Set("DefaultExtension", "html")
|
||||
viper.Set("baseurl", "http://example.com/blog")
|
||||
viper.Set("DisableSitemap", false)
|
||||
viper.Set("DisableRSS", false)
|
||||
viper.Set("RSSUri", "index.xml")
|
||||
viper.Set("Taxonomies", map[string]string{"tag": "tags"})
|
||||
viper.Set("Permalinks", map[string]string{"other": "/somewhere/else/:filename"})
|
||||
|
||||
// Sources
|
||||
sources := []source.ByteSource{
|
||||
{filepath.FromSlash("sect/doc1.en.md"), []byte(`---
|
||||
title: doc1
|
||||
slug: doc1-slug
|
||||
tags:
|
||||
- tag1
|
||||
publishdate: "2000-01-01"
|
||||
---
|
||||
# doc1
|
||||
*some content*
|
||||
NOTE: slug should be used as URL
|
||||
`)},
|
||||
{filepath.FromSlash("sect/doc1.fr.md"), []byte(`---
|
||||
title: doc1
|
||||
tags:
|
||||
- tag1
|
||||
- tag2
|
||||
publishdate: "2000-01-04"
|
||||
---
|
||||
# doc1
|
||||
*quelque contenu*
|
||||
NOTE: should be in the 'en' Page's 'Translations' field.
|
||||
NOTE: date is after "doc3"
|
||||
`)},
|
||||
{filepath.FromSlash("sect/doc2.en.md"), []byte(`---
|
||||
title: doc2
|
||||
publishdate: "2000-01-02"
|
||||
---
|
||||
# doc2
|
||||
*some content*
|
||||
NOTE: without slug, "doc2" should be used, without ".en" as URL
|
||||
`)},
|
||||
{filepath.FromSlash("sect/doc3.en.md"), []byte(`---
|
||||
title: doc3
|
||||
publishdate: "2000-01-03"
|
||||
tags:
|
||||
- tag2
|
||||
url: /superbob
|
||||
---
|
||||
# doc3
|
||||
*some content*
|
||||
NOTE: third 'en' doc, should trigger pagination on home page.
|
||||
`)},
|
||||
{filepath.FromSlash("sect/doc4.md"), []byte(`---
|
||||
title: doc4
|
||||
tags:
|
||||
- tag1
|
||||
publishdate: "2000-01-05"
|
||||
---
|
||||
# doc4
|
||||
*du contenu francophone*
|
||||
NOTE: should use the DefaultContentLanguage and mark this doc as 'fr'.
|
||||
NOTE: doesn't have any corresponding translation in 'en'
|
||||
`)},
|
||||
{filepath.FromSlash("other/doc5.fr.md"), []byte(`---
|
||||
title: doc5
|
||||
publishdate: "2000-01-06"
|
||||
---
|
||||
# doc5
|
||||
*autre contenu francophone*
|
||||
NOTE: should use the "permalinks" configuration with :filename
|
||||
`)},
|
||||
}
|
||||
|
||||
hugofs.InitMemFs()
|
||||
|
||||
s := &Site{
|
||||
Source: &source.InMemorySource{ByteSource: sources},
|
||||
Multilingual: &Multilingual{
|
||||
config: viper.New(),
|
||||
enabled: true,
|
||||
},
|
||||
}
|
||||
// Multilingual settings
|
||||
viper.Set("Multilingual", true)
|
||||
s.Multilingual.config.Set("CurrentLanguage", "en")
|
||||
viper.Set("DefaultContentLanguage", "fr")
|
||||
viper.Set("paginate", "2")
|
||||
|
||||
s.prepTemplates()
|
||||
s.initializeSiteInfo()
|
||||
|
||||
createPagesAndMeta(t, s)
|
||||
|
||||
assert.Len(t, s.Source.Files(), 6, "should have 6 source files")
|
||||
assert.Len(t, s.Pages, 3, "should have 3 pages")
|
||||
assert.Len(t, s.AllPages, 6, "should have 6 total pages (including translations)")
|
||||
|
||||
doc1en := s.Pages[0]
|
||||
permalink, err := doc1en.Permalink()
|
||||
assert.NoError(t, err, "permalink call failed")
|
||||
assert.Equal(t, "http://example.com/blog/en/sect/doc1-slug", permalink, "invalid doc1.en permalink")
|
||||
assert.Len(t, doc1en.Translations, 1, "doc1-en should have one translation, excluding itself")
|
||||
|
||||
doc2 := s.Pages[1]
|
||||
permalink, err = doc2.Permalink()
|
||||
assert.NoError(t, err, "permalink call failed")
|
||||
assert.Equal(t, "http://example.com/blog/en/sect/doc2", permalink, "invalid doc2 permalink")
|
||||
|
||||
doc3 := s.Pages[2]
|
||||
permalink, err = doc3.Permalink()
|
||||
assert.NoError(t, err, "permalink call failed")
|
||||
assert.Equal(t, "http://example.com/blog/superbob", permalink, "invalid doc3 permalink")
|
||||
assert.Equal(t, "/superbob", doc3.URL, "invalid url, was specified on doc3")
|
||||
|
||||
assert.Equal(t, doc2.Next, doc3, "doc3 should follow doc2, in .Next")
|
||||
|
||||
doc1fr := doc1en.Translations["fr"]
|
||||
permalink, err = doc1fr.Permalink()
|
||||
assert.NoError(t, err, "permalink call failed")
|
||||
assert.Equal(t, "http://example.com/blog/fr/sect/doc1", permalink, "invalid doc1fr permalink")
|
||||
|
||||
assert.Equal(t, doc1en.Translations["fr"], doc1fr, "doc1-en should have doc1-fr as translation")
|
||||
assert.Equal(t, doc1fr.Translations["en"], doc1en, "doc1-fr should have doc1-en as translation")
|
||||
|
||||
doc4 := s.AllPages[4]
|
||||
permalink, err = doc4.Permalink()
|
||||
assert.NoError(t, err, "permalink call failed")
|
||||
assert.Equal(t, "http://example.com/blog/fr/sect/doc4", permalink, "invalid doc4 permalink")
|
||||
assert.Len(t, doc4.Translations, 0, "found translations for doc4")
|
||||
|
||||
doc5 := s.AllPages[5]
|
||||
permalink, err = doc5.Permalink()
|
||||
assert.NoError(t, err, "permalink call failed")
|
||||
assert.Equal(t, "http://example.com/blog/fr/somewhere/else/doc5", permalink, "invalid doc5 permalink")
|
||||
|
||||
// Taxonomies and their URLs
|
||||
assert.Len(t, s.Taxonomies, 1, "should have 1 taxonomy")
|
||||
tags := s.Taxonomies["tags"]
|
||||
assert.Len(t, tags, 2, "should have 2 different tags")
|
||||
assert.Equal(t, tags["tag1"][0].Page, doc1en, "first tag1 page should be doc1")
|
||||
|
||||
// Expect the tags locations to be in certain places, with the /en/ prefixes, etc..
|
||||
}
|
||||
|
||||
func assertFileContent(t *testing.T, path string, content string) {
|
||||
fl, err := hugofs.Destination().Open(path)
|
||||
assert.NoError(t, err, "file content not found when asserting on content of %s", path)
|
||||
|
||||
cnt, err := ioutil.ReadAll(fl)
|
||||
assert.NoError(t, err, "cannot read file content when asserting on content of %s", path)
|
||||
|
||||
assert.Equal(t, content, string(cnt))
|
||||
}
|
||||
|
|
|
@ -97,12 +97,7 @@ func TestPageCount(t *testing.T) {
|
|||
s.initializeSiteInfo()
|
||||
s.prepTemplates("indexes/blue.html", indexTemplate)
|
||||
|
||||
if err := s.createPages(); err != nil {
|
||||
t.Errorf("Unable to create pages: %s", err)
|
||||
}
|
||||
if err := s.buildSiteMeta(); err != nil {
|
||||
t.Errorf("Unable to build site metadata: %s", err)
|
||||
}
|
||||
createPagesAndMeta(t, s)
|
||||
|
||||
if err := s.renderSectionLists(); err != nil {
|
||||
t.Errorf("Unable to render section lists: %s", err)
|
||||
|
|
|
@ -51,13 +51,7 @@ func TestSitemapOutput(t *testing.T) {
|
|||
|
||||
s.prepTemplates("sitemap.xml", SITEMAP_TEMPLATE)
|
||||
|
||||
if err := s.createPages(); err != nil {
|
||||
t.Fatalf("Unable to create pages: %s", err)
|
||||
}
|
||||
|
||||
if err := s.buildSiteMeta(); err != nil {
|
||||
t.Fatalf("Unable to build site metadata: %s", err)
|
||||
}
|
||||
createPagesAndMeta(t, s)
|
||||
|
||||
if err := s.renderHomePage(); err != nil {
|
||||
t.Fatalf("Unable to RenderHomePage: %s", err)
|
||||
|
|
|
@ -20,18 +20,6 @@ import (
|
|||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func TestSitePossibleTaxonomies(t *testing.T) {
|
||||
site := new(Site)
|
||||
page, _ := NewPageFrom(strings.NewReader(pageYamlWithTaxonomiesA), "path/to/page")
|
||||
site.Pages = append(site.Pages, page)
|
||||
taxonomies := site.possibleTaxonomies()
|
||||
if !compareStringSlice(taxonomies, []string{"tags", "categories"}) {
|
||||
if !compareStringSlice(taxonomies, []string{"categories", "tags"}) {
|
||||
t.Fatalf("possible taxonomies do not match [tags categories]. Got: %s", taxonomies)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestByCountOrderOfTaxonomies(t *testing.T) {
|
||||
viper.Reset()
|
||||
defer viper.Reset()
|
||||
|
|
59
hugolib/translations.go
Normal file
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"
|
||||
|
||||
"github.com/spf13/hugo/helpers"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// File represents a source content file.
|
||||
|
@ -26,11 +27,15 @@ import (
|
|||
type File struct {
|
||||
relpath string // Original relative path, e.g. content/foo.txt
|
||||
logicalName string // foo.txt
|
||||
baseName string // `post` for `post.md`, also `post.en` for `post.en.md`
|
||||
Contents io.Reader
|
||||
section string // The first directory
|
||||
dir string // The relative directory Path (minus file name)
|
||||
ext string // Just the ext (eg txt)
|
||||
uniqueID string // MD5 of the filename
|
||||
|
||||
translationBaseName string // `post` for `post.es.md` (if `Multilingual` is enabled.)
|
||||
lang string // The language code if `Multilingual` is enabled
|
||||
}
|
||||
|
||||
// UniqueID is the MD5 hash of the filename and is for most practical applications,
|
||||
|
@ -51,7 +56,17 @@ func (f *File) Bytes() []byte {
|
|||
|
||||
// BaseFileName Filename without extension.
|
||||
func (f *File) BaseFileName() string {
|
||||
return helpers.Filename(f.LogicalName())
|
||||
return f.baseName
|
||||
}
|
||||
|
||||
// Filename with no extension, not even the optional language extension part.
|
||||
func (f *File) TranslationBaseName() string {
|
||||
return f.translationBaseName
|
||||
}
|
||||
|
||||
// Lang for this page, if `Multilingual` is enabled on your site.
|
||||
func (f *File) Lang() string {
|
||||
return f.lang
|
||||
}
|
||||
|
||||
// Section is first directory below the content root.
|
||||
|
@ -108,6 +123,17 @@ func NewFile(relpath string) *File {
|
|||
|
||||
f.dir, f.logicalName = filepath.Split(f.relpath)
|
||||
f.ext = strings.TrimPrefix(filepath.Ext(f.LogicalName()), ".")
|
||||
f.baseName = helpers.Filename(f.LogicalName())
|
||||
if viper.GetBool("Multilingual") {
|
||||
f.lang = strings.TrimPrefix(filepath.Ext(f.baseName), ".")
|
||||
if f.lang == "" {
|
||||
f.lang = viper.GetString("DefaultContentLanguage")
|
||||
}
|
||||
f.translationBaseName = helpers.Filename(f.baseName)
|
||||
} else {
|
||||
f.translationBaseName = f.baseName
|
||||
}
|
||||
|
||||
f.section = helpers.GuessSection(f.Dir())
|
||||
f.uniqueID = helpers.Md5String(f.LogicalName())
|
||||
|
||||
|
|
|
@ -1920,5 +1920,7 @@ func init() {
|
|||
"upper": func(a string) string { return strings.ToUpper(a) },
|
||||
"urlize": helpers.URLize,
|
||||
"where": where,
|
||||
"i18n": I18nTranslate,
|
||||
"T": I18nTranslate,
|
||||
}
|
||||
}
|
||||
|
|
47
tpl/template_i18n.go
Normal file
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
Reference in a new issue