Browse Source

Add translations to page/sections

index-subcmd
Vincent Prouillet 5 years ago
parent
commit
19075191ff
16 changed files with 184 additions and 29 deletions
  1. +7
    -0
      components/library/src/content/file_info.rs
  2. +53
    -0
      components/library/src/content/ser.rs
  3. +58
    -7
      components/library/src/library.rs
  4. +1
    -1
      components/library/src/pagination/mod.rs
  5. +2
    -2
      components/library/src/taxonomies/mod.rs
  6. +10
    -10
      components/site/src/lib.rs
  7. +4
    -4
      components/site/tests/common.rs
  8. +12
    -1
      components/site/tests/site_i18n.rs
  9. +2
    -2
      components/templates/src/global_fns/mod.rs
  10. +22
    -2
      docs/content/documentation/templates/pages-sections.md
  11. +1
    -0
      test_site_i18n/content/blog/_index.fr.md
  12. +1
    -0
      test_site_i18n/content/blog/_index.it.md
  13. +1
    -0
      test_site_i18n/content/blog/_index.md
  14. +0
    -0
      test_site_i18n/content/blog/not-translated.md
  15. +6
    -0
      test_site_i18n/templates/page.html
  16. +4
    -0
      test_site_i18n/templates/section.html

+ 7
- 0
components/library/src/content/file_info.rs View File

@@ -46,6 +46,9 @@ pub struct FileInfo {
/// For example a file at content/kb/solutions/blabla.md will have 2 components:
/// `kb` and `solutions`
pub components: Vec<String>,
/// This is `parent` + `name`, used to find content referring to the same content but in
/// various languages.
pub canonical: PathBuf,
}

impl FileInfo {
@@ -74,6 +77,7 @@ impl FileInfo {
path: file_path,
// We don't care about grand parent for pages
grand_parent: None,
canonical: parent.join(&name),
parent,
name,
components,
@@ -96,6 +100,7 @@ impl FileInfo {
FileInfo {
filename: file_path.file_name().unwrap().to_string_lossy().to_string(),
path: file_path,
canonical: parent.join(&name),
parent,
grand_parent,
name,
@@ -128,6 +133,7 @@ impl FileInfo {
}

self.name = parts.swap_remove(0);
self.canonical = self.parent.join(&self.name);
let lang = parts.swap_remove(0);

Ok(Some(lang))
@@ -145,6 +151,7 @@ impl Default for FileInfo {
name: String::new(),
components: vec![],
relative: String::new(),
canonical: PathBuf::new(),
}
}
}


+ 53
- 0
components/library/src/content/ser.rs View File

@@ -7,6 +7,38 @@ use content::{Page, Section};
use library::Library;
use rendering::Header;

#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct TranslatedContent<'a> {
lang: &'a Option<String>,
permalink: &'a str,
title: &'a Option<String>,
}

impl<'a> TranslatedContent<'a> {
// copypaste eh, not worth creating an enum imo
pub fn find_all_sections(section: &'a Section, library: &'a Library) -> Vec<Self> {
let mut translations = vec![];

for key in &section.translations {
let other = library.get_section_by_key(*key);
translations.push(TranslatedContent { lang: &other.lang, permalink: &other.permalink, title: &other.meta.title });
}

translations
}

pub fn find_all_pages(page: &'a Page, library: &'a Library) -> Vec<Self> {
let mut translations = vec![];

for key in &page.translations {
let other = library.get_page_by_key(*key);
translations.push(TranslatedContent { lang: &other.lang, permalink: &other.permalink, title: &other.meta.title });
}

translations
}
}

#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct SerializingPage<'a> {
relative_path: &'a str,
@@ -35,6 +67,7 @@ pub struct SerializingPage<'a> {
heavier: Option<Box<SerializingPage<'a>>>,
earlier: Option<Box<SerializingPage<'a>>>,
later: Option<Box<SerializingPage<'a>>>,
translations: Vec<TranslatedContent<'a>>,
}

