From 5bee453f2f4faabcd1ee4269bb2bc52131b48a46 Mon Sep 17 00:00:00 2001 From: Raphael Date: Sat, 26 Apr 2025 20:48:06 +0200 Subject: [PATCH 01/13] feat: added example json files with translations --- translation/de-DE.json | 6 ++++++ translation/en-US.json | 6 ++++++ 2 files changed, 12 insertions(+) create mode 100644 translation/de-DE.json create mode 100644 translation/en-US.json diff --git a/translation/de-DE.json b/translation/de-DE.json new file mode 100644 index 0000000..309c441 --- /dev/null +++ b/translation/de-DE.json @@ -0,0 +1,6 @@ +{ + "greeting": "Guten Tag!", + "farewell": "Auf Wiedersehen!", + "debug.enabled": "Ja", + "debug.level": "Information" +} diff --git a/translation/en-US.json b/translation/en-US.json new file mode 100644 index 0000000..4866b3a --- /dev/null +++ b/translation/en-US.json @@ -0,0 +1,6 @@ +{ + "greeting": "Hello There!", + "farewell": "Goodbye!", + "debug.enabled": "yes", + "debug.level": "information" +} From 7ce025281a4dbfa7d03ced3449192504100344b1 Mon Sep 17 00:00:00 2001 From: Raphael Date: Sat, 26 Apr 2025 20:48:16 +0200 Subject: [PATCH 02/13] feat: added language codes --- src/locale/code.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/locale/code.rs diff --git a/src/locale/code.rs b/src/locale/code.rs new file mode 100644 index 0000000..a84e27e --- /dev/null +++ b/src/locale/code.rs @@ -0,0 +1,23 @@ +use std::ffi::OsString; + +pub enum CountryCode { + Germany, + UnitedStates, +} + +impl ToString for CountryCode { + fn to_string(&self) -> String { + match self { + CountryCode::Germany => "de-DE".to_string(), + CountryCode::UnitedStates => "en-US".to_string(), + } + } +} + +pub fn code_from_file_name(file_name: String, default: CountryCode) -> CountryCode { + match file_name.as_str() { + "de-DE" => CountryCode::Germany, + "en-US" => CountryCode::UnitedStates, + _ => default, + } +} From c033da355a194eddde8e06e1942fdbf097c513de Mon Sep 17 00:00:00 2001 From: Raphael Date: Sat, 26 Apr 2025 20:49:00 +0200 Subject: [PATCH 03/13] feat: started to work on locales --- Cargo.lock | 18 +-------- src/locale/locale.rs | 92 ++++++++++++++++++++++++++++++++++++++++++++ src/locale/mod.rs | 2 + src/main.rs | 5 +++ 4 files changed, 101 insertions(+), 16 deletions(-) create mode 100644 src/locale/locale.rs create mode 100644 src/locale/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 765a559..cfe660e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -525,7 +525,7 @@ dependencies = [ "serde", "serde_json", "tokio", - "tucana 0.0.20", + "tucana", ] [[package]] @@ -2293,7 +2293,7 @@ dependencies = [ "serde", "serde_json", "tokio", - "tucana 0.0.20", + "tucana", ] [[package]] @@ -2544,20 +2544,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "tucana" -version = "0.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6c0791d9505b75b1405db08e0a2fd32d3dff98a4346c2d3ecf17ed55ea36de2" -dependencies = [ - "prost", - "prost-types", - "serde", - "serde_json", - "tonic", - "tonic-build", -] - [[package]] name = "tucana" version = "0.0.20" diff --git a/src/locale/locale.rs b/src/locale/locale.rs new file mode 100644 index 0000000..aa86fa3 --- /dev/null +++ b/src/locale/locale.rs @@ -0,0 +1,92 @@ +use std::{ + collections::HashMap, + fs::{self, read_dir}, +}; + +use tucana::shared::Translation; + +use super::code::{code_from_file_name, CountryCode}; + +pub struct Locale { + pub translations: HashMap>, + pub accepted_locales: Vec, + pub default_locale: CountryCode, + pub path: String, +} + +pub struct TranslationMissingError; + +impl Locale { + /* + Todo. + - find better way then json to save the language json! + + add the following functions: + - get tranlsations for certain key + - reduce the list of translation to only the accepted locales + - add a new function + */ + + pub fn default() -> Self { + let path = "./translation"; + let translations: HashMap> = HashMap::new(); + let mut accepted_locales: Vec = vec![]; + let entries = read_dir(path).expect("msg"); + + for entry in entries { + let entry = entry.expect("msg"); + let meta = entry.metadata().expect("msg"); + + if meta.is_file() { + let file_name = entry.file_name(); + + let mut real_file_name = match file_name.to_str() { + Some(name) => name, + None => continue, + }; + let file_path = format!("{}/{}", path, real_file_name); + let file = fs::File::open(&file_path).expect("file should open read only"); + + real_file_name = match real_file_name.strip_suffix(".json") { + Some(name) => name, + None => continue, + }; + + let code = code_from_file_name( + real_file_name.to_string().clone(), + CountryCode::UnitedStates, + ); + accepted_locales.push(code); + + let json: serde_json::Value = + serde_json::from_reader(file).expect("file should be proper JSON"); + + for entry in json.as_object().expect("msg").into_iter() { + let key = entry.0.to_string(); + let value = entry.1.as_str().expect("msg"); + + let translation = Translation { + code: code.to_string(), + content: value.to_string(), + }; + + let vec = translations.get(&key.to_string()); + + match vec { + Some(trans) => trans.push(translation), + None => { + translations.insert(key.clone(), vec![translation]); + } + } + } + } + } + + Locale { + translations, + accepted_locales, + default_locale: CountryCode::UnitedStates, + path: path.to_string(), + } + } +} diff --git a/src/locale/mod.rs b/src/locale/mod.rs new file mode 100644 index 0000000..3522ae4 --- /dev/null +++ b/src/locale/mod.rs @@ -0,0 +1,2 @@ +pub mod code; +pub mod locale; diff --git a/src/main.rs b/src/main.rs index fdc10d5..5a84ada 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,7 @@ +pub mod locale; + use code0_flow::flow_queue::service::{Message, RabbitmqClient}; +use locale::locale::Locale; use std::sync::Arc; fn handle_message(message: Message) -> Result { @@ -13,6 +16,8 @@ fn handle_message(message: Message) -> Result { #[tokio::main] async fn main() { + let locale = Locale::default(); + let rabbitmq_client = Arc::new(RabbitmqClient::new("amqp://localhost:5672").await); // Receive messages from the send_queue From 433a887a7ecbc1911e036d728232c90f8daae0bd Mon Sep 17 00:00:00 2001 From: Raphael Date: Sun, 27 Apr 2025 11:16:02 +0200 Subject: [PATCH 04/13] ref: switched from JSON to TOML --- src/locale/locale.rs | 17 +++++++++++++++-- translation/de-DE.json | 6 ------ translation/de-DE.toml | 6 ++++++ translation/en-US.json | 6 ------ translation/en-US.toml | 6 ++++++ 5 files changed, 27 insertions(+), 14 deletions(-) delete mode 100644 translation/de-DE.json create mode 100644 translation/de-DE.toml delete mode 100644 translation/en-US.json create mode 100644 translation/en-US.toml diff --git a/src/locale/locale.rs b/src/locale/locale.rs index aa86fa3..9de004f 100644 --- a/src/locale/locale.rs +++ b/src/locale/locale.rs @@ -58,8 +58,21 @@ impl Locale { ); accepted_locales.push(code); - let json: serde_json::Value = - serde_json::from_reader(file).expect("file should be proper JSON"); + pub fn reduce_to_default(&mut self) { + let code = self.default_locale.to_string(); + for entry in self.translations.iter_mut() { + entry + .1 + .into_iter() + .filter_map(|translation| ({ + if translation.code == code { + translation + } + + })) + } + } +} for entry in json.as_object().expect("msg").into_iter() { let key = entry.0.to_string(); diff --git a/translation/de-DE.json b/translation/de-DE.json deleted file mode 100644 index 309c441..0000000 --- a/translation/de-DE.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "greeting": "Guten Tag!", - "farewell": "Auf Wiedersehen!", - "debug.enabled": "Ja", - "debug.level": "Information" -} diff --git a/translation/de-DE.toml b/translation/de-DE.toml new file mode 100644 index 0000000..cb12939 --- /dev/null +++ b/translation/de-DE.toml @@ -0,0 +1,6 @@ +greeting = "Guten Tag!" +farewell = "Auf Wiedersehen!" + +[debug] +enabled = "Ja" +level = "Information" diff --git a/translation/en-US.json b/translation/en-US.json deleted file mode 100644 index 4866b3a..0000000 --- a/translation/en-US.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "greeting": "Hello There!", - "farewell": "Goodbye!", - "debug.enabled": "yes", - "debug.level": "information" -} diff --git a/translation/en-US.toml b/translation/en-US.toml new file mode 100644 index 0000000..e1c2a58 --- /dev/null +++ b/translation/en-US.toml @@ -0,0 +1,6 @@ +greeting = "Hello There!" +farewell = "Goodbye!" + +[debug] +enabled = "yes" +level = "information" From b56de315f7164e1d8cb993ca87f17cc9be668dc0 Mon Sep 17 00:00:00 2001 From: Raphael Date: Sun, 27 Apr 2025 11:16:17 +0200 Subject: [PATCH 05/13] dependencies: added toml --- Cargo.lock | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + 2 files changed, 61 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index cfe660e..1277295 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2156,6 +2156,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2293,6 +2302,7 @@ dependencies = [ "serde", "serde_json", "tokio", + "toml", "tucana", ] @@ -2433,6 +2443,47 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900f6c86a685850b1bc9f6223b20125115ee3f31e01207d81655bbcc0aea9231" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10558ed0bd2a1562e630926a2d1f0b98c827da99fabd3fe20920a59642504485" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28391a4201ba7eb1984cfeb6862c0b3ea2cfe23332298967c749dddc0d6cd976" + [[package]] name = "tonic" version = "0.13.0" @@ -2892,6 +2943,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb8234a863ea0e8cd7284fcdd4f145233eb00fee02bbdd9861aec44e6477bc5" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen-rt" version = "0.39.0" diff --git a/Cargo.toml b/Cargo.toml index 704c31a..303368e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,4 @@ serde = "1.0.219" serde_json = "1.0.140" tokio = { version = "1.44.1", features = ["rt-multi-thread"] } tucana = "0.0.20" +toml = "0.8.0" From 61cb84094646372fc02a7298d2ea84cbb45835c8 Mon Sep 17 00:00:00 2001 From: Raphael Date: Sun, 27 Apr 2025 11:17:43 +0200 Subject: [PATCH 06/13] feat: adjusted locale system to match toml files --- src/locale/code.rs | 3 +- src/locale/locale.rs | 287 +++++++++++++++++++++++++++++++++---------- src/main.rs | 1 - 3 files changed, 222 insertions(+), 69 deletions(-) diff --git a/src/locale/code.rs b/src/locale/code.rs index a84e27e..26e8ae7 100644 --- a/src/locale/code.rs +++ b/src/locale/code.rs @@ -1,5 +1,4 @@ -use std::ffi::OsString; - +#[derive(Clone, PartialEq)] pub enum CountryCode { Germany, UnitedStates, diff --git a/src/locale/locale.rs b/src/locale/locale.rs index 9de004f..26d6f24 100644 --- a/src/locale/locale.rs +++ b/src/locale/locale.rs @@ -1,105 +1,260 @@ use std::{ collections::HashMap, - fs::{self, read_dir}, + fs::{self, read_dir, DirEntry}, }; +use serde::Deserialize; use tucana::shared::Translation; use super::code::{code_from_file_name, CountryCode}; pub struct Locale { - pub translations: HashMap>, - pub accepted_locales: Vec, - pub default_locale: CountryCode, - pub path: String, + translations: HashMap>, + accepted_locales: Vec, + default_locale: CountryCode, } pub struct TranslationMissingError; -impl Locale { - /* - Todo. - - find better way then json to save the language json! - - add the following functions: - - get tranlsations for certain key - - reduce the list of translation to only the accepted locales - - add a new function - */ +#[derive(Deserialize)] +pub struct Translations { + #[serde(flatten)] + pub entries: HashMap, +} +impl Locale { pub fn default() -> Self { let path = "./translation"; - let translations: HashMap> = HashMap::new(); + let mut dictionary: HashMap> = HashMap::new(); let mut accepted_locales: Vec = vec![]; - let entries = read_dir(path).expect("msg"); - for entry in entries { - let entry = entry.expect("msg"); - let meta = entry.metadata().expect("msg"); + let entries = match read_dir(path) { + Ok(entries) => entries, + Err(e) => panic!("Failed to read translation directory: {}", e), + }; - if meta.is_file() { - let file_name = entry.file_name(); + for entry_result in entries { + let entry = match entry_result { + Ok(entry) => entry, + Err(e) => { + eprintln!("Error reading directory entry: {}", e); + continue; + } + }; - let mut real_file_name = match file_name.to_str() { - Some(name) => name, - None => continue, - }; - let file_path = format!("{}/{}", path, real_file_name); - let file = fs::File::open(&file_path).expect("file should open read only"); + if !is_translation_file(&entry) { + continue; + } - real_file_name = match real_file_name.strip_suffix(".json") { - Some(name) => name, - None => continue, - }; + if let Some((file_name, content)) = read_translation_file(path, &entry) { + let code = code_from_file_name(file_name.clone(), CountryCode::UnitedStates); + accepted_locales.push(code.clone()); + + process_translation_file(&content, &file_name, &code, &mut dictionary); + } + } + + Locale { + translations: dictionary, + accepted_locales, + default_locale: CountryCode::UnitedStates, + } + } + + pub fn new( + path: &str, + accepted_locales: Vec, + default_locale: CountryCode, + ) -> Self { + let mut dictionary = HashMap::new(); - let code = code_from_file_name( - real_file_name.to_string().clone(), - CountryCode::UnitedStates, - ); - accepted_locales.push(code); + let entries = match read_dir(path) { + Ok(entries) => entries, + Err(e) => panic!("Failed to read translation directory: {}", e), + }; + + for entry_result in entries { + let entry = match entry_result { + Ok(entry) => entry, + Err(e) => { + eprintln!("Error reading directory entry: {}", e); + continue; + } + }; + + if !is_translation_file(&entry) { + continue; + } + + if let Some((file_name, content)) = read_translation_file(path, &entry) { + let code = code_from_file_name(file_name.clone(), CountryCode::UnitedStates); + if !accepted_locales.contains(&code) { + continue; + } + + process_translation_file(&content, &file_name, &code, &mut dictionary); + } + } + + Locale { + translations: dictionary, + accepted_locales, + default_locale, + } + } pub fn reduce_to_default(&mut self) { let code = self.default_locale.to_string(); - for entry in self.translations.iter_mut() { - entry - .1 - .into_iter() - .filter_map(|translation| ({ - if translation.code == code { - translation - } - - })) + for (_, translations) in self.translations.iter_mut() { + *translations = translations + .iter() + .filter(|translation| translation.code == code) + .cloned() + .collect(); + } + } + + pub fn reduce_to_accepted(&mut self) { + let codes: Vec = self + .accepted_locales + .iter() + .map(|code| code.to_string()) + .collect(); + for (_, translations) in self.translations.iter_mut() { + *translations = translations + .iter() + .filter(|translation| codes.contains(&translation.code.to_string())) + .cloned() + .collect(); + } + } + + pub fn get_translations(&self, key: String) -> Option> { + self.translations.get(&key).cloned() + } + + pub fn get_dictionary(&self) -> HashMap> { + self.translations.clone() + } +} + +// Check if entry is a file that we should process +fn is_translation_file(entry: &DirEntry) -> bool { + match entry.metadata() { + Ok(meta) => meta.is_file(), + Err(_) => false, + } +} + +// Read and parse a translation file +fn read_translation_file(path: &str, entry: &DirEntry) -> Option<(String, String)> { + let file_name = entry.file_name(); + + let file_name_str = match file_name.to_str() { + Some(name) => name, + None => return None, + }; + + let file_path = format!("{}/{}", path, file_name_str); + let content = match fs::read_to_string(&file_path) { + Ok(content) => content, + Err(e) => { + eprintln!("Failed to read file {}: {}", file_path, e); + return None; + } + }; + + let base_name = match file_name_str.strip_suffix(".toml") { + Some(name) => name.to_string(), + None => return None, + }; + + Some((base_name, content)) +} + +// Process the content of a translation file +fn process_translation_file( + content: &str, + file_name: &str, + code: &CountryCode, + dictionary: &mut HashMap>, +) { + match toml::from_str::(content) { + Ok(value) => { + // Process nested TOML by flattening it + let flattened = flatten_toml(&value, ""); + add_translations_to_dictionary(flattened, code, dictionary); + } + Err(err) => { + eprintln!("Warning: Error parsing '{}': {}", file_name, err); } } } - for entry in json.as_object().expect("msg").into_iter() { - let key = entry.0.to_string(); - let value = entry.1.as_str().expect("msg"); +// Add translations to the dictionary +fn add_translations_to_dictionary( + flattened: HashMap, + code: &CountryCode, + dictionary: &mut HashMap>, +) { + for (key, entry) in flattened { + let translation = Translation { + code: code.clone().to_string(), + content: entry, + }; - let translation = Translation { - code: code.to_string(), - content: value.to_string(), - }; + dictionary + .entry(key) + .or_insert_with(Vec::new) + .push(translation); + } +} - let vec = translations.get(&key.to_string()); +// Helper function to flatten nested TOML structures +fn flatten_toml(value: &toml::Value, prefix: &str) -> HashMap { + let mut result = HashMap::new(); - match vec { - Some(trans) => trans.push(translation), - None => { - translations.insert(key.clone(), vec![translation]); - } + match value { + toml::Value::Table(table) => { + for (key, val) in table { + let new_prefix = if prefix.is_empty() { + key.clone() + } else { + format!("{}.{}", prefix, key) + }; + + match val { + toml::Value::Table(_) => { + let nested = flatten_toml(val, &new_prefix); + result.extend(nested); + } + _ => { + extract_value_to_string(val, &new_prefix, &mut result); } } } } - - Locale { - translations, - accepted_locales, - default_locale: CountryCode::UnitedStates, - path: path.to_string(), + _ => { + if !prefix.is_empty() { + extract_value_to_string(value, prefix, &mut result); + } } } + + result +} + +// Extract a TOML value into a string +fn extract_value_to_string(val: &toml::Value, key: &str, result: &mut HashMap) { + if let Some(str_val) = val.as_str() { + result.insert(key.to_string(), str_val.to_string()); + } else if let Some(string_val) = val + .to_string() + .strip_prefix('"') + .and_then(|s| s.strip_suffix('"')) + { + result.insert(key.to_string(), string_val.to_string()); + } else { + result.insert(key.to_string(), val.to_string()); + } } diff --git a/src/main.rs b/src/main.rs index 5a84ada..f220cdf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,7 +17,6 @@ fn handle_message(message: Message) -> Result { #[tokio::main] async fn main() { let locale = Locale::default(); - let rabbitmq_client = Arc::new(RabbitmqClient::new("amqp://localhost:5672").await); // Receive messages from the send_queue From 9d1f647f1e57c8c74d39edc5bb126aebe448f65c Mon Sep 17 00:00:00 2001 From: Raphael Date: Sun, 27 Apr 2025 12:41:38 +0200 Subject: [PATCH 07/13] Update Cargo.lock --- Cargo.lock | 73 ++++++++++++++++++++++-------------------------------- 1 file changed, 29 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 765a559..f2ac09c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -87,9 +87,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.97" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "arc-swap" @@ -476,9 +476,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.18" +version = "1.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c" +checksum = "04da6a0d40b948dfc4fa8f5bbf402b0fc1a64a28dbf7d12ffd683550f2c1b63a" dependencies = [ "shlex", ] @@ -525,7 +525,7 @@ dependencies = [ "serde", "serde_json", "tokio", - "tucana 0.0.20", + "tucana", ] [[package]] @@ -606,15 +606,15 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "der" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", "der_derive", @@ -892,9 +892,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", @@ -933,9 +933,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" +checksum = "75249d144030531f8dee69fe9cea04d3edf809a017ae445e2abdff6629e86633" dependencies = [ "atomic-waker", "bytes", @@ -1332,9 +1332,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.171" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "linux-raw-sys" @@ -1732,9 +1732,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] @@ -1808,13 +1808,12 @@ checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" [[package]] name = "rand" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" dependencies = [ "rand_chacha", "rand_core", - "zerocopy", ] [[package]] @@ -1927,7 +1926,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", @@ -2258,9 +2257,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.100" +version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", @@ -2293,7 +2292,7 @@ dependencies = [ "serde", "serde_json", "tokio", - "tucana 0.0.20", + "tucana", ] [[package]] @@ -2422,9 +2421,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" dependencies = [ "bytes", "futures-core", @@ -2544,20 +2543,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "tucana" -version = "0.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6c0791d9505b75b1405db08e0a2fd32d3dff98a4346c2d3ecf17ed55ea36de2" -dependencies = [ - "prost", - "prost-types", - "serde", - "serde_json", - "tonic", - "tonic-build", -] - [[package]] name = "tucana" version = "0.0.20" @@ -2981,18 +2966,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.24" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.24" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" dependencies = [ "proc-macro2", "quote", From 87e773d6b85af0362c8f71d678d82d5b5bb67f25 Mon Sep 17 00:00:00 2001 From: Raphael Date: Sun, 27 Apr 2025 13:05:18 +0200 Subject: [PATCH 08/13] dependencies: added tempfile as dev dependency --- Cargo.lock | 1 + Cargo.toml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 1277295..fa26fc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2301,6 +2301,7 @@ dependencies = [ "lapin", "serde", "serde_json", + "tempfile", "tokio", "toml", "tucana", diff --git a/Cargo.toml b/Cargo.toml index 303368e..dfd8f7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,6 @@ serde_json = "1.0.140" tokio = { version = "1.44.1", features = ["rt-multi-thread"] } tucana = "0.0.20" toml = "0.8.0" + +[dev-dependencies] +tempfile = "3.19.1" From 4ffe0b94638f23361be2ce4035ca8736261a1fcf Mon Sep 17 00:00:00 2001 From: Raphael Date: Sun, 27 Apr 2025 13:05:26 +0200 Subject: [PATCH 09/13] feat: added tests for locale --- src/locale/code.rs | 5 +- src/locale/locale.rs | 279 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 266 insertions(+), 18 deletions(-) diff --git a/src/locale/code.rs b/src/locale/code.rs index 26e8ae7..9d4cb3f 100644 --- a/src/locale/code.rs +++ b/src/locale/code.rs @@ -1,7 +1,8 @@ -#[derive(Clone, PartialEq)] +#[derive(Clone, PartialEq, Debug)] pub enum CountryCode { Germany, UnitedStates, + France, } impl ToString for CountryCode { @@ -9,6 +10,7 @@ impl ToString for CountryCode { match self { CountryCode::Germany => "de-DE".to_string(), CountryCode::UnitedStates => "en-US".to_string(), + CountryCode::France => "fr-FR".to_string(), } } } @@ -17,6 +19,7 @@ pub fn code_from_file_name(file_name: String, default: CountryCode) -> CountryCo match file_name.as_str() { "de-DE" => CountryCode::Germany, "en-US" => CountryCode::UnitedStates, + "fr-FR" => CountryCode::France, _ => default, } } diff --git a/src/locale/locale.rs b/src/locale/locale.rs index 26d6f24..468ed41 100644 --- a/src/locale/locale.rs +++ b/src/locale/locale.rs @@ -8,6 +8,7 @@ use tucana::shared::Translation; use super::code::{code_from_file_name, CountryCode}; +#[derive(Debug)] pub struct Locale { translations: HashMap>, accepted_locales: Vec, @@ -87,7 +88,7 @@ impl Locale { } if let Some((file_name, content)) = read_translation_file(path, &entry) { - let code = code_from_file_name(file_name.clone(), CountryCode::UnitedStates); + let code = code_from_file_name(file_name.clone(), default_locale.clone()); if !accepted_locales.contains(&code) { continue; } @@ -106,11 +107,7 @@ impl Locale { pub fn reduce_to_default(&mut self) { let code = self.default_locale.to_string(); for (_, translations) in self.translations.iter_mut() { - *translations = translations - .iter() - .filter(|translation| translation.code == code) - .cloned() - .collect(); + translations.retain(|translation| translation.code == code); } } @@ -121,11 +118,7 @@ impl Locale { .map(|code| code.to_string()) .collect(); for (_, translations) in self.translations.iter_mut() { - *translations = translations - .iter() - .filter(|translation| codes.contains(&translation.code.to_string())) - .cloned() - .collect(); + translations.retain(|translation| codes.contains(&translation.code)); } } @@ -155,6 +148,11 @@ fn read_translation_file(path: &str, entry: &DirEntry) -> Option<(String, String None => return None, }; + // Check if it's a .toml file + if !file_name_str.ends_with(".toml") { + return None; + } + let file_path = format!("{}/{}", path, file_name_str); let content = match fs::read_to_string(&file_path) { Ok(content) => content, @@ -164,11 +162,7 @@ fn read_translation_file(path: &str, entry: &DirEntry) -> Option<(String, String } }; - let base_name = match file_name_str.strip_suffix(".toml") { - Some(name) => name.to_string(), - None => return None, - }; - + let base_name = file_name_str.strip_suffix(".toml").unwrap().to_string(); Some((base_name, content)) } @@ -199,7 +193,7 @@ fn add_translations_to_dictionary( ) { for (key, entry) in flattened { let translation = Translation { - code: code.clone().to_string(), + code: code.to_string(), content: entry, }; @@ -258,3 +252,254 @@ fn extract_value_to_string(val: &toml::Value, key: &str, result: &mut HashMap std::io::Result<()> { + let file_path = dir.join(filename); + let mut file = File::create(file_path)?; + file.write_all(content.as_bytes())?; + Ok(()) + } + + #[test] + fn test_flatten_toml() { + let toml_str = r#" + key1 = "value1" + + [section1] + key2 = "value2" + + [section1.subsection] + key3 = "value3" + "#; + + let value: toml::Value = toml::from_str(toml_str).unwrap(); + let flattened = flatten_toml(&value, ""); + + assert_eq!(flattened.get("key1"), Some(&"value1".to_string())); + assert_eq!(flattened.get("section1.key2"), Some(&"value2".to_string())); + assert_eq!( + flattened.get("section1.subsection.key3"), + Some(&"value3".to_string()) + ); + } + + #[test] + fn test_add_translations_to_dictionary() { + let mut flattened = HashMap::new(); + flattened.insert("key1".to_string(), "value1".to_string()); + flattened.insert("key2".to_string(), "value2".to_string()); + + let code = CountryCode::UnitedStates; + let mut dictionary = HashMap::new(); + + add_translations_to_dictionary(flattened, &code, &mut dictionary); + + assert_eq!(dictionary.len(), 2); + assert_eq!(dictionary["key1"][0].content, "value1"); + assert_eq!(dictionary["key1"][0].code, code.to_string()); + assert_eq!(dictionary["key2"][0].content, "value2"); + assert_eq!(dictionary["key2"][0].code, code.to_string()); + } + + #[test] + fn test_reduce_to_default_empty() { + let mut translations = HashMap::new(); + + let key = "greeting".to_string(); + let mut values = Vec::new(); + + values.push(Translation { + code: CountryCode::Germany.to_string(), + content: "Hallo".to_string(), + }); + + values.push(Translation { + code: CountryCode::France.to_string(), + content: "Bonjour".to_string(), + }); + + translations.insert(key.clone(), values); + + let mut locale = Locale { + translations, + accepted_locales: vec![CountryCode::UnitedStates, CountryCode::France], + default_locale: CountryCode::UnitedStates, + }; + + locale.reduce_to_default(); + + let translations = locale.get_translations(key); + assert!(translations.is_some()); + assert_eq!(translations.unwrap().len(), 0); + } + + #[test] + fn test_reduce_to_default() { + let mut translations = HashMap::new(); + + let key = "greeting".to_string(); + let mut values = Vec::new(); + + values.push(Translation { + code: CountryCode::Germany.to_string(), + content: "Hallo".to_string(), + }); + + values.push(Translation { + code: CountryCode::France.to_string(), + content: "Bonjour".to_string(), + }); + + values.push(Translation { + code: CountryCode::UnitedStates.to_string(), + content: "Hello".to_string(), + }); + + translations.insert(key.clone(), values); + + let mut locale = Locale { + translations, + accepted_locales: vec![CountryCode::UnitedStates, CountryCode::France], + default_locale: CountryCode::UnitedStates, + }; + + locale.reduce_to_default(); + + let translations = locale.get_translations(key); + assert!(translations.is_some()); + assert_eq!(translations.unwrap().len(), 1); + } + + #[test] + fn test_reduce_to_accepted() { + let mut translations = HashMap::new(); + + let key = "greeting".to_string(); + let mut values = Vec::new(); + + values.push(Translation { + code: CountryCode::UnitedStates.to_string(), + content: "Hello".to_string(), + }); + + values.push(Translation { + code: CountryCode::France.to_string(), + content: "Bonjour".to_string(), + }); + + values.push(Translation { + code: CountryCode::Germany.to_string(), + content: "Hallo".to_string(), + }); + + translations.insert(key.clone(), values); + + let mut locale = Locale { + translations, + accepted_locales: vec![CountryCode::UnitedStates, CountryCode::France], + default_locale: CountryCode::UnitedStates, + }; + + locale.reduce_to_accepted(); + + let translations = locale.get_translations(key); + assert!(translations.is_some()); + assert_eq!(translations.unwrap().len(), 2); + } + + #[test] + fn test_reduce_to_accepted_empty() { + let mut translations = HashMap::new(); + + let key = "greeting".to_string(); + let mut values = Vec::new(); + + values.push(Translation { + code: CountryCode::UnitedStates.to_string(), + content: "Hello".to_string(), + }); + + values.push(Translation { + code: CountryCode::France.to_string(), + content: "Bonjour".to_string(), + }); + + translations.insert(key.clone(), values); + + let mut locale = Locale { + translations, + accepted_locales: vec![CountryCode::Germany], + default_locale: CountryCode::UnitedStates, + }; + + locale.reduce_to_accepted(); + + let translations = locale.get_translations(key); + assert!(translations.is_some()); + assert_eq!(translations.unwrap().len(), 0); + } + + #[test] + fn test_locale_new_with_files() { + let temp_dir = tempdir().unwrap(); + let temp_path = temp_dir.path(); + + // Create test translation files + let en_file = create_test_translation_file( + temp_path, + "en_US.toml", + r#" + welcome = "Welcome" + goodbye = "Goodbye" + "#, + ); + + assert!(en_file.is_ok()); + + let fr_file = create_test_translation_file( + temp_path, + "fr_FR.toml", + r#" + welcome = "Bienvenue" + goodbye = "Au revoir" + "#, + ); + + assert!(fr_file.is_ok()); + + let locale = Locale::new( + temp_path.to_str().unwrap(), + vec![CountryCode::UnitedStates, CountryCode::France], + CountryCode::UnitedStates, + ); + + assert_eq!(locale.accepted_locales.len(), 2); + assert!(locale.accepted_locales.contains(&CountryCode::UnitedStates)); + assert!(locale.accepted_locales.contains(&CountryCode::France)); + assert_eq!( + locale.default_locale, + CountryCode::UnitedStates, + "Not the same default locale" + ); + + // Test that translations were loaded correctly + let welcome_translations = locale.get_translations("welcome".to_string()).unwrap(); + let goodbye_translations = locale.get_translations("goodbye".to_string()).unwrap(); + let empty_translations = locale.get_translations("empty".to_string()); + assert_eq!(welcome_translations.len(), 2); + assert_eq!(goodbye_translations.len(), 2); + assert!(empty_translations.is_none()); + } +} From 60bb5e5b966cd0ad135068bd8fcf1ab6ecb22821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Raphael=20G=C3=B6tz?= <52959657+raphael-goetz@users.noreply.github.com> Date: Sun, 27 Apr 2025 13:08:17 +0200 Subject: [PATCH 10/13] feat: added test workflow --- .github/workflows/build-and-test.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/workflows/build-and-test.yml diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml new file mode 100644 index 0000000..6585db8 --- /dev/null +++ b/.github/workflows/build-and-test.yml @@ -0,0 +1,19 @@ +name: Build packages + +on: + push: + +jobs: + aquila: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Setup rust + run: rustup update --no-self-update stable + - name: Build Taurus + run: PATH=${{ runner.temp }}/proto/bin:$PATH cargo build + env: + RUST_BACKTRACE: 'full' + - name: Run Tests + run: cargo test \ No newline at end of file From 07d0bea4a927ab3e38d53bf304d70283cad8c916 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Raphael=20G=C3=B6tz?= <52959657+raphael-goetz@users.noreply.github.com> Date: Sun, 27 Apr 2025 13:09:15 +0200 Subject: [PATCH 11/13] ref: renamed aquila to taurus as step --- .github/workflows/build-and-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 6585db8..a5506e0 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -4,7 +4,7 @@ on: push: jobs: - aquila: + taurus: runs-on: ubuntu-latest steps: From f00364b65401fb236cbd0852a0186cb1d824d2c1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 20:40:31 +0000 Subject: [PATCH 12/13] fix(deps): update rust crate toml to v0.8.22 --- Cargo.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d491bec..155512e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2445,9 +2445,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900f6c86a685850b1bc9f6223b20125115ee3f31e01207d81655bbcc0aea9231" +checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" dependencies = [ "serde", "serde_spanned", @@ -2466,9 +2466,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.25" +version = "0.22.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10558ed0bd2a1562e630926a2d1f0b98c827da99fabd3fe20920a59642504485" +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" dependencies = [ "indexmap", "serde", @@ -2480,9 +2480,9 @@ dependencies = [ [[package]] name = "toml_write" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28391a4201ba7eb1984cfeb6862c0b3ea2cfe23332298967c749dddc0d6cd976" +checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" [[package]] name = "tonic" From a0addf2f4740df1f0a031154569def5aff0cad82 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 May 2025 13:02:44 +0000 Subject: [PATCH 13/13] fix(deps): update rust crate tokio to v1.45.0 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 155512e..1ebaa1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2394,9 +2394,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.44.2" +version = "1.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" +checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" dependencies = [ "backtrace", "bytes",