mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-30 01:53:34 -05:00
Rework the multilingual docs
And in the same go adjusted some minor parts of the language API: Add LanguagePrefix alias to Node and rename the Multilingual config section to Languages. See #2309
This commit is contained in:
parent
ed0985404d
commit
f0b91852ea
5 changed files with 51 additions and 168 deletions
|
@ -13,7 +13,7 @@ import (
|
||||||
|
|
||||||
func readMultilingualConfiguration() (*hugolib.HugoSites, error) {
|
func readMultilingualConfiguration() (*hugolib.HugoSites, error) {
|
||||||
sites := make([]*hugolib.Site, 0)
|
sites := make([]*hugolib.Site, 0)
|
||||||
multilingual := viper.GetStringMap("Multilingual")
|
multilingual := viper.GetStringMap("Languages")
|
||||||
if len(multilingual) == 0 {
|
if len(multilingual) == 0 {
|
||||||
// TODO(bep) multilingo langConfigsList = append(langConfigsList, hugolib.NewLanguage("en"))
|
// TODO(bep) multilingo langConfigsList = append(langConfigsList, hugolib.NewLanguage("en"))
|
||||||
sites = append(sites, hugolib.NewSite(hugolib.NewLanguage("en")))
|
sites = append(sites, hugolib.NewSite(hugolib.NewLanguage("en")))
|
||||||
|
|
|
@ -9,13 +9,12 @@ title: Multilingual Mode
|
||||||
weight: 68
|
weight: 68
|
||||||
toc: true
|
toc: true
|
||||||
---
|
---
|
||||||
|
Hugo supports multiple languages side-by-side (added in `Hugo 0.17`). Define the available languages in a `Languages` section in your top-level `config.yaml` (or equivalent).
|
||||||
|
|
||||||
Since version 0.17, Hugo supports a native Multilingual mode. In your
|
Example:
|
||||||
top-level `config.yaml` (or equivalent), you define the available
|
|
||||||
languages in a `Multilingual` section such as:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
Multilingual:
|
Languages:
|
||||||
en:
|
en:
|
||||||
weight: 1
|
weight: 1
|
||||||
title: "My blog"
|
title: "My blog"
|
||||||
|
@ -23,7 +22,6 @@ Multilingual:
|
||||||
linkedin: "english-link"
|
linkedin: "english-link"
|
||||||
fr:
|
fr:
|
||||||
weight: 2
|
weight: 2
|
||||||
|
|
||||||
title: "Mon blog"
|
title: "Mon blog"
|
||||||
params:
|
params:
|
||||||
linkedin: "lien-francais"
|
linkedin: "lien-francais"
|
||||||
|
@ -33,21 +31,16 @@ copyright: "Everything is mine"
|
||||||
```
|
```
|
||||||
|
|
||||||
Anything not defined in a `[lang]:` block will fall back to the global
|
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
|
value for that key (like `copyright` for the English (`en`) language in this example).
|
||||||
example).
|
|
||||||
|
|
||||||
With the config above, all content, sitemap, RSS feeds, paginations
|
With the config above, all content, sitemap, RSS feeds, paginations
|
||||||
and taxonomy pages will be rendered under `/en` in English, and under
|
and taxonomy pages will be rendered below `/en` in English, and below `/fr` in French.
|
||||||
`/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).
|
|
||||||
|
|
||||||
|
Only the obvious non-global options can be overridden per language. Examples of global options are `BaseURL`, `BuildDrafts`, etc.
|
||||||
|
|
||||||
### Translating your content
|
### Translating your content
|
||||||
|
|
||||||
Translated articles are picked up by the name of the content files.
|
Translated articles are identified by the name of the content file.
|
||||||
|
|
||||||
Example of translated articles:
|
Example of translated articles:
|
||||||
|
|
||||||
|
@ -59,180 +52,66 @@ You can also have:
|
||||||
1. `/content/about.md`
|
1. `/content/about.md`
|
||||||
2. `/content/about.fr.md`
|
2. `/content/about.fr.md`
|
||||||
|
|
||||||
in which case the config variable `DefaultContentLanguage` will be
|
In which case the config variable `DefaultContentLanguage` will be used to affect the default language `about.md`. This way, you can
|
||||||
used to affect the default language `about.md`. This way, you can
|
slowly start to translate your current content without having to rename everything.
|
||||||
slowly start to translate your current content without having to
|
|
||||||
rename everything.
|
|
||||||
|
|
||||||
If left unspecified, the value for `DefaultContentLanguage` defaults
|
If left unspecified, the value for `DefaultContentLanguage` defaults to `en`.
|
||||||
to `en`.
|
|
||||||
|
|
||||||
By having the same _base file name_, the content pieces are linked
|
By having the same _base file name_, the content pieces are linked together as translated pieces.
|
||||||
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.
|
|
||||||
|
|
||||||
|
### Link to translated content
|
||||||
|
|
||||||
### Language switching links
|
To create a list of links to translated content, use a template similar to this:
|
||||||
|
|
||||||
Here is a simple example if all your pages are translated:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
{{if .IsPage}}
|
{{ $translations := .Translations }}
|
||||||
{{ range $txLang := .Site.Languages }}
|
{{ if gt (len $translations) 0 }}
|
||||||
{{if isset $.Translations $txLang}}
|
<h4>{{ i18n "translations" }}</h4>
|
||||||
<a href="{{ (index $.Translations $txLang).Permalink }}">{{ i18n ( printf "language_switcher_%s" $txLang ) }}</a>
|
<ul>
|
||||||
{{end}}
|
{{ range $translations }}
|
||||||
{{end}}
|
<li>
|
||||||
{{end}}
|
<a href="{{ .Permalink }}">{{ .Lang }}: {{ .Title }}</a>
|
||||||
|
</li>
|
||||||
{{if .IsNode}}
|
{{ end}}
|
||||||
{{ range $txLang := .Site.Languages }}
|
</ul>
|
||||||
<a href="/{{$txLang}}">{{ i18n ( printf "language_switcher_%s" $txLang ) }}</a>
|
{{ end }}
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
```
|
```
|
||||||
|
The above can be put in a `partial` and included in any template, be it for a content page or the home page. It will not print anything if there are no translations for a given page, or if it is -- in the case of the home page, section listing etc. -- a site with only one language.
|
||||||
|
|
||||||
This is a more complete example. It handles missing translations and will support non-multilingual sites. Better for theme authors:
|
The above also uses the `i8n` func, see [Translation of strings](#translation-of-strings).
|
||||||
|
|
||||||
```
|
|
||||||
{{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
|
### Translation of strings
|
||||||
|
|
||||||
Hugo uses [go-i18n](https://github.com/nicksnyder/go-i18n) to support
|
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.
|
||||||
string translations. Follow the link to find tools to manage your
|
|
||||||
translation workflows.
|
|
||||||
|
|
||||||
Translations are collected from the `themes/[name]/i18n/` folder
|
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. Language files should be named according to RFC 5646 with names such as `en-US.yaml`, `fr.yaml`, etc.
|
||||||
(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:
|
From within your templates, use the `i18n` function like this:
|
||||||
|
|
||||||
```
|
```
|
||||||
{{ i18n "home" }}
|
{{ i18n "home" }}
|
||||||
```
|
```
|
||||||
|
This uses a definition like this one in `i18n/en-US.yaml`:
|
||||||
to use a definition like this one in `i18n/en-US.yaml`:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
- id: home
|
- id: home
|
||||||
translation: "Home"
|
translation: "Home"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Often you will want to use to the page variables in the translations strings. To do that, pass on the "." context when calling `18n`:
|
||||||
|
|
||||||
|
```
|
||||||
|
{{ i18n "wordCount" . }}
|
||||||
|
```
|
||||||
|
This uses a definition like this one in `i18n/en-US.yaml`:
|
||||||
|
```
|
||||||
|
- id: wordCount
|
||||||
|
translation: "This article has {{ .WordCount }} words."
|
||||||
|
```
|
||||||
|
|
||||||
### Multilingual Themes support
|
### Multilingual Themes support
|
||||||
|
|
||||||
To support Multilingual mode in your themes, you only need to make
|
To support Multilingual mode in your themes, some considerations must be taken for the URLs in the templates. If there are more than one language, URLs must either come from the built-in `.Permalink` or `.URL`, be constructed with `relURL` or `absURL` -- or prefixed with `{{.LanguagePrefix }}`.
|
||||||
sure URLs defined manually (those not using `.Permalink` or `.URL`
|
|
||||||
variables) in your templates are prefixed with `{{
|
If there are more than one language defined, 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 single-language sites.
|
||||||
.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.
|
|
||||||
|
|
|
@ -48,7 +48,7 @@ func testCommonResetState() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func _TestMultiSites(t *testing.T) {
|
func TestMultiSites(t *testing.T) {
|
||||||
|
|
||||||
sites := createMultiTestSites(t)
|
sites := createMultiTestSites(t)
|
||||||
|
|
||||||
|
@ -65,7 +65,7 @@ func _TestMultiSites(t *testing.T) {
|
||||||
if len(enSite.Pages) != 3 {
|
if len(enSite.Pages) != 3 {
|
||||||
t.Fatal("Expected 3 english pages")
|
t.Fatal("Expected 3 english pages")
|
||||||
}
|
}
|
||||||
assert.Len(t, enSite.Source.Files(), 6, "should have 6 source files")
|
assert.Len(t, enSite.Source.Files(), 11, "should have 11 source files")
|
||||||
assert.Len(t, enSite.AllPages, 6, "should have 6 total pages (including translations)")
|
assert.Len(t, enSite.AllPages, 6, "should have 6 total pages (including translations)")
|
||||||
|
|
||||||
doc1en := enSite.Pages[0]
|
doc1en := enSite.Pages[0]
|
||||||
|
|
|
@ -201,6 +201,10 @@ func (n *Node) Lang() string {
|
||||||
return n.lang
|
return n.lang
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *Node) LanguagePrefix() string {
|
||||||
|
return n.Site.LanguagePrefix
|
||||||
|
}
|
||||||
|
|
||||||
// AllTranslations returns all translations, including the current Node.
|
// AllTranslations returns all translations, including the current Node.
|
||||||
// Note that this and the one below is kind of a temporary hack before #2297 is solved.
|
// Note that this and the one below is kind of a temporary hack before #2297 is solved.
|
||||||
func (n *Node) AllTranslations() Nodes {
|
func (n *Node) AllTranslations() Nodes {
|
||||||
|
|
|
@ -866,7 +866,7 @@ func (s *Site) initializeSiteInfo() {
|
||||||
CurrentLanguage: lang.Lang,
|
CurrentLanguage: lang.Lang,
|
||||||
LanguagePrefix: languagePrefix,
|
LanguagePrefix: languagePrefix,
|
||||||
Languages: languages,
|
Languages: languages,
|
||||||
GoogleAnalytics: viper.GetString("GoogleAnalytics"),
|
GoogleAnalytics: lang.GetString("GoogleAnalytics"),
|
||||||
RSSLink: permalinkStr(viper.GetString("RSSUri")),
|
RSSLink: permalinkStr(viper.GetString("RSSUri")),
|
||||||
BuildDrafts: viper.GetBool("BuildDrafts"),
|
BuildDrafts: viper.GetBool("BuildDrafts"),
|
||||||
canonifyURLs: viper.GetBool("CanonifyURLs"),
|
canonifyURLs: viper.GetBool("CanonifyURLs"),
|
||||||
|
|
Loading…
Reference in a new issue