impl<'a> SerializingPage<'a> {
@@ -67,6 +100,8 @@ impl<'a> SerializingPage<'a> {
.map(|k| library.get_section_by_key(*k).file.relative.clone())
.collect();

let translations = TranslatedContent::find_all_pages(page, library);

SerializingPage {
relative_path: &page.file.relative,
ancestors,
@@ -94,6 +129,7 @@ impl<'a> SerializingPage<'a> {
heavier,
earlier,
later,
translations,
}
}

@@ -116,6 +152,12 @@ impl<'a> SerializingPage<'a> {
vec![]
};

let translations = if let Some(ref lib) = library {
TranslatedContent::find_all_pages(page, lib)
} else {
vec![]
};

SerializingPage {
relative_path: &page.file.relative,
ancestors,
@@ -143,6 +185,7 @@ impl<'a> SerializingPage<'a> {
heavier: None,
earlier: None,
later: None,
translations,
}
}
}
@@ -165,6 +208,7 @@ pub struct SerializingSection<'a> {
assets: &'a [String],
pages: Vec<SerializingPage<'a>>,
subsections: Vec<&'a str>,
translations: Vec<TranslatedContent<'a>>,
}

impl<'a> SerializingSection<'a> {
@@ -185,6 +229,7 @@ impl<'a> SerializingSection<'a> {
.iter()
.map(|k| library.get_section_by_key(*k).file.relative.clone())
.collect();
let translations = TranslatedContent::find_all_sections(section, library);

SerializingSection {
relative_path: &section.file.relative,
@@ -203,6 +248,7 @@ impl<'a> SerializingSection<'a> {
lang: &section.lang,
pages,
subsections,
translations,
}
}

@@ -218,6 +264,12 @@ impl<'a> SerializingSection<'a> {
vec![]
};

let translations = if let Some(ref lib) = library {
TranslatedContent::find_all_sections(section, lib)
} else {
vec![]
};

SerializingSection {
relative_path: &section.file.relative,
ancestors,
@@ -235,6 +287,7 @@ impl<'a> SerializingSection<'a> {
lang: &section.lang,
pages: vec![],
subsections: vec![],
translations,
}
}
}

+ 58
- 7
components/library/src/library.rs View File

@@ -22,18 +22,21 @@ pub struct Library {
/// All the sections of the site
sections: DenseSlotMap<Section>,
/// A mapping path -> key for pages so we can easily get their key
paths_to_pages: HashMap<PathBuf, Key>,
pub paths_to_pages: HashMap<PathBuf, Key>,
/// A mapping path -> key for sections so we can easily get their key
pub paths_to_sections: HashMap<PathBuf, Key>,
/// Whether we need to look for translations
is_multilingual: bool,
}

impl Library {
pub fn new(cap_pages: usize, cap_sections: usize) -> Self {
pub fn new(cap_pages: usize, cap_sections: usize, is_multilingual: bool) -> Self {
Library {
pages: DenseSlotMap::with_capacity(cap_pages),
sections: DenseSlotMap::with_capacity(cap_sections),
paths_to_pages: HashMap::with_capacity(cap_pages),
paths_to_sections: HashMap::with_capacity(cap_sections),
is_multilingual,
}
}

@@ -116,10 +119,10 @@ impl Library {
continue;
}
if let Some(section_key) =
self.paths_to_sections.get(&path.join(&section.file.filename))
{
parents.push(*section_key);
}
self.paths_to_sections.get(&path.join(&section.file.filename))
{
parents.push(*section_key);
}
}
ancestors.insert(section.file.path.clone(), parents);
}
@@ -169,6 +172,7 @@ impl Library {
}
}

self.populate_translations();
self.sort_sections_pages();

let sections = self.paths_to_sections.clone();
@@ -188,7 +192,8 @@ impl Library {
}
}

