From 19075191ffec973d4e0f27c86cb1465700a3c1ef Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Fri, 4 Jan 2019 20:31:31 +0100 Subject: [PATCH] Add translations to page/sections --- components/library/src/content/file_info.rs | 7 ++ components/library/src/content/ser.rs | 53 +++++++++++++++ components/library/src/library.rs | 65 +++++++++++++++++-- components/library/src/pagination/mod.rs | 2 +- components/library/src/taxonomies/mod.rs | 4 +- components/site/src/lib.rs | 20 +++--- components/site/tests/common.rs | 8 +-- components/site/tests/site_i18n.rs | 13 +++- components/templates/src/global_fns/mod.rs | 4 +- .../documentation/templates/pages-sections.md | 24 ++++++- test_site_i18n/content/blog/_index.fr.md | 1 + test_site_i18n/content/blog/_index.it.md | 1 + test_site_i18n/content/blog/_index.md | 1 + .../{not-in-frend.md => not-translated.md} | 0 test_site_i18n/templates/page.html | 6 ++ test_site_i18n/templates/section.html | 4 ++ 16 files changed, 184 insertions(+), 29 deletions(-) rename test_site_i18n/content/blog/{not-in-frend.md => not-translated.md} (100%) diff --git a/components/library/src/content/file_info.rs b/components/library/src/content/file_info.rs index 58a5990c..7111fa50 100644 --- a/components/library/src/content/file_info.rs +++ b/components/library/src/content/file_info.rs @@ -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, + /// 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(), } } } diff --git a/components/library/src/content/ser.rs b/components/library/src/content/ser.rs index 1c902eb6..56a27685 100644 --- a/components/library/src/content/ser.rs +++ b/components/library/src/content/ser.rs @@ -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, + permalink: &'a str, + title: &'a Option, +} + +impl<'a> TranslatedContent<'a> { + // copypaste eh, not worth creating an enum imo + pub fn find_all_sections(section: &'a Section, library: &'a Library) -> Vec { + let mut translations = vec![]; + + for key in §ion.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 { + 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>>, earlier: Option>>, later: Option>>, + translations: Vec>, } 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>, subsections: Vec<&'a str>, + translations: Vec>, } 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: §ion.file.relative, @@ -203,6 +248,7 @@ impl<'a> SerializingSection<'a> { lang: §ion.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: §ion.file.relative, ancestors, @@ -235,6 +287,7 @@ impl<'a> SerializingSection<'a> { lang: §ion.lang, pages: vec![], subsections: vec![], + translations, } } } diff --git a/components/library/src/library.rs b/components/library/src/library.rs index 59175883..558958fa 100644 --- a/components/library/src/library.rs +++ b/components/library/src/library.rs @@ -22,18 +22,21 @@ pub struct Library { /// All the sections of the site sections: DenseSlotMap
, /// A mapping path -> key for pages so we can easily get their key - paths_to_pages: HashMap, + pub paths_to_pages: HashMap, /// A mapping path -> key for sections so we can easily get their key pub paths_to_sections: HashMap, + /// 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(§ion.file.filename)) - { - parents.push(*section_key); - } + self.paths_to_sections.get(&path.join(§ion.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 = §ions_translations[§ion.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 = diff --git a/components/library/src/pagination/mod.rs b/components/library/src/pagination/mod.rs index 9566189b..ad02385b 100644 --- a/components/library/src/pagination/mod.rs +++ b/components/library/src/pagination/mod.rs @@ -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()); diff --git a/components/library/src/taxonomies/mod.rs b/components/library/src/taxonomies/mod.rs index 6bba13c2..f2708903 100644 --- a/components/library/src/taxonomies/mod.rs +++ b/components/library/src/taxonomies/mod.rs @@ -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() }]; diff --git a/components/site/src/lib.rs b/components/site/src/lib.rs index 99a0892e..402c04e1 100644 --- a/components/site/src/lib.rs +++ b/components/site/src/lib.rs @@ -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, ¤t_path.join(asset_path.file_name().unwrap()))?; + copy(&asset_path, ¤t_path.join(asset_path.file_name().expect("Couldn't get filename from page asset")))?; } Ok(()) @@ -626,7 +626,7 @@ impl Site { ) -> Result> { 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 §ion.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, ) } diff --git a/components/site/tests/common.rs b/components/site/tests/common.rs index bab5db9b..77d5de88 100644 --- a/components/site/tests/common.rs +++ b/components/site/tests/common.rs @@ -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()) } diff --git a/components/site/tests/site_i18n.rs b/components/site/tests/site_i18n.rs index d4bf5198..e0518c57 100644 --- a/components/site/tests/site_i18n.rs +++ b/components/site/tests/site_i18n.rs @@ -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")); diff --git a/components/templates/src/global_fns/mod.rs b/components/templates/src/global_fns/mod.rs index fb9fb986..24ba3934 100644 --- a/components/templates/src/global_fns/mod.rs +++ b/components/templates/src/global_fns/mod.rs @@ -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] }; diff --git a/docs/content/documentation/templates/pages-sections.md b/docs/content/documentation/templates/pages-sections.md index 88476a23..b311bfa9 100644 --- a/docs/content/documentation/templates/pages-sections.md +++ b/docs/content/documentation/templates/pages-sections.md @@ -52,7 +52,9 @@ ancestors: Array; // 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; ``` ## Section variables @@ -96,7 +98,9 @@ ancestors: Array; // 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; ``` ## Table of contents @@ -116,3 +120,19 @@ permalink: String; // All lower level headers below this header children: Array
; ``` + +## 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; +``` + diff --git a/test_site_i18n/content/blog/_index.fr.md b/test_site_i18n/content/blog/_index.fr.md index b19438ae..2f97f2ff 100644 --- a/test_site_i18n/content/blog/_index.fr.md +++ b/test_site_i18n/content/blog/_index.fr.md @@ -1,4 +1,5 @@ +++ +title = "Mon blog" sort_by = "date" insert_anchors = "right" +++ diff --git a/test_site_i18n/content/blog/_index.it.md b/test_site_i18n/content/blog/_index.it.md index b19438ae..5e008825 100644 --- a/test_site_i18n/content/blog/_index.it.md +++ b/test_site_i18n/content/blog/_index.it.md @@ -1,4 +1,5 @@ +++ +title = "Il mio blog" sort_by = "date" insert_anchors = "right" +++ diff --git a/test_site_i18n/content/blog/_index.md b/test_site_i18n/content/blog/_index.md index 028555a2..a0b42407 100644 --- a/test_site_i18n/content/blog/_index.md +++ b/test_site_i18n/content/blog/_index.md @@ -1,4 +1,5 @@ +++ +title = "My blog" sort_by = "date" insert_anchors = "left" +++ diff --git a/test_site_i18n/content/blog/not-in-frend.md b/test_site_i18n/content/blog/not-translated.md similarity index 100% rename from test_site_i18n/content/blog/not-in-frend.md rename to test_site_i18n/content/blog/not-translated.md diff --git a/test_site_i18n/templates/page.html b/test_site_i18n/templates/page.html index ae69e06e..585468d1 100644 --- a/test_site_i18n/templates/page.html +++ b/test_site_i18n/templates/page.html @@ -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 %} + diff --git a/test_site_i18n/templates/section.html b/test_site_i18n/templates/section.html index e1b47f13..1f5e03c9 100644 --- a/test_site_i18n/templates/section.html +++ b/test_site_i18n/templates/section.html @@ -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 %}