diff --git a/.gitignore b/.gitignore index e5d75c4..114edb0 100644 --- a/.gitignore +++ b/.gitignore @@ -7,13 +7,13 @@ docker-compose.yml /.vscode # Data -data/index.md +data/index*.md data/contacts/* data/cours/* data/projects/* # Blog -data/blog/*.md +data/blog/about*.md data/blog/posts/* !data/blog/posts/Makefile diff --git a/Documentation.md b/Documentation.md index 973b58b..2aeb917 100644 --- a/Documentation.md +++ b/Documentation.md @@ -145,6 +145,8 @@ Markdown file Markdown file is stored in `/app/data/index.md` +> For french clients, `/app/data/index-fr.md` will be read instead. + ``` --- name: Option @@ -188,6 +190,8 @@ Post content The file is stored at `/app/data/blog/about.md`. +> For french clients, `/app/data/blog/about-fr.md` will be read instead. + ## Projects Markdown files are stored in `/app/data/projects/apps/` @@ -214,6 +218,8 @@ files in `archive` subdirectory of `apps`. The file is stored at `/app/data/projects/about.md`. +> For french clients, `/app/data/projects/about-fr.md` will be read instead. + ## Contacts Markdown files are stored in `/app/data/contacts/` @@ -254,6 +260,8 @@ For example, `socials` contact files are stored in `/app/data/contacts/socials/` The file is stored at `/app/data/contacts/about.md`. +> For french clients, `/app/data/contacts/about-fr.md` will be read instead. + ## Courses Markdown files are stored in `/app/data/cours/` diff --git a/src/config.rs b/src/config.rs index d1bec25..5058103 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,7 +7,7 @@ use std::{fs::File, io::Write, path::Path}; use crate::template::Template; /// Store the configuration of config/config.toml -#[derive(Clone, Debug, Default, Deserialize)] +#[derive(Clone, Debug, Default, Deserialize, Hash, PartialEq, Eq)] pub struct FileConfiguration { /// http/https pub scheme: Option, @@ -75,14 +75,14 @@ impl FileConfiguration { } // Paths where files are stored -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct Locations { pub static_dir: String, pub data_dir: String, } /// Configuration used internally in the app -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct Config { /// Information given in the config file pub fc: FileConfiguration, diff --git a/src/routes/blog.rs b/src/routes/blog.rs index 0f26237..6676794 100644 --- a/src/routes/blog.rs +++ b/src/routes/blog.rs @@ -1,5 +1,7 @@ -use actix_web::{get, http::header::ContentType, routes, web, HttpResponse, Responder}; -use cached::proc_macro::once; +use actix_web::{ + get, http::header::ContentType, routes, web, HttpRequest, HttpResponse, Responder, +}; +use cached::proc_macro::cached; use ramhorns::Content; use crate::{ @@ -8,14 +10,17 @@ use crate::{ utils::{ markdown::{File, FilePath}, metadata::MType, - misc::{make_kw, read_file, Html}, + misc::{lang, make_kw, read_file_fallback, Html, Lang}, routes::blog::{build_rss, get_post, get_posts, Post, BLOG_DIR, MIME_TYPE_RSS, POST_DIR}, }, }; #[get("/blog")] -pub async fn index(config: web::Data) -> impl Responder { - Html(build_index(config.get_ref().to_owned())) +pub async fn index(req: HttpRequest, config: web::Data) -> impl Responder { + Html(build_index( + config.get_ref().to_owned(), + lang(req.headers()), + )) } #[derive(Content, Debug)] @@ -26,18 +31,19 @@ struct BlogIndexTemplate { no_posts: bool, } -#[once(time = 60)] -fn build_index(config: Config) -> String { +#[cached(time = 60)] +fn build_index(config: Config, lang: Lang) -> String { let blog_dir = format!("{}/{}", config.locations.data_dir, BLOG_DIR); let mut posts = get_posts(&format!("{blog_dir}/{POST_DIR}")); // Get about - let about: Option = read_file( + let about = read_file_fallback( FilePath { base: blog_dir, path: "about.md".to_owned(), }, MType::Generic, + lang, ); // Sort from newest to oldest diff --git a/src/routes/contact.rs b/src/routes/contact.rs index 56c6dcb..494bf0b 100644 --- a/src/routes/contact.rs +++ b/src/routes/contact.rs @@ -1,5 +1,5 @@ use actix_web::{get, routes, web, HttpRequest, Responder}; -use cached::proc_macro::once; +use cached::proc_macro::cached; use ramhorns::Content; use crate::{ @@ -8,7 +8,7 @@ use crate::{ utils::{ markdown::{File, FilePath}, metadata::MType, - misc::{make_kw, read_file, Html}, + misc::{lang, make_kw, read_file_fallback, Html, Lang}, routes::contact::{find_links, read, remove_paragraphs}, }, }; @@ -28,8 +28,8 @@ pub fn pages(cfg: &mut web::ServiceConfig) { } #[get("")] -async fn page(config: web::Data) -> impl Responder { - Html(build_page(config.get_ref().to_owned())) +async fn page(req: HttpRequest, config: web::Data) -> impl Responder { + Html(build_page(config.get_ref().to_owned(), lang(req.headers()))) } #[routes] @@ -78,18 +78,19 @@ struct NetworksTemplate { others: Vec, } -#[once(time = 60)] -fn build_page(config: Config) -> String { +#[cached(time = 60)] +fn build_page(config: Config, lang: Lang) -> String { let contacts_dir = format!("{}/{}", config.locations.data_dir, CONTACT_DIR); let ext = ".md"; // Get about - let about = read_file( + let about = read_file_fallback( FilePath { base: contacts_dir.clone(), path: "about.md".to_owned(), }, MType::Generic, + lang, ); let mut socials = read(&FilePath { diff --git a/src/routes/cours.rs b/src/routes/cours.rs index fb9bdad..ad3a711 100644 --- a/src/routes/cours.rs +++ b/src/routes/cours.rs @@ -60,13 +60,14 @@ fn get_content( path: filename.to_owned(), }, MType::Cours, + None, ) } fn build_page(info: &web::Query, config: Config) -> String { let cours_dir = "data/cours"; - let (ep, el): (_, Vec) = config + let (ep, el): (_, Vec<_>) = config .fc .exclude_courses .unwrap() diff --git a/src/routes/index.rs b/src/routes/index.rs index 1b5855e..da16348 100644 --- a/src/routes/index.rs +++ b/src/routes/index.rs @@ -1,5 +1,5 @@ -use actix_web::{get, web, Responder}; -use cached::proc_macro::once; +use actix_web::{get, web, HttpRequest, Responder}; +use cached::proc_macro::cached; use ramhorns::Content; use crate::{ @@ -8,13 +8,13 @@ use crate::{ utils::{ markdown::{File, FilePath}, metadata::MType, - misc::{make_kw, read_file, Html}, + misc::{lang, make_kw, read_file, read_file_fallback, Html, Lang}, }, }; #[get("/")] -pub async fn page(config: web::Data) -> impl Responder { - Html(build_page(config.get_ref().to_owned())) +pub async fn page(req: HttpRequest, config: web::Data) -> impl Responder { + Html(build_page(config.get_ref().to_owned(), lang(req.headers()))) } #[derive(Content, Debug)] @@ -34,14 +34,15 @@ struct StyleAvatar { square: bool, } -#[once(time = 60)] -fn build_page(config: Config) -> String { - let mut file = read_file( +#[cached(time = 60)] +fn build_page(config: Config, lang: Lang) -> String { + let mut file = read_file_fallback( FilePath { - base: config.locations.data_dir, + base: config.locations.data_dir.clone(), path: "index.md".to_owned(), }, MType::Index, + lang, ); // Default values @@ -77,6 +78,7 @@ fn build_page(config: Config) -> String { path: "README.md".to_owned(), }, MType::Generic, + None, ); } diff --git a/src/routes/portfolio.rs b/src/routes/portfolio.rs index e0d3b49..9950eb5 100644 --- a/src/routes/portfolio.rs +++ b/src/routes/portfolio.rs @@ -1,5 +1,5 @@ -use actix_web::{get, web, Responder}; -use cached::proc_macro::once; +use actix_web::{get, web, HttpRequest, Responder}; +use cached::proc_macro::cached; use glob::glob; use ramhorns::Content; @@ -9,13 +9,13 @@ use crate::{ utils::{ markdown::{File, FilePath}, metadata::MType, - misc::{make_kw, read_file, Html}, + misc::{lang, make_kw, read_file, read_file_fallback, Html, Lang}, }, }; #[get("/portfolio")] -pub async fn page(config: web::Data) -> impl Responder { - Html(build_page(config.get_ref().to_owned())) +pub async fn page(req: HttpRequest, config: web::Data) -> impl Responder { + Html(build_page(config.get_ref().to_owned(), lang(req.headers()))) } #[derive(Content, Debug)] @@ -29,8 +29,8 @@ struct PortfolioTemplate<'a> { err_msg: &'a str, } -#[once(time = 60)] -fn build_page(config: Config) -> String { +#[cached(time = 60)] +fn build_page(config: Config, lang: Lang) -> String { let projects_dir = format!("{}/projects", config.locations.data_dir); let apps_dir = FilePath { base: format!("{projects_dir}/apps"), @@ -39,12 +39,13 @@ fn build_page(config: Config) -> String { let ext = ".md"; // Get about - let about = read_file( + let about = read_file_fallback( FilePath { base: projects_dir, path: "about.md".to_owned(), }, MType::Generic, + lang, ); // Get apps @@ -54,6 +55,7 @@ fn build_page(config: Config) -> String { read_file( apps_dir.from(&e.unwrap().to_string_lossy()), MType::Portfolio, + None, ) .unwrap() }) @@ -72,6 +74,7 @@ fn build_page(config: Config) -> String { read_file( apps_dir.from(&e.unwrap().to_string_lossy()), MType::Portfolio, + None, ) .unwrap() }) diff --git a/src/template.rs b/src/template.rs index 52a4f4b..205752d 100644 --- a/src/template.rs +++ b/src/template.rs @@ -1,7 +1,7 @@ use ramhorns::{Content, Ramhorns}; /// Structure used in the config variable of the app -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct Template { /// Root directory where templates are stored pub directory: String, diff --git a/src/utils/misc.rs b/src/utils/misc.rs index dacfd8b..e267d7b 100644 --- a/src/utils/misc.rs +++ b/src/utils/misc.rs @@ -1,8 +1,10 @@ use std::{fs, os::unix::fs::MetadataExt, path::Path}; use actix_web::{ - http::header::{self, ContentType, TryIntoHeaderValue}, - http::StatusCode, + http::{ + header::{self, ContentType, HeaderMap, TryIntoHeaderValue}, + StatusCode, + }, HttpRequest, HttpResponse, Responder, }; use base64::{engine::general_purpose, Engine}; @@ -56,15 +58,33 @@ impl Responder for Html { } } +/// Read a file localized, fallback to default file if localized file isn't found +pub fn read_file_fallback(filename: FilePath, expected_file: MType, lang: Lang) -> Option { + read_file(filename.clone(), expected_file, Some(lang)).or(read_file( + filename, + expected_file, + None, + )) +} + /// Read a file -pub fn read_file(filename: FilePath, expected_file: MType) -> Option { - reader(filename, expected_file) +pub fn read_file(filename: FilePath, expected_file: MType, lang: Option) -> Option { + reader(filename, expected_file, lang.unwrap_or(Lang::English)) } #[cached(time = 600)] -fn reader(filename: FilePath, expected_file: MType) -> Option { - let as_str = filename.to_string(); - let path = Path::new(&as_str); +fn reader(filename: FilePath, expected_file: MType, lang: Lang) -> Option { + let as_str = match lang { + Lang::French => { + let str = filename.to_string(); + let mut parts = str.split('.').collect::>(); + let extension = parts.pop().unwrap_or(""); + let filename = parts.join("."); + &format!("{filename}-fr.{extension}") + } + Lang::English => &filename.to_string(), + }; + let path = Path::new(as_str); if let Ok(metadata) = path.metadata() { // Taille maximale : 30M @@ -76,12 +96,12 @@ fn reader(filename: FilePath, expected_file: MType) -> Option { path.extension().and_then(|ext| { match mime_guess::from_ext(ext.to_str().unwrap_or_default()).first_or_text_plain() { mime if mime == mime::APPLICATION_PDF => { - fs::read(&as_str).map_or(None, |bytes| Some(read_pdf(bytes))) + fs::read(as_str).map_or(None, |bytes| Some(read_pdf(bytes))) } mime if mime.type_() == mime::IMAGE => { - fs::read(&as_str).map_or(None, |bytes| Some(read_img(bytes, &mime))) + fs::read(as_str).map_or(None, |bytes| Some(read_img(bytes, &mime))) } - _ => fs::read_to_string(&as_str).map_or(None, |text| { + _ => fs::read_to_string(as_str).map_or(None, |text| { Some(read_md(&filename, &text, expected_file, None, true)) }), } @@ -116,3 +136,23 @@ fn read_img(data: Vec, mime: &mime::Mime) -> File { pub fn remove_first_letter(s: &str) -> &str { s.chars().next().map(|c| &s[c.len_utf8()..]).unwrap() } + +#[derive(Hash, PartialEq, Eq, Clone)] +pub enum Lang { + French, + English, +} + +/// Get the browser language +pub fn lang(headers: &HeaderMap) -> Lang { + headers + .get("Accept-Language") + .and_then(|lang| lang.to_str().ok()) + .and_then(|lang| { + ["fr", "fr-FR"] + .into_iter() + .any(|i| lang.contains(i)) + .then_some(Lang::French) + }) + .unwrap_or(Lang::English) +} diff --git a/src/utils/routes/blog.rs b/src/utils/routes/blog.rs index 56a41df..2c48cee 100644 --- a/src/utils/routes/blog.rs +++ b/src/utils/routes/blog.rs @@ -51,6 +51,7 @@ impl Post { path: format!("{}{ext}", self.url), }, MType::Blog, + None, ) { self.content = Some(file.content); } @@ -149,6 +150,7 @@ pub fn get_post( path: format!("{filename}{ext}"), }, MType::Blog, + None, ); let default = ( diff --git a/src/utils/routes/contact.rs b/src/utils/routes/contact.rs index f8c58bf..c424d29 100644 --- a/src/utils/routes/contact.rs +++ b/src/utils/routes/contact.rs @@ -57,7 +57,14 @@ pub fn remove_paragraphs(list: &mut [File]) { pub fn read(path: &FilePath) -> Vec { glob(&path.to_string()) .unwrap() - .map(|e| read_file(path.from(&e.unwrap().to_string_lossy()), MType::Contact).unwrap()) + .map(|e| { + read_file( + path.from(&e.unwrap().to_string_lossy()), + MType::Contact, + None, + ) + .unwrap() + }) .filter(|f| { !f.metadata .info