Add multilingual taxonomies

This commit is contained in:
Vincent Prouillet 2019-01-07 21:03:34 +01:00
parent cae9223ebd
commit 538866487b
16 changed files with 404 additions and 207 deletions

387
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -42,6 +42,8 @@ pub struct Taxonomy {
pub paginate_path: Option<String>, pub paginate_path: Option<String>,
/// Whether to generate a RSS feed only for each taxonomy term, defaults to false /// Whether to generate a RSS feed only for each taxonomy term, defaults to false
pub rss: bool, pub rss: bool,
/// The language for that taxonomy, only used in multilingual sites
pub lang: Option<String>,
} }
impl Taxonomy { impl Taxonomy {
@ -64,7 +66,7 @@ impl Taxonomy {
impl Default for Taxonomy { impl Default for Taxonomy {
fn default() -> Taxonomy { fn default() -> Taxonomy {
Taxonomy { name: String::new(), paginate_by: None, paginate_path: None, rss: false } Taxonomy { name: String::new(), paginate_by: None, paginate_path: None, rss: false, lang: None }
} }
} }

View file

@ -48,7 +48,7 @@ pub struct TaxonomyItem {
} }
impl TaxonomyItem { impl TaxonomyItem {
pub fn new(name: &str, path: &str, config: &Config, keys: Vec<Key>, library: &Library) -> Self { pub fn new(name: &str, taxonomy: &TaxonomyConfig, config: &Config, keys: Vec<Key>, library: &Library) -> Self {
// Taxonomy are almost always used for blogs so we filter by dates // Taxonomy are almost always used for blogs so we filter by dates
// and it's not like we can sort things across sections by anything other // and it's not like we can sort things across sections by anything other
// than dates // than dates
@ -64,7 +64,11 @@ impl TaxonomyItem {
.collect(); .collect();
let (mut pages, ignored_pages) = sort_pages_by_date(data); let (mut pages, ignored_pages) = sort_pages_by_date(data);
let slug = slugify(name); let slug = slugify(name);
let permalink = config.make_permalink(&format!("/{}/{}", path, slug)); let permalink = if let Some(ref lang) = taxonomy.lang {
config.make_permalink(&format!("/{}/{}/{}", lang, taxonomy.name, slug))
} else {
config.make_permalink(&format!("/{}/{}", taxonomy.name, slug))
};
// We still append pages without dates at the end // We still append pages without dates at the end
pages.extend(ignored_pages); pages.extend(ignored_pages);
@ -108,7 +112,7 @@ impl Taxonomy {
) -> Taxonomy { ) -> Taxonomy {
let mut sorted_items = vec![]; let mut sorted_items = vec![];
for (name, pages) in items { for (name, pages) in items {
sorted_items.push(TaxonomyItem::new(&name, &kind.name, config, pages, library)); sorted_items.push(TaxonomyItem::new(&name, &kind, config, pages, library));
} }
sorted_items.sort_by(|a, b| a.name.cmp(&b.name)); sorted_items.sort_by(|a, b| a.name.cmp(&b.name));
@ -186,6 +190,14 @@ pub fn find_taxonomies(config: &Config, library: &Library) -> Result<Vec<Taxonom
for (name, val) in &page.meta.taxonomies { for (name, val) in &page.meta.taxonomies {
if taxonomies_def.contains_key(name) { if taxonomies_def.contains_key(name) {
if taxonomies_def[name].lang != page.lang {
bail!(
"Page `{}` has taxonomy `{}` which is not available in that language",
page.file.path.display(),
name
);
}
all_taxonomies.entry(name).or_insert_with(HashMap::new); all_taxonomies.entry(name).or_insert_with(HashMap::new);
for v in val { for v in val {
@ -220,7 +232,7 @@ mod tests {
use super::*; use super::*;
use std::collections::HashMap; use std::collections::HashMap;
use config::{Config, Taxonomy as TaxonomyConfig}; use config::{Config, Taxonomy as TaxonomyConfig, Language};
use content::Page; use content::Page;
use library::Library; use library::Library;
@ -326,4 +338,112 @@ mod tests {
"Page `` has taxonomy `tags` which is not defined in config.toml" "Page `` has taxonomy `tags` which is not defined in config.toml"
); );
} }
#[test]
fn can_make_taxonomies_in_multiple_languages() {
let mut config = Config::default();
config.languages.push(Language {rss: false, code: "fr".to_string()});
let mut library = Library::new(2, 0, true);
config.taxonomies = vec![
TaxonomyConfig { name: "categories".to_string(), ..TaxonomyConfig::default() },
TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() },
TaxonomyConfig { name: "auteurs".to_string(), lang: Some("fr".to_string()), ..TaxonomyConfig::default() },
];
let mut page1 = Page::default();
let mut taxo_page1 = HashMap::new();
taxo_page1.insert("tags".to_string(), vec!["rust".to_string(), "db".to_string()]);
taxo_page1.insert("categories".to_string(), vec!["Programming tutorials".to_string()]);
page1.meta.taxonomies = taxo_page1;
library.insert_page(page1);
let mut page2 = Page::default();
let mut taxo_page2 = HashMap::new();
taxo_page2.insert("tags".to_string(), vec!["rust".to_string()]);
taxo_page2.insert("categories".to_string(), vec!["Other".to_string()]);
page2.meta.taxonomies = taxo_page2;
library.insert_page(page2);
let mut page3 = Page::default();
page3.lang = Some("fr".to_string());
let mut taxo_page3 = HashMap::new();
taxo_page3.insert("auteurs".to_string(), vec!["Vincent Prouillet".to_string()]);
page3.meta.taxonomies = taxo_page3;
library.insert_page(page3);
let taxonomies = find_taxonomies(&config, &library).unwrap();
let (tags, categories, authors) = {
let mut t = None;
let mut c = None;
let mut a = None;
for x in taxonomies {
match x.kind.name.as_ref() {
"tags" => t = Some(x),
"categories" => c = Some(x),
"auteurs" => a = Some(x),
_ => unreachable!(),
}
}
(t.unwrap(), c.unwrap(), a.unwrap())
};
assert_eq!(tags.items.len(), 2);
assert_eq!(categories.items.len(), 2);
assert_eq!(authors.items.len(), 1);
assert_eq!(tags.items[0].name, "db");
assert_eq!(tags.items[0].slug, "db");
assert_eq!(tags.items[0].permalink, "http://a-website.com/tags/db/");
assert_eq!(tags.items[0].pages.len(), 1);
assert_eq!(tags.items[1].name, "rust");
assert_eq!(tags.items[1].slug, "rust");
assert_eq!(tags.items[1].permalink, "http://a-website.com/tags/rust/");
assert_eq!(tags.items[1].pages.len(), 2);
assert_eq!(authors.items[0].name, "Vincent Prouillet");
assert_eq!(authors.items[0].slug, "vincent-prouillet");
assert_eq!(authors.items[0].permalink, "http://a-website.com/fr/auteurs/vincent-prouillet/");
assert_eq!(authors.items[0].pages.len(), 1);
assert_eq!(categories.items[0].name, "Other");
assert_eq!(categories.items[0].slug, "other");
assert_eq!(categories.items[0].permalink, "http://a-website.com/categories/other/");
assert_eq!(categories.items[0].pages.len(), 1);
assert_eq!(categories.items[1].name, "Programming tutorials");
assert_eq!(categories.items[1].slug, "programming-tutorials");
assert_eq!(
categories.items[1].permalink,
"http://a-website.com/categories/programming-tutorials/"
);
assert_eq!(categories.items[1].pages.len(), 1);
}
#[test]
fn errors_on_taxonomy_of_different_language() {
let mut config = Config::default();
config.languages.push(Language {rss: false, code: "fr".to_string()});
let mut library = Library::new(2, 0, false);
config.taxonomies =
vec![TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() }];
let mut page1 = Page::default();
page1.lang = Some("fr".to_string());
let mut taxo_page1 = HashMap::new();
taxo_page1.insert("tags".to_string(), vec!["rust".to_string(), "db".to_string()]);
page1.meta.taxonomies = taxo_page1;
library.insert_page(page1);
let taxonomies = find_taxonomies(&config, &library);
assert!(taxonomies.is_err());
let err = taxonomies.unwrap_err();
// no path as this is created by Default
assert_eq!(
err.description(),
"Page `` has taxonomy `tags` which is not available in that language"
);
}
} }

View file

@ -723,7 +723,13 @@ impl Site {
} }
ensure_directory_exists(&self.output_path)?; ensure_directory_exists(&self.output_path)?;
let output_path = self.output_path.join(&taxonomy.kind.name); let output_path = if let Some(ref lang) = taxonomy.kind.lang {
let mid_path = self.output_path.join(lang);
create_directory(&mid_path)?;
mid_path.join(&taxonomy.kind.name)
} else {
self.output_path.join(&taxonomy.kind.name)
};
let list_output = taxonomy.render_all_terms(&self.tera, &self.config, &self.library)?; let list_output = taxonomy.render_all_terms(&self.tera, &self.config, &self.library)?;
create_directory(&output_path)?; create_directory(&output_path)?;
create_file(&output_path.join("index.html"), &self.inject_livereload(list_output))?; create_file(&output_path.join("index.html"), &self.inject_livereload(list_output))?;

View file

@ -479,6 +479,7 @@ fn can_build_site_with_pagination_for_taxonomy() {
paginate_by: Some(2), paginate_by: Some(2),
paginate_path: None, paginate_path: None,
rss: true, rss: true,
lang: None,
}); });
site.load().unwrap(); site.load().unwrap();

View file

@ -125,4 +125,18 @@ fn can_build_multilingual_site() {
assert!(file_contains!(public, "fr/rss.xml", "https://example.com/fr/blog/something-else/")); assert!(file_contains!(public, "fr/rss.xml", "https://example.com/fr/blog/something-else/"));
// Italian doesn't have RSS enabled // Italian doesn't have RSS enabled
assert!(!file_exists!(public, "it/rss.xml")); assert!(!file_exists!(public, "it/rss.xml"));
// Taxonomies are per-language
assert!(file_exists!(public, "authors/index.html"));
assert!(file_contains!(public, "authors/index.html", "Queen"));
assert!(!file_contains!(public, "authors/index.html", "Vincent"));
assert!(!file_exists!(public, "auteurs/index.html"));
assert!(file_exists!(public, "authors/queen-elizabeth/rss.xml"));
assert!(!file_exists!(public, "fr/authors/index.html"));
assert!(file_exists!(public, "fr/auteurs/index.html"));
assert!(!file_contains!(public, "fr/auteurs/index.html", "Queen"));
assert!(file_contains!(public, "fr/auteurs/index.html", "Vincent"));
assert!(!file_exists!(public, "fr/auteurs/vincent-prouillet/rss.xml"));
} }

View file

@ -297,7 +297,7 @@ mod tests {
fn can_get_taxonomy() { fn can_get_taxonomy() {
let taxo_config = TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() }; let taxo_config = TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() };
let library = Library::new(0, 0, false); let library = Library::new(0, 0, false);
let tag = TaxonomyItem::new("Programming", "tags", &Config::default(), vec![], &library); let tag = TaxonomyItem::new("Programming", &taxo_config, &Config::default(), vec![], &library);
let tags = Taxonomy { kind: taxo_config, items: vec![tag] }; let tags = Taxonomy { kind: taxo_config, items: vec![tag] };
let taxonomies = vec![tags.clone()]; let taxonomies = vec![tags.clone()];
@ -336,7 +336,7 @@ mod tests {
fn can_get_taxonomy_url() { fn can_get_taxonomy_url() {
let taxo_config = TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() }; let taxo_config = TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() };
let library = Library::new(0, 0, false); let library = Library::new(0, 0, false);
let tag = TaxonomyItem::new("Programming", "tags", &Config::default(), vec![], &library); let tag = TaxonomyItem::new("Programming", &taxo_config, &Config::default(), vec![], &library);
let tags = Taxonomy { kind: taxo_config, items: vec![tag] }; let tags = Taxonomy { kind: taxo_config, items: vec![tag] };
let taxonomies = vec![tags.clone()]; let taxonomies = vec![tags.clone()];

View file

@ -16,6 +16,9 @@ languages = [
] ]
``` ```
If you want to use per-language taxonomies, ensure you set the `lang` field in their
configuration.
## Content ## Content
Once the languages are added in, you can start to translate your content. Zola Once the languages are added in, you can start to translate your content. Zola
uses the filename to detect the language: uses the filename to detect the language:

View file

@ -7,13 +7,14 @@ Zola has built-in support for taxonomies.
The first step is to define the taxonomies in your [config.toml](./documentation/getting-started/configuration.md). The first step is to define the taxonomies in your [config.toml](./documentation/getting-started/configuration.md).
A taxonomy has 4 variables: A taxonomy has 5 variables:
- `name`: a required string that will be used in the URLs, usually the plural version (i.e. tags, categories etc) - `name`: a required string that will be used in the URLs, usually the plural version (i.e. tags, categories etc)
- `paginate_by`: if this is set to a number, each term page will be paginated by this much. - `paginate_by`: if this is set to a number, each term page will be paginated by this much.
- `paginate_path`: if set, will be the path used by paginated page and the page number will be appended after it. - `paginate_path`: if set, will be the path used by paginated page and the page number will be appended after it.
For example the default would be page/1 For example the default would be page/1
- `rss`: if set to `true`, a RSS feed will be generated for each individual term. - `rss`: if set to `true`, a RSS feed will be generated for each individual term.
- `lang`: only set this if you are making a multilingual site and want to indicate which language this taxonomy is for
Once this is done, you can then set taxonomies in your content and Zola will pick Once this is done, you can then set taxonomies in your content and Zola will pick
them up: them up:

View file

@ -13,6 +13,11 @@ build_search_index = false
generate_rss = true generate_rss = true
taxonomies = [
{name = "authors", rss = true},
{name = "auteurs", lang = "fr"},
]
languages = [ languages = [
{code = "fr", rss = true}, {code = "fr", rss = true},
{code = "it", rss = false}, {code = "it", rss = false},

View file

@ -1,6 +1,9 @@
+++ +++
title = "Quelque chose" title = "Quelque chose"
date = 2018-10-09 date = 2018-10-09
[taxonomies]
auteurs = ["Vincent Prouillet"]
+++ +++
Un article Un article

View file

@ -1,6 +1,9 @@
+++ +++
title = "Something" title = "Something"
date = 2018-10-09 date = 2018-10-09
[taxonomies]
authors = ["Queen Elizabeth"]
+++ +++
A blog post A blog post

View file

@ -0,0 +1,3 @@
{% for author in terms %}
{{ author.name }} {{ author.slug }} {{ author.pages | length }}
{% endfor %}

View file

@ -0,0 +1,21 @@
{% if not paginator %}
Tag: {{ term.name }}
{% for page in term.pages %}
<article>
<h3 class="post__title"><a href="{{ page.permalink | safe }}">{{ page.title | safe }}</a></h3>
</article>
{% endfor %}
{% else %}
Tag: {{ term.name }}
{% for page in paginator.pages %}
{{page.title|safe}}
{% endfor %}
Num pagers: {{ paginator.number_pagers }}
Page size: {{ paginator.paginate_by }}
Current index: {{ paginator.current_index }}
First: {{ paginator.first | safe }}
Last: {{ paginator.last | safe }}
{% if paginator.previous %}has_prev{% endif%}
{% if paginator.next %}has_next{% endif%}
{% endif %}

View file

@ -0,0 +1,3 @@
{% for term in terms %}
{{ term.name }} {{ term.slug }} {{ term.pages | length }}
{% endfor %}

View file

@ -0,0 +1,21 @@
{% if not paginator %}
Tag: {{ term.name }}
{% for page in term.pages %}
<article>
<h3 class="post__title"><a href="{{ page.permalink | safe }}">{{ page.title | safe }}</a></h3>
</article>
{% endfor %}
{% else %}
Tag: {{ term.name }}
{% for page in paginator.pages %}
{{page.title|safe}}
{% endfor %}
Num pagers: {{ paginator.number_pagers }}
Page size: {{ paginator.paginate_by }}
Current index: {{ paginator.current_index }}
First: {{ paginator.first | safe }}
Last: {{ paginator.last | safe }}
{% if paginator.previous %}has_prev{% endif%}
{% if paginator.next %}has_next{% endif%}
{% endif %}