/// Sort all sections pages
/// Sort all sections pages according to sorting method given
/// Pages that cannot be sorted are set to the section.ignored_pages instead
pub fn sort_sections_pages(&mut self) {
let mut updates = HashMap::new();
for (key, section) in &self.sections {
@@ -268,6 +273,52 @@ impl Library {
}
}

/// Finds all the translations for each section/page and set the `translations`
/// field of each as needed
/// A no-op for sites without multiple languages
fn populate_translations(&mut self) {
if !self.is_multilingual {
return;
}

// Sections first
let mut sections_translations = HashMap::new();
for (key, section) in &self.sections {
sections_translations
.entry(section.file.canonical.clone()) // TODO: avoid this clone
.or_insert_with(Vec::new)
.push(key);
}

for (key, section) in self.sections.iter_mut() {
let translations = &sections_translations[&section.file.canonical];
if translations.len() == 1 {
section.translations = vec![];
continue;
}
section.translations = translations.iter().filter(|k| **k != key).cloned().collect();
}

// Same thing for pages
let mut pages_translations = HashMap::new();
for (key, page) in &self.pages {
pages_translations
.entry(page.file.canonical.clone()) // TODO: avoid this clone
.or_insert_with(Vec::new)
.push(key);
}

for (key, page) in self.pages.iter_mut() {
let translations = &pages_translations[&page.file.canonical];
if translations.len() == 1 {
page.translations = vec![];
continue;
}
page.translations = translations.iter().filter(|k| **k != key).cloned().collect();
}

}

/// Find all the orphan pages: pages that are in a folder without an `_index.md`
pub fn get_all_orphan_pages(&self) -> Vec<&Page> {
let pages_in_sections =


+ 1
- 1
components/library/src/pagination/mod.rs View File

@@ -254,7 +254,7 @@ mod tests {
}

fn create_library(is_index: bool) -> (Section, Library) {
let mut library = Library::new(3, 0);
let mut library = Library::new(3, 0, false);
library.insert_page(Page::default());
library.insert_page(Page::default());
library.insert_page(Page::default());


+ 2
- 2
components/library/src/taxonomies/mod.rs View File

@@ -227,7 +227,7 @@ mod tests {
#[test]
fn can_make_taxonomies() {
let mut config = Config::default();
let mut library = Library::new(2, 0);
let mut library = Library::new(2, 0, false);

config.taxonomies = vec![
TaxonomyConfig { name: "categories".to_string(), ..TaxonomyConfig::default() },
@@ -307,7 +307,7 @@ mod tests {
#[test]
fn errors_on_unknown_taxonomy() {
let mut config = Config::default();
let mut library = Library::new(2, 0);
let mut library = Library::new(2, 0, false);

config.taxonomies =
vec![TaxonomyConfig { name: "authors".to_string(), ..TaxonomyConfig::default() }];


+ 10
- 10
components/site/src/lib.rs View File

@@ -141,7 +141,7 @@ impl Site {
taxonomies: Vec::new(),
permalinks: HashMap::new(),
// We will allocate it properly later on
library: Library::new(0, 0),
library: Library::new(0, 0, false),
};

Ok(site)
@@ -173,7 +173,7 @@ impl Site {
}

pub fn set_base_url(&mut self, base_url: String) {
let mut imageproc = self.imageproc.lock().unwrap();
let mut imageproc = self.imageproc.lock().expect("Couldn't lock imageproc (set_base_url)");
imageproc.set_base_url(&base_url);
self.config.base_url = base_url;
}
@@ -189,14 +189,14 @@ impl Site {
let content_glob = format!("{}/{}", base_path, "content/**/*.md");

let (section_entries, page_entries): (Vec<_>, Vec<_>) = glob(&content_glob)
.unwrap()
.expect("Invalid glob")
.filter_map(|e| e.ok())
.filter(|e| !e.as_path().file_name().unwrap().to_str().unwrap().starts_with('.'))
.partition(|entry| {
entry.as_path().file_name().unwrap().to_str().unwrap().starts_with("_index.")
});

self.library = Library::new(page_entries.len(), section_entries.len());
self.library = Library::new(page_entries.len(), section_entries.len(), self.config.is_multilingual());

let sections = {
let config = &self.config;
@@ -452,12 +452,12 @@ impl Site {
}

pub fn num_img_ops(&self) -> usize {
let imageproc = self.imageproc.lock().unwrap();
let imageproc = self.imageproc.lock().expect("Couldn't lock imageproc (num_img_ops)");
imageproc.num_img_ops()
}

pub fn process_images(&self) -> Result<()> {
let mut imageproc = self.imageproc.lock().unwrap();
let mut imageproc = self.imageproc.lock().expect("Couldn't lock imageproc (process_images)");
imageproc.prune()?;
imageproc.do_process()
}
@@ -497,7 +497,7 @@ impl Site {
// Copy any asset we found previously into the same directory as the index.html
for asset in &page.assets {
let asset_path = asset.as_path();
copy(&asset_path, &current_path.join(asset_path.file_name().unwrap()))?;
copy(&asset_path, &current_path.join(asset_path.file_name().expect("Couldn't get filename from page asset")))?;
}

Ok(())
@@ -626,7 +626,7 @@ impl Site {
) -> Result<Vec<(PathBuf, PathBuf)>> {
let glob_string = format!("{}/**/*.{}", sass_path.display(), extension);
let files = glob(&glob_string)
.unwrap()
.expect("Invalid glob for sass")
.filter_map(|e| e.ok())
.filter(|entry| {
!entry.as_path().file_name().unwrap().to_string_lossy().starts_with('_')
@@ -920,7 +920,7 @@ impl Site {
// Copy any asset we found previously into the same directory as the index.html
for asset in &section.assets {
let asset_path = asset.as_path();
copy(&asset_path, &output_path.join(asset_path.file_name().unwrap()))?;
copy(&asset_path, &output_path.join(asset_path.file_name().expect("Failed to get asset filename for section")))?;
}

if render_pages {
@@ -957,7 +957,7 @@ impl Site {
/// Used only on reload
pub fn render_index(&self) -> Result<()> {
self.render_section(
&self.library.get_section(&self.content_path.join("_index.md")).unwrap(),
&self.library.get_section(&self.content_path.join("_index.md")).expect("Failed to get index section"),
false,
)
}


+ 4
- 4
components/site/tests/common.rs View File

@@ -27,10 +27,10 @@ macro_rules! file_contains {
for component in $path.split("/") {
path = path.join(component);
}
let mut file = std::fs::File::open(&path).unwrap();
let mut file = std::fs::File::open(&path).expect(&format!("Failed to open {:?}", $path));
let mut s = String::new();
file.read_to_string(&mut s).unwrap();
// println!("{}", s);
println!("{}", s);
s.contains($text)
}};
}
@@ -45,7 +45,7 @@ pub fn build_site(name: &str) -> (Site, TempDir, PathBuf) {
let tmp_dir = tempdir().expect("create temp dir");
let public = &tmp_dir.path().join("public");
site.set_output_path(&public);
site.build().unwrap();
site.build().expect("Couldn't build the site");
(site, tmp_dir, public.clone())
}

@@ -64,6 +64,6 @@ where
let tmp_dir = tempdir().expect("create temp dir");
let public = &tmp_dir.path().join("public");
site.set_output_path(&public);
site.build().unwrap();
site.build().expect("Couldn't build the site");
(site, tmp_dir, public.clone())
}

+ 12
- 1
components/site/tests/site_i18n.rs View File

@@ -70,10 +70,21 @@ fn can_build_multilingual_site() {
assert!(file_exists!(public, "base/index.html"));
assert!(file_exists!(public, "fr/base/index.html"));

// Sections are there as well
// Sections are there as well, with translations info
assert!(file_exists!(public, "blog/index.html"));
assert!(file_contains!(public, "blog/index.html", "Translated in fr: Mon blog https://example.com/fr/blog/"));
assert!(file_contains!(public, "blog/index.html", "Translated in it: Il mio blog https://example.com/it/blog/"));
assert!(file_exists!(public, "fr/blog/index.html"));
assert!(file_contains!(public, "fr/blog/index.html", "Language: fr"));
assert!(file_contains!(public, "fr/blog/index.html", "Translated in : My blog https://example.com/blog/"));
assert!(file_contains!(public, "fr/blog/index.html", "Translated in it: Il mio blog https://example.com/it/blog/"));

// Normal pages are there with the translations
assert!(file_exists!(public, "blog/something/index.html"));
assert!(file_contains!(public, "blog/something/index.html", "Translated in fr: Quelque chose https://example.com/fr/blog/something/"));
assert!(file_exists!(public, "fr/blog/something/index.html"));
assert!(file_contains!(public, "fr/blog/something/index.html", "Language: fr"));
assert!(file_contains!(public, "fr/blog/something/index.html", "Translated in : Something https://example.com/blog/something/"));

// sitemap contains all languages
assert!(file_exists!(public, "sitemap.xml"));


+ 2
- 2
components/templates/src/global_fns/mod.rs View File

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

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



+ 22
- 2
docs/content/documentation/templates/pages-sections.md View File

@@ -52,7 +52,9 @@ ancestors: Array<String>;
// The relative path from the `content` directory to the markdown file
relative_path: String;
// The language for the page if there is one
lang: String?
lang: String?;
// Information about all the available languages for that content
translations: Array<TranslatedContent>;
```

## Section variables
@@ -96,7 +98,9 @@ ancestors: Array<String>;
// The relative path from the `content` directory to the markdown file
relative_path: String;
// The language for the section if there is one
lang: String?
lang: String?;
// Information about all the available languages for that content
translations: Array<TranslatedContent>;
```

## Table of contents
@@ -116,3 +120,19 @@ permalink: String;
// All lower level headers below this header
children: Array<Header>;
```

## Translated content

Both page and section have a `translations` field which corresponds to an array of `TranslatedContent`. If your site is not using multiple languages,
this will always be an empty array.
A `TranslatedContent` has the following fields:

```ts
// The language code for that content, empty if it is the default language
lang: String?;
// The title of that content if there is one
title: String?;
// A permalink to that content
permalink: String;
```


+ 1
- 0
test_site_i18n/content/blog/_index.fr.md View File

@@ -1,4 +1,5 @@
+++
title = "Mon blog"
sort_by = "date"
insert_anchors = "right"
+++

+ 1
- 0
test_site_i18n/content/blog/_index.it.md View File

@@ -1,4 +1,5 @@
+++
title = "Il mio blog"
sort_by = "date"
insert_anchors = "right"
+++

+ 1
- 0
test_site_i18n/content/blog/_index.md View File

@@ -1,4 +1,5 @@
+++
title = "My blog"
sort_by = "date"
insert_anchors = "left"
+++

test_site_i18n/content/blog/not-in-frend.md → test_site_i18n/content/blog/not-translated.md View File


+ 6
- 0
test_site_i18n/templates/page.html View File

@@ -1,2 +1,8 @@
{{page.title}}
{{page.content | safe}}
Language: {{lang}}

{% for t in page.translations %}
Translated in {{t.lang|default(value=config.default_language)}}: {{t.title}} {{t.permalink|safe}}
{% endfor %}


+ 4
- 0
test_site_i18n/templates/section.html View File

@@ -2,3 +2,7 @@
{{page.title}}
{% endfor %}
Language: {{lang}}

{% for t in section.translations %}
Translated in {{t.lang|default(value=config.default_language)}}: {{t.title}} {{t.permalink|safe}}
{% endfor %}

Loading…
Cancel
Save