diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 9c1d668..280d975 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,6 +1,15 @@ ## Pre-submission Checklist + + - [ ] I've checked existing issues and pull requests - [ ] I've read the [Code of Conduct](https://github.com/FlakySL/translatable/blob/main/CODE_OF_CONDUCT.md) - [ ] I've [implemented tests](https://github.com/FlakySL/translatable/blob/main/translatable/tests/README.md) for my changes @@ -8,8 +17,28 @@ ## Changes + + - ## Linked Issues + + - fixes # diff --git a/.gitignore b/.gitignore index e594a4d..79e4603 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ .direnv **/*.lcov + +*-lsp-config.toml diff --git a/Cargo.lock b/Cargo.lock index 70b3b6d..9869ff1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,16 +3,28 @@ version = 4 [[package]] -name = "equivalent" -version = "1.0.2" +name = "dyn-clone" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" + +[[package]] +name = "dyn_path" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f15ad1b8978243fb38d0b2b95e9f79a3a7328a3bd0972b2bdd6b4a93d9dfac2c" [[package]] -name = "glob" -version = "0.3.2" +name = "edit-distance" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "e3f497e87b038c09a155dfd169faa5ec940d0644635555ef6bd464ac20e97397" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "hashbrown" @@ -36,12 +48,6 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - [[package]] name = "memchr" version = "2.7.4" @@ -73,50 +79,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" [[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "serde" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.140" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", -] - -[[package]] -name = "serde_spanned" -version = "0.6.8" +name = "same-file" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ - "serde", + "winapi-util", ] [[package]] @@ -143,30 +111,15 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.100" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "target-triple" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac9aa371f599d22256307c24a9d748c041e548cbf599f35d890f9d365361790" - -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - [[package]] name = "thiserror" version = "2.0.12" @@ -187,36 +140,19 @@ dependencies = [ "syn", ] -[[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" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" -dependencies = [ - "serde", -] +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" [[package]] name = "toml_edit" -version = "0.22.26" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", - "serde", - "serde_spanned", "toml_datetime", "toml_write", "winnow", @@ -224,21 +160,13 @@ dependencies = [ [[package]] name = "toml_write" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "translatable" version = "1.0.0" -dependencies = [ - "quote", - "thiserror", - "toml_edit", - "translatable_proc", - "translatable_shared", - "trybuild", -] [[package]] name = "translatable_proc" @@ -246,38 +174,23 @@ version = "1.0.0" dependencies = [ "proc-macro2", "quote", - "strum", "syn", - "thiserror", - "toml_edit", - "translatable_shared", ] [[package]] name = "translatable_shared" version = "1.0.0" dependencies = [ + "dyn-clone", + "dyn_path", + "edit-distance", "proc-macro2", "quote", "strum", "syn", "thiserror", "toml_edit", -] - -[[package]] -name = "trybuild" -version = "1.0.105" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c9bf9513a2f4aeef5fdac8677d7d349c79fdbcc03b9c86da6e9d254f1e43be2" -dependencies = [ - "glob", - "serde", - "serde_derive", - "serde_json", - "target-triple", - "termcolor", - "toml", + "walkdir", ] [[package]] @@ -286,6 +199,16 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "winapi-util" version = "0.1.9" @@ -370,9 +293,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.7.7" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb8234a863ea0e8cd7284fcdd4f145233eb00fee02bbdd9861aec44e6477bc5" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" dependencies = [ "memchr", ] diff --git a/src/translations.rs b/src/translations.rs deleted file mode 100644 index 4ea7a88..0000000 --- a/src/translations.rs +++ /dev/null @@ -1,216 +0,0 @@ -use std::{fs::{read_dir, read_to_string}, io::Error as IoError, sync::OnceLock}; -use proc_macro::TokenStream; -use strum::IntoEnumIterator; -use thiserror::Error; -use toml::{de::Error as TomlError, Table, Value}; -use crate::{config::{load_config, ConfigError, SeekMode, TranslationOverlap}, macros::{TranslationLanguageType, TranslationPathType}, languages::Iso639a}; - -#[derive(Error, Debug)] -pub enum TranslationError { - #[error("{0}")] - Config(#[from] ConfigError), - - #[error("An IO Error occurred: {0:#}")] - Io(#[from] IoError), - - #[error("The path contains invalid unicode characters.")] - InvalidUnicode, - - #[error( - "Toml parse error '{}'{}", - .0.message(), - .0.span() - .map(|l| format!(" in {}:{}:{}", .1, l.start, l.end)) - .unwrap_or("".into()) - )] - ParseToml(TomlError, String), - - #[error( - "'{0}' is not valid ISO 639-1, valid languages including '{0}' are:\n{valid}", - valid = Iso639a::iter() - .filter(|lang| format!("{lang:?}") - .to_lowercase() - .contains(&.0.to_lowercase() - )) - .map(|lang| format!("{} ({lang:#})", format!("{lang:?}").to_lowercase())) - .collect::>() - .join(",\n") - )] -<<<<<<< HEAD - InvalidLangauge(String), - - #[error("{}", " -Translation files can only contain objects, -objects in objects, if an object contains a string, -all it's other branches should also be strings -where it's keys are valid ISO 639-1 languages -in lowercase. - ".trim())] - InvalidTomlFormat -======= - InvalidLanguage(String) ->>>>>>> 4b3c35c (feat: better errors for invalid languages and fix typo) -} - -static TRANSLATIONS: OnceLock> = OnceLock::new(); - -fn walk_dir(path: &str) -> Result, TranslationError> { - let directory = read_dir(path)? - .into_iter() - .collect::, _>>()?; - - let mut result = Vec::new(); - - for path in directory { - let path = path.path(); - - if path.is_dir() { - result.extend(walk_dir( - path - .to_str() - .ok_or(TranslationError::InvalidUnicode)? - )?); - } else { - result.push( - path - .to_string_lossy() - .to_string() - ); - } - } - - Ok(result) -} - -fn translations_valid(table: &Table) -> bool { - let mut contains_translation = false; - let mut contains_table = false; - - for (key, raw) in table { - match raw { - Value::Table(table) => { - // if the current nesting contains a translation - // it can't contain anything else, thus invalid. - if contains_translation { - return false; - } - - // if the value is a table call the function recursively. - // if the nesting it's invalid it invalidates the whole file. - if !translations_valid(table) { - return false; - } - - // since it passes the validation and it's inside the Table match - // it contains a table. - contains_table = true; - }, - - Value::String(translation) => { - // if the current nesting contains a table - // it can't contain anything else, thus invalid. - if contains_table { - return false; - } - - // an object that contains translations - // can't contain an invalid key. - if !Iso639a::is_valid(&key) { - return false; - } - - // a translation can't contain unclosed - // delimiters for '{' and '}', because - // these are used for templating. - let mut open_templates = 0i32; - - for c in translation.chars() { - match c { - '{' => open_templates += 1, - '}' => open_templates -= 1, - _ => {} - } - } - - if open_templates != 0 { - return false; - } - - // if all the checks above pass - // it means the table defintively - // contains a translation. - contains_translation = true; - }, - - // if the table contains anything else than - // a translation (string) or a nested table - // it's automatically invalid. - _ => return false - } - } - - // if nothing returns false it means everything - // is valid. - true -} - -fn load_translations() -> Result<&'static Vec, TranslationError> { - if let Some(translations) = TRANSLATIONS.get() { - return Ok(translations); - } - - let config = load_config()?; - - let mut translation_paths = walk_dir(config.path())?; - translation_paths.sort_by_key(|path| path.to_lowercase()); - - if let SeekMode::Unalphabetical = config.seek_mode() { - translation_paths.reverse(); - } - - let translations = translation_paths - .iter() - .map(|path| Ok( - read_to_string(&path)? - .parse::
() - .map_err(|err| TranslationError::ParseToml(err, path.clone())) - .and_then(|table| translations_valid(&table) - .then_some(table) - .ok_or(TranslationError::InvalidTomlFormat) - )? - )) - .collect::, TranslationError>>()?; - - Ok(TRANSLATIONS.get_or_init(|| translations)) -} - -pub fn load_translation_static(lang: &str, path: &str) -> Result, TranslationError> { - let translations = load_translations()?; - let config = load_config()?; - - if !Iso639a::is_valid(lang) { - return Err(TranslationError::InvalidLanguage(lang.into())) - } - - let mut choosen_translation = None; - for translation in translations { - choosen_translation = path - .split('.') - .fold(Some(translation), |acc, key| acc?.get(key)?.as_table()) - .and_then(|translation| translation.get(lang)) - .map(|translation| translation.to_string()); - - if choosen_translation.is_some() && matches!(config.overlap(), TranslationOverlap::Ignore) { - break; - } - } - - Ok(choosen_translation) -} - -pub fn load_translation_dynamic(lang: TranslationLanguageType, path: TranslationPathType) -> TokenStream { - let lang = lang.dynamic(); - let path = path.dynamic(); - - todo!() -} diff --git a/translatable/Cargo.toml b/translatable/Cargo.toml index 14fcd1b..8f3aead 100644 --- a/translatable/Cargo.toml +++ b/translatable/Cargo.toml @@ -16,11 +16,5 @@ keywords = [ ] [dependencies] -thiserror = "2.0.12" -translatable_proc = { version = "1", path = "../translatable_proc" } -translatable_shared = { version = "1", path = "../translatable_shared/" } [dev-dependencies] -quote = "1.0.40" -toml_edit = "0.22.26" -trybuild = "1.0.105" diff --git a/translatable/src/error.rs b/translatable/src/error.rs deleted file mode 100644 index 3552de3..0000000 --- a/translatable/src/error.rs +++ /dev/null @@ -1,102 +0,0 @@ -//! Runtime error module. -//! -//! This module contains all the runtime -//! errors that could be generated by -//! macro calls or user-facing helper -//! method invocations. - -use thiserror::Error; -use translatable_shared::misc::language::Language; -use translatable_shared::translations::node::TranslationNodeError; - -/// Macro runtime error handling. -/// -/// Used in [`translation`] invocations for non -/// compile-time validations and errors. -/// -/// Use the [`Display`] implementation to obtain the -/// error message, [`Self::cause`] is available as -/// a helper method for such purpose. Read it's -/// documentation before using. -/// -/// [`translation`]: crate::translation -/// [`Display`]: std::fmt::Display -#[derive(Error, Debug)] -pub enum RuntimeError { - /// Translation node error derivations. - /// - /// [`TranslationNode`] construction - /// failure, usually nesting missmatch, invalid - /// template validation... - /// - /// [`Display`] directly forwards the inner - /// error [`Display`] value. - /// - /// The enum implements - /// [`From`] to allow - /// conversion from - /// [`TranslationNodeError`]. - /// - /// **Parameters** - /// * `0` - The [`TranslationNodeError`] derivation. - /// - /// [`TranslationNode`]: crate::shared::translations::node::TranslationNode - /// [`TranslationNodeError`]: crate::shared::translations::node::TranslationNodeError - /// [`Display`]: std::fmt::Display - #[error("{0:#}")] - TranslationNode(#[from] TranslationNodeError), - - /// Dynamic path resolve error. - /// - /// The specified path may not be found - /// in any of the translation files. - /// - /// This is not related to runtime language - /// validity, check [`LanguageNotAvailable`] - /// for that purpose. - /// - /// **Parameters** - /// * `0` - The path that could not be found - /// appended with it's separator. - /// - /// [`LanguageNotAvailable`]: crate::Error::LanguageNotAvailable - #[error("The path '{0}' could not be found")] - PathNotFound(String), - - /// Dynamic language obtention error. - /// - /// This specifically happens when a language - /// is not available for a specific translation. - /// - /// Language parsing is delegated to the user, - /// the language parameter must be a [`Language`], - /// if it's a &[`str`] the validation is made in compile - /// time. In that case we don't reach runtime. - /// - /// **Parameters** - /// * `0` - The language that is not available. - /// * `1` - The path for which the language is not available - /// appended with it's separator. - #[error("The language '{0:?}' ('{0:#}') is not available for the path '{1}'")] - LanguageNotAvailable(Language, String), -} - -impl RuntimeError { - /// Runtime error display helper. - /// - /// This method is marked as `#[cold]` - /// so it should be called lazily with - /// monads such as [`ok_or_else`] or any - /// other `or_else` method. - /// - /// **Returns** - /// A heap allocated [`String`] containing - /// the cause of the error. - /// - /// [`ok_or_else`]: std::option::Option::ok_or_else - #[cold] - #[inline] - pub fn cause(&self) -> String { - format!("{self:#}") - } -} diff --git a/translatable/src/lib.rs b/translatable/src/lib.rs index 76397e7..139597f 100644 --- a/translatable/src/lib.rs +++ b/translatable/src/lib.rs @@ -1,43 +1,2 @@ -//! # Translatable -//! -//! A robust internationalization solution for -//! Rust featuring compile-time validation, -//! ISO 639-1 compliance, and TOML-based -//! translation management. -#![warn(missing_docs)] -mod error; - -/// Runtime error re-export. -/// -/// This `use` statement renames -/// the run time error as a common -/// error by rust practice and exports -/// it. -#[rustfmt::skip] -pub use error::RuntimeError as Error; - -/// Macro re-exports. -/// -/// This `use` statement re-exports -/// all the macros on `translatable_proc` -/// which only work if included from -/// this module due to path generation. -#[rustfmt::skip] -pub use translatable_proc::translation; - -#[rustfmt::skip] -pub use translatable_proc::translation_context; - -/// Language enum re-export. -/// -/// This `use` statement re-exports -/// from the hidden shared re-export -/// for user convenience on parsing. -#[rustfmt::skip] -pub use shared::misc::language::Language; - -#[doc(hidden)] -#[rustfmt::skip] -pub use translatable_shared as shared; diff --git a/translatable/tests/README.md b/translatable/tests/README.md deleted file mode 100644 index 0bc1714..0000000 --- a/translatable/tests/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# Tests - -First of all, thanks for your intention on contributing to this project. - -In this crate we aim for stability and ease of use for all the macros the crate -declares, we want to be helpful not a burden. To accomplish this we need to test -every part of the crate. - -There are two types of test declared in this crate. - -## Integration Testing - -Integration testing helps us test the user experience, what errors should the user -receive on miss-use of a macro whether it's runtime or not. - -The integration tests that pass should be prefixed as `pass_`, otherwise as `fail_`, -the structure for the tests is separated by parameters, so `language/` parameter, -`path/` parameter and `templates/` parameters. Environments is meant to simulate -miss-configuration and the respective errors that should give. - -The tests that pass should also be tested in runtime, so added to the mod file as -modules and annotated conditionally with `#[cfg(test)] #[test]`. - -## Unitary Testing - -Unitary testing is simpler, as it's only functions possessing functions usually from -`translatable::shared`, each module should have its own file and every function -in the module should be tested. - -## Running the tests - -This project uses make for some command recipes. You can run `make test` and it will -test the application with the correct parameters. - -If you are using `cargo test` directly make sure to run the tests with `--test-threds=1`, -there are locks in place so nothing happens, but to make sure you should do that -anyway. diff --git a/translatable/tests/environments/everything_valid/translations/test.toml b/translatable/tests/environments/everything_valid/translations/test.toml deleted file mode 100644 index 6c88b20..0000000 --- a/translatable/tests/environments/everything_valid/translations/test.toml +++ /dev/null @@ -1,16 +0,0 @@ - -# test no templates in string. -[greetings.formal] -es = "Bueno conocerte." -en = "Nice to meet you." - -# test single template in string. -[greetings.informal] -es = "Hey {user}, todo bien?" -en = "What's good {user}?" - -# test multiple templates in same string. -[auditory.actions.delete_user] -es = "{author} ha borrado al usuario {target}." -en = "{author} deleted the user {target}." - diff --git a/translatable/tests/environments/translations_malformed/translations/test.toml b/translatable/tests/environments/translations_malformed/translations/test.toml deleted file mode 100644 index ba40268..0000000 --- a/translatable/tests/environments/translations_malformed/translations/test.toml +++ /dev/null @@ -1,3 +0,0 @@ - -[some.translation] -value = 1 diff --git a/translatable/tests/integration/config/fail_config_invalid_enums.rs b/translatable/tests/integration/config/fail_config_invalid_enums.rs deleted file mode 100644 index 9ba836e..0000000 --- a/translatable/tests/integration/config/fail_config_invalid_enums.rs +++ /dev/null @@ -1,9 +0,0 @@ -// the macro isn't filled because the expected -// failure is on configuration. - -#[allow(unused_imports)] -use translatable::{translation, Language}; - -fn main() { - let _ = translation!(Language::ES, vec![""]); -} diff --git a/translatable/tests/integration/config/fail_config_invalid_enums.stderr b/translatable/tests/integration/config/fail_config_invalid_enums.stderr deleted file mode 100644 index c985428..0000000 --- a/translatable/tests/integration/config/fail_config_invalid_enums.stderr +++ /dev/null @@ -1,7 +0,0 @@ -error: Couldn't parse configuration entry '49854835093459fjkdjfkj' for 'overlap' - --> tests/integration/config/fail_config_invalid_enums.rs - | - | let _ = translation!(Language::ES, vec![""]); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/translatable/tests/integration/config/fail_config_path_missmatch.rs b/translatable/tests/integration/config/fail_config_path_missmatch.rs deleted file mode 100644 index 9ba836e..0000000 --- a/translatable/tests/integration/config/fail_config_path_missmatch.rs +++ /dev/null @@ -1,9 +0,0 @@ -// the macro isn't filled because the expected -// failure is on configuration. - -#[allow(unused_imports)] -use translatable::{translation, Language}; - -fn main() { - let _ = translation!(Language::ES, vec![""]); -} diff --git a/translatable/tests/integration/config/fail_config_path_missmatch.stderr b/translatable/tests/integration/config/fail_config_path_missmatch.stderr deleted file mode 100644 index 9a6e191..0000000 --- a/translatable/tests/integration/config/fail_config_path_missmatch.stderr +++ /dev/null @@ -1,7 +0,0 @@ -error: IO Error: "No such file or directory (os error 2)". Please check the specified path in your configuration file. - --> tests/integration/config/fail_config_path_missmatch.rs - | - | let _ = translation!(Language::ES, vec![""]); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/translatable/tests/integration/config/fail_translations_malformed.rs b/translatable/tests/integration/config/fail_translations_malformed.rs deleted file mode 100644 index 9ba836e..0000000 --- a/translatable/tests/integration/config/fail_translations_malformed.rs +++ /dev/null @@ -1,9 +0,0 @@ -// the macro isn't filled because the expected -// failure is on configuration. - -#[allow(unused_imports)] -use translatable::{translation, Language}; - -fn main() { - let _ = translation!(Language::ES, vec![""]); -} diff --git a/translatable/tests/integration/config/fail_translations_malformed.stderr b/translatable/tests/integration/config/fail_translations_malformed.stderr deleted file mode 100644 index 4da0199..0000000 --- a/translatable/tests/integration/config/fail_translations_malformed.stderr +++ /dev/null @@ -1,7 +0,0 @@ -error: A nesting can only contain translation objects or other nestings - --> tests/integration/config/fail_translations_malformed.rs - | - | let _ = translation!(Language::ES, vec![""]); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/translatable/tests/integration/context/fail_disallowed_type.rs b/translatable/tests/integration/context/fail_disallowed_type.rs deleted file mode 100644 index dc80d25..0000000 --- a/translatable/tests/integration/context/fail_disallowed_type.rs +++ /dev/null @@ -1,12 +0,0 @@ -#[allow(unused_imports)] // trybuild -use ::{std::collections::HashMap, translatable::translation_context}; - -#[translation_context(base_path = greetings)] -struct Context { - formal: i32, - informal: String, -} - -#[allow(unused)] -fn main() {} // trybuild - diff --git a/translatable/tests/integration/context/fail_disallowed_type.stderr b/translatable/tests/integration/context/fail_disallowed_type.stderr deleted file mode 100644 index d0af50b..0000000 --- a/translatable/tests/integration/context/fail_disallowed_type.stderr +++ /dev/null @@ -1,7 +0,0 @@ -error: Only String' and '&str' is allowed for translation contexts - --> tests/integration/context/fail_disallowed_type.rs:4:1 - | -4 | #[translation_context(base_path = greetings)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: this error originates in the attribute macro `translation_context` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/translatable/tests/integration/context/fail_fallback_is_raw.rs b/translatable/tests/integration/context/fail_fallback_is_raw.rs deleted file mode 100644 index 6a422ff..0000000 --- a/translatable/tests/integration/context/fail_fallback_is_raw.rs +++ /dev/null @@ -1,13 +0,0 @@ -use std::collections::HashMap; -use translatable::{translation_context, Language}; - -#[translation_context(base_path = greetings, fallback_language = "en")] -struct Context { - formal: String, - informal: String -} - -fn main() { - let ctx = Context::load_translations(Language::ES, &HashMap::new()); - assert!(ctx.is_ok()); // invalid call -} diff --git a/translatable/tests/integration/context/fail_fallback_is_raw.stderr b/translatable/tests/integration/context/fail_fallback_is_raw.stderr deleted file mode 100644 index 0126b0e..0000000 --- a/translatable/tests/integration/context/fail_fallback_is_raw.stderr +++ /dev/null @@ -1,8 +0,0 @@ -error[E0599]: no method named `is_ok` found for struct `Context` in the current scope - --> tests/integration/context/fail_fallback_is_raw.rs:12:17 - | -4 | #[translation_context(base_path = greetings, fallback_language = "en")] - | ----------------------------------------------------------------------- method `is_ok` not found for this struct -... -12 | assert!(ctx.is_ok()); // invalid call - | ^^^^^ method not found in `Context` diff --git a/translatable/tests/integration/context/fail_invalid_base_path.rs b/translatable/tests/integration/context/fail_invalid_base_path.rs deleted file mode 100644 index e91dfd4..0000000 --- a/translatable/tests/integration/context/fail_invalid_base_path.rs +++ /dev/null @@ -1,9 +0,0 @@ -use translatable::translation_context; - -#[translation_context(base_path = hello)] -struct Context { - formal: String, - informal: String -} - -fn main() {} diff --git a/translatable/tests/integration/context/fail_invalid_base_path.stderr b/translatable/tests/integration/context/fail_invalid_base_path.stderr deleted file mode 100644 index aedd58d..0000000 --- a/translatable/tests/integration/context/fail_invalid_base_path.stderr +++ /dev/null @@ -1,7 +0,0 @@ -error: A translation with the path 'hello::formal' could not be found - --> tests/integration/context/fail_invalid_base_path.rs:3:1 - | -3 | #[translation_context(base_path = hello)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: this error originates in the attribute macro `translation_context` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/translatable/tests/integration/context/fail_invalid_fallback.rs b/translatable/tests/integration/context/fail_invalid_fallback.rs deleted file mode 100644 index 5aa1353..0000000 --- a/translatable/tests/integration/context/fail_invalid_fallback.rs +++ /dev/null @@ -1,9 +0,0 @@ -use translatable::translation_context; - -#[translation_context(base_path = greetings, fallback_language = "invalid")] -struct Context { - formal: String, - informal: String -} - -fn main() {} diff --git a/translatable/tests/integration/context/fail_invalid_fallback.stderr b/translatable/tests/integration/context/fail_invalid_fallback.stderr deleted file mode 100644 index 98180be..0000000 --- a/translatable/tests/integration/context/fail_invalid_fallback.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: Invalid language literal 'invalid' is not a valid ISO-639-1 language - --> tests/integration/context/fail_invalid_fallback.rs:3:66 - | -3 | #[translation_context(base_path = greetings, fallback_language = "invalid")] - | ^^^^^^^^^ diff --git a/translatable/tests/integration/context/fail_no_fallback_is_result.rs b/translatable/tests/integration/context/fail_no_fallback_is_result.rs deleted file mode 100644 index 5abd09f..0000000 --- a/translatable/tests/integration/context/fail_no_fallback_is_result.rs +++ /dev/null @@ -1,13 +0,0 @@ -use std::collections::HashMap; -use translatable::{translation_context, Language}; - -#[translation_context(base_path = greetings)] -struct Context { - formal: String, - informal: String -} - -fn main() { - let ctx = Context::load_translations(Language::ES, &HashMap::new()); - assert!(ctx.formal); // invalid call -} diff --git a/translatable/tests/integration/context/fail_no_fallback_is_result.stderr b/translatable/tests/integration/context/fail_no_fallback_is_result.stderr deleted file mode 100644 index be42190..0000000 --- a/translatable/tests/integration/context/fail_no_fallback_is_result.stderr +++ /dev/null @@ -1,10 +0,0 @@ -error[E0609]: no field `formal` on type `Result` - --> tests/integration/context/fail_no_fallback_is_result.rs:12:17 - | -12 | assert!(ctx.formal); // invalid call - | ^^^^^^ unknown field - | -help: one of the expressions' fields has a field of the same name - | -12 | assert!(ctx.unwrap().formal); // invalid call - | +++++++++ diff --git a/translatable/tests/integration/context/mod.rs b/translatable/tests/integration/context/mod.rs deleted file mode 100644 index acef2bf..0000000 --- a/translatable/tests/integration/context/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod pass_fallback_catch; -pub mod pass_invalid_runtime_language; -pub mod pass_without_params; diff --git a/translatable/tests/integration/context/pass_fallback_catch.rs b/translatable/tests/integration/context/pass_fallback_catch.rs deleted file mode 100644 index a220a1a..0000000 --- a/translatable/tests/integration/context/pass_fallback_catch.rs +++ /dev/null @@ -1,22 +0,0 @@ -#[allow(unused_imports)] // trybuild -use ::{std::collections::HashMap, translatable::translation_context}; - -#[translation_context(base_path = greetings, fallback_language = "en")] -struct Context { - formal: String, - informal: String, -} - -#[test] -fn pass_fallback_catch() { - let translations = - Context::load_translations(translatable::Language::AA, &HashMap::from([ - ("user", "John") - ])); - - assert_eq!(translations.formal, "Nice to meet you."); - assert_eq!(translations.informal, "What's good John?"); -} - -#[allow(unused)] -fn main() {} // trybuild diff --git a/translatable/tests/integration/context/pass_invalid_runtime_language.rs b/translatable/tests/integration/context/pass_invalid_runtime_language.rs deleted file mode 100644 index 85acd33..0000000 --- a/translatable/tests/integration/context/pass_invalid_runtime_language.rs +++ /dev/null @@ -1,21 +0,0 @@ -#![allow(dead_code)] - -#[allow(unused_imports)] // trybuild -use ::{std::collections::HashMap, translatable::translation_context}; - -#[translation_context(base_path = greetings)] -struct Context { - formal: String, - informal: String, -} - -#[test] -fn pass_invalid_runtime_language() { - let translations = - Context::load_translations(translatable::Language::AA, &HashMap::::new()); - - assert!(translations.is_err()); -} - -#[allow(unused)] -fn main() {} // trybuild diff --git a/translatable/tests/integration/context/pass_without_params.rs b/translatable/tests/integration/context/pass_without_params.rs deleted file mode 100644 index cbf2f50..0000000 --- a/translatable/tests/integration/context/pass_without_params.rs +++ /dev/null @@ -1,18 +0,0 @@ -#[allow(unused_imports)] // trybuild -use translatable::translation_context; - -#[translation_context] -struct Context { - #[path(greetings::formal)] - formal: String, - #[path(greetings::informal)] - informal: String, -} - -#[test] -fn pass_without_params() { - -} - -#[allow(unused)] -fn main() {} // trybuild diff --git a/translatable/tests/integration/translation/language/fail_static_invalid.rs b/translatable/tests/integration/translation/language/fail_static_invalid.rs deleted file mode 100644 index 4bd4c76..0000000 --- a/translatable/tests/integration/translation/language/fail_static_invalid.rs +++ /dev/null @@ -1,6 +0,0 @@ -#[allow(unused_imports)] -use translatable::translation; - -fn main() { - translation!("xx", static greetings::formal); -} diff --git a/translatable/tests/integration/translation/language/fail_static_invalid.stderr b/translatable/tests/integration/translation/language/fail_static_invalid.stderr deleted file mode 100644 index 9f5a77f..0000000 --- a/translatable/tests/integration/translation/language/fail_static_invalid.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: The literal 'xx' is an invalid ISO 639-1 string, and cannot be parsed - --> tests/integration/translation/language/fail_static_invalid.rs:5:18 - | -5 | translation!("xx", static greetings::formal); - | ^^^^ diff --git a/translatable/tests/integration/translation/language/mod.rs b/translatable/tests/integration/translation/language/mod.rs deleted file mode 100644 index 0676883..0000000 --- a/translatable/tests/integration/translation/language/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod pass_dynamic_enum; -pub mod pass_dynamic_expr; -pub mod pass_dynamic_invalid_runtime; -pub mod pass_static_lowercase; -pub mod pass_static_uppercase; diff --git a/translatable/tests/integration/translation/language/pass_dynamic_enum.rs b/translatable/tests/integration/translation/language/pass_dynamic_enum.rs deleted file mode 100644 index 537165d..0000000 --- a/translatable/tests/integration/translation/language/pass_dynamic_enum.rs +++ /dev/null @@ -1,14 +0,0 @@ -#[allow(unused_imports)] // trybuild -use translatable::{Language, translation}; - -#[cfg(test)] -#[test] -pub fn pass_dynamic_enum() { - let translation = translation!(Language::ES, static greetings::formal) - .expect("Expected translation generation to be OK"); - - assert_eq!(translation, "Bueno conocerte."); -} - -#[allow(dead_code)] -fn main() {} // trybuild diff --git a/translatable/tests/integration/translation/language/pass_dynamic_expr.rs b/translatable/tests/integration/translation/language/pass_dynamic_expr.rs deleted file mode 100644 index d5afe75..0000000 --- a/translatable/tests/integration/translation/language/pass_dynamic_expr.rs +++ /dev/null @@ -1,17 +0,0 @@ -#[allow(unused_imports)] // trybuild -use translatable::translation; - -#[cfg(test)] -#[test] -pub fn pass_dynamic_expr() { - let translation = translation!( - "es".parse().expect("Expected language parsing to be OK"), - static greetings::formal - ) - .expect("Expected translation generation to be OK"); - - assert_eq!(translation, "Bueno conocerte."); -} - -#[allow(dead_code)] -fn main() {} // trybuild diff --git a/translatable/tests/integration/translation/language/pass_dynamic_invalid_runtime.rs b/translatable/tests/integration/translation/language/pass_dynamic_invalid_runtime.rs deleted file mode 100644 index 8e8dd02..0000000 --- a/translatable/tests/integration/translation/language/pass_dynamic_invalid_runtime.rs +++ /dev/null @@ -1,13 +0,0 @@ -#[allow(unused_imports)] // trybuild -use translatable::{Language, translation}; - -#[cfg(test)] -#[test] -pub fn pass_dynamic_invalid_runtime() { - let language = "invalid".parse::(); - - assert!(language.is_err()); -} - -#[allow(dead_code)] -fn main() {} // trybuild diff --git a/translatable/tests/integration/translation/language/pass_static_lowercase.rs b/translatable/tests/integration/translation/language/pass_static_lowercase.rs deleted file mode 100644 index c18a052..0000000 --- a/translatable/tests/integration/translation/language/pass_static_lowercase.rs +++ /dev/null @@ -1,13 +0,0 @@ -#[allow(unused_imports)] // trybuild -use translatable::translation; - -#[cfg(test)] -#[test] -pub fn pass_static_lowercase() { - let translation = translation!("es", static greetings::formal); - - assert_eq!(translation, "Bueno conocerte."); -} - -#[allow(dead_code)] -fn main() {} // trybuild diff --git a/translatable/tests/integration/translation/language/pass_static_uppercase.rs b/translatable/tests/integration/translation/language/pass_static_uppercase.rs deleted file mode 100644 index 710e048..0000000 --- a/translatable/tests/integration/translation/language/pass_static_uppercase.rs +++ /dev/null @@ -1,13 +0,0 @@ -#[allow(unused_imports)] // trybuild -use translatable::translation; - -#[cfg(test)] -#[test] -pub fn pass_static_uppercase() { - let translation = translation!("ES", static greetings::formal); - - assert_eq!(translation, "Bueno conocerte."); -} - -#[allow(dead_code)] -fn main() {} // trybuild diff --git a/translatable/tests/integration/translation/mod.rs b/translatable/tests/integration/translation/mod.rs deleted file mode 100644 index b600327..0000000 --- a/translatable/tests/integration/translation/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod language; -pub mod path; -pub mod templates; diff --git a/translatable/tests/integration/translation/path/fail_generic_arguments.rs b/translatable/tests/integration/translation/path/fail_generic_arguments.rs deleted file mode 100644 index 4562b19..0000000 --- a/translatable/tests/integration/translation/path/fail_generic_arguments.rs +++ /dev/null @@ -1,6 +0,0 @@ -#[allow(unused_imports)] -use translatable::translation; - -fn main() { - translation!("es", static greetings::formal); -} diff --git a/translatable/tests/integration/translation/path/fail_generic_arguments.stderr b/translatable/tests/integration/translation/path/fail_generic_arguments.stderr deleted file mode 100644 index 7a83806..0000000 --- a/translatable/tests/integration/translation/path/fail_generic_arguments.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: A translation path can't contain generic arguments. - --> tests/integration/translation/path/fail_generic_arguments.rs:5:48 - | -5 | translation!("es", static greetings::formal); - | ^^^^^^^^ diff --git a/translatable/tests/integration/translation/path/fail_static_nonexistent.rs b/translatable/tests/integration/translation/path/fail_static_nonexistent.rs deleted file mode 100644 index 51a6865..0000000 --- a/translatable/tests/integration/translation/path/fail_static_nonexistent.rs +++ /dev/null @@ -1,6 +0,0 @@ -#[allow(unused_imports)] -use translatable::translation; - -fn main() { - translation!("es", static non::existing::path); -} diff --git a/translatable/tests/integration/translation/path/fail_static_nonexistent.stderr b/translatable/tests/integration/translation/path/fail_static_nonexistent.stderr deleted file mode 100644 index 128ba6f..0000000 --- a/translatable/tests/integration/translation/path/fail_static_nonexistent.stderr +++ /dev/null @@ -1,7 +0,0 @@ -error: The path 'non::existing::path' could not be found - --> tests/integration/translation/path/fail_static_nonexistent.rs:5:5 - | -5 | translation!("es", static non::existing::path); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/translatable/tests/integration/translation/path/mod.rs b/translatable/tests/integration/translation/path/mod.rs deleted file mode 100644 index c4c603d..0000000 --- a/translatable/tests/integration/translation/path/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod pass_dynamic_expr; -pub mod pass_static_existing; diff --git a/translatable/tests/integration/translation/path/pass_dynamic_expr.rs b/translatable/tests/integration/translation/path/pass_dynamic_expr.rs deleted file mode 100644 index ada7c14..0000000 --- a/translatable/tests/integration/translation/path/pass_dynamic_expr.rs +++ /dev/null @@ -1,19 +0,0 @@ -#[allow(unused_imports)] // trybuild -use translatable::translation; - -#[cfg(test)] -#[test] -pub fn pass_dynamic_expr() { - let translation = translation!( - "es", - "greetings.formal" - .split(".") - .collect() - ) - .expect("Expected translation generation to be OK"); - - assert_eq!(translation, "Bueno conocerte."); -} - -#[allow(dead_code)] -fn main() {} // trybuild diff --git a/translatable/tests/integration/translation/path/pass_static_existing.rs b/translatable/tests/integration/translation/path/pass_static_existing.rs deleted file mode 100644 index d37c4ab..0000000 --- a/translatable/tests/integration/translation/path/pass_static_existing.rs +++ /dev/null @@ -1,13 +0,0 @@ -#[allow(unused_imports)] // trybuild -use translatable::translation; - -#[cfg(test)] -#[test] -pub fn pass_static_existing() { - let translation = translation!("es", static greetings::formal); - - assert_eq!(translation, "Bueno conocerte."); -} - -#[allow(dead_code)] -fn main() {} // trybuild diff --git a/translatable/tests/integration/translation/templates/fail_invalid_ident.rs b/translatable/tests/integration/translation/templates/fail_invalid_ident.rs deleted file mode 100644 index 9542925..0000000 --- a/translatable/tests/integration/translation/templates/fail_invalid_ident.rs +++ /dev/null @@ -1,6 +0,0 @@ -#[allow(unused_imports)] -use translatable::translation; - -fn main() { - translation!("es", static greetings::informal, %%$invalid = $ident); -} diff --git a/translatable/tests/integration/translation/templates/fail_invalid_ident.stderr b/translatable/tests/integration/translation/templates/fail_invalid_ident.stderr deleted file mode 100644 index dd964ea..0000000 --- a/translatable/tests/integration/translation/templates/fail_invalid_ident.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: expected identifier - --> tests/integration/translation/templates/fail_invalid_ident.rs:5:52 - | -5 | translation!("es", static greetings::informal, %%$invalid = $ident); - | ^ diff --git a/translatable/tests/integration/translation/templates/fail_not_display.rs b/translatable/tests/integration/translation/templates/fail_not_display.rs deleted file mode 100644 index d089999..0000000 --- a/translatable/tests/integration/translation/templates/fail_not_display.rs +++ /dev/null @@ -1,8 +0,0 @@ -#[allow(unused_imports)] -use translatable::translation; - -struct NotDisplay; - -fn main() { - translation!("es", static greetings::informal, user = NotDisplay); -} diff --git a/translatable/tests/integration/translation/templates/fail_not_display.stderr b/translatable/tests/integration/translation/templates/fail_not_display.stderr deleted file mode 100644 index 0a78c8d..0000000 --- a/translatable/tests/integration/translation/templates/fail_not_display.stderr +++ /dev/null @@ -1,22 +0,0 @@ -error[E0599]: `NotDisplay` doesn't implement `std::fmt::Display` - --> tests/integration/translation/templates/fail_not_display.rs:7:5 - | -4 | struct NotDisplay; - | ----------------- method `to_string` not found for this struct because it doesn't satisfy `NotDisplay: ToString` or `NotDisplay: std::fmt::Display` -... -7 | translation!("es", static greetings::informal, user = NotDisplay); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `NotDisplay` cannot be formatted with the default formatter - | - = note: the following trait bounds were not satisfied: - `NotDisplay: std::fmt::Display` - which is required by `NotDisplay: ToString` - = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead -note: the trait `std::fmt::Display` must be implemented - --> $RUST/core/src/fmt/mod.rs - | - | pub trait Display { - | ^^^^^^^^^^^^^^^^^ - = help: items from traits can only be used if the trait is implemented and in scope - = note: the following trait defines an item `to_string`, perhaps you need to implement it: - candidate #1: `ToString` - = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/translatable/tests/integration/translation/templates/fail_value_not_found.rs b/translatable/tests/integration/translation/templates/fail_value_not_found.rs deleted file mode 100644 index 2f665db..0000000 --- a/translatable/tests/integration/translation/templates/fail_value_not_found.rs +++ /dev/null @@ -1,8 +0,0 @@ -#[allow(unused_imports)] -use translatable::translation; - -struct NotDisplay; - -fn main() { - translation!("es", static greetings::informal, user); -} diff --git a/translatable/tests/integration/translation/templates/fail_value_not_found.stderr b/translatable/tests/integration/translation/templates/fail_value_not_found.stderr deleted file mode 100644 index 1ed020e..0000000 --- a/translatable/tests/integration/translation/templates/fail_value_not_found.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error[E0425]: cannot find value `user` in this scope - --> tests/integration/translation/templates/fail_value_not_found.rs:7:52 - | -7 | translation!("es", static greetings::informal, user); - | ^^^^ not found in this scope diff --git a/translatable/tests/integration/translation/templates/mod.rs b/translatable/tests/integration/translation/templates/mod.rs deleted file mode 100644 index 306f03a..0000000 --- a/translatable/tests/integration/translation/templates/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod pass_ident_ref; -pub mod pass_multiple_templates; -pub mod pass_trailing_comma; -pub mod pass_trailing_comma_no_args; diff --git a/translatable/tests/integration/translation/templates/pass_ident_ref.rs b/translatable/tests/integration/translation/templates/pass_ident_ref.rs deleted file mode 100644 index 34fdf84..0000000 --- a/translatable/tests/integration/translation/templates/pass_ident_ref.rs +++ /dev/null @@ -1,15 +0,0 @@ -#[allow(unused_imports)] // trybuild -use translatable::translation; - -#[cfg(test)] -#[test] -pub fn pass_dynamic_expr() { - let user = "Juan"; - - let translation = translation!("es", static greetings::informal, user); - - assert_eq!(translation, "Hey Juan, todo bien?"); -} - -#[allow(dead_code)] -fn main() {} // trybuild diff --git a/translatable/tests/integration/translation/templates/pass_multiple_templates.rs b/translatable/tests/integration/translation/templates/pass_multiple_templates.rs deleted file mode 100644 index c59b18f..0000000 --- a/translatable/tests/integration/translation/templates/pass_multiple_templates.rs +++ /dev/null @@ -1,16 +0,0 @@ -#[allow(unused_imports)] // trybuild -use translatable::translation; - -#[cfg(test)] -#[test] -pub fn pass_dynamic_expr() { - let author = "Juan"; - let target = "Pepito"; - - let translation = translation!("es", static auditory::actions::delete_user, author, target); - - assert_eq!(translation, "Juan ha borrado al usuario Pepito."); -} - -#[allow(dead_code)] -fn main() {} // trybuild diff --git a/translatable/tests/integration/translation/templates/pass_trailing_comma.rs b/translatable/tests/integration/translation/templates/pass_trailing_comma.rs deleted file mode 100644 index c153c0f..0000000 --- a/translatable/tests/integration/translation/templates/pass_trailing_comma.rs +++ /dev/null @@ -1,16 +0,0 @@ -#[allow(unused_imports)] // trybuild -use translatable::translation; - -#[cfg(test)] -#[test] -pub fn pass_dynamic_expr() { - let author = "Juan"; - let target = "Pepito"; - - let translation = translation!("es", static auditory::actions::delete_user, author, target,); - - assert_eq!(translation, "Juan ha borrado al usuario Pepito."); -} - -#[allow(dead_code)] -fn main() {} // trybuild diff --git a/translatable/tests/integration/translation/templates/pass_trailing_comma_no_args.rs b/translatable/tests/integration/translation/templates/pass_trailing_comma_no_args.rs deleted file mode 100644 index 849de90..0000000 --- a/translatable/tests/integration/translation/templates/pass_trailing_comma_no_args.rs +++ /dev/null @@ -1,13 +0,0 @@ -#[allow(unused_imports)] // trybuild -use translatable::translation; - -#[cfg(test)] -#[test] -pub fn pass_static_existing() { - let translation = translation!("es", static greetings::formal,); - - assert_eq!(translation, "Bueno conocerte."); -} - -#[allow(dead_code)] -fn main() {} // trybuild diff --git a/translatable/tests/integration_tests.rs b/translatable/tests/integration_tests.rs deleted file mode 100644 index 7b0fc89..0000000 --- a/translatable/tests/integration_tests.rs +++ /dev/null @@ -1,106 +0,0 @@ -use std::env::{remove_var, set_var}; -use std::fs::canonicalize; -use std::sync::Mutex; - -use trybuild::TestCases; - -mod integration; - -const PATH_ENV: &str = "TRANSLATABLE_LOCALES_PATH"; -const OVERLAP_ENV: &str = "TRANSLATABLE_OVERLAP"; - -static ENV_MUTEX: Mutex<()> = Mutex::new(()); - -macro_rules! lock_env { - () => { - let _env_guard = ENV_MUTEX.lock(); - }; -} - -#[inline] -unsafe fn set_default_env() { - unsafe { - set_locales_env("everything_valid"); - remove_var(OVERLAP_ENV); - } -} - -#[inline] -unsafe fn set_locales_env(env: &str) { - unsafe { - set_var( - PATH_ENV, - canonicalize(format!("./tests/environments/{env}/translations/")).unwrap(), - ); - } -} - -#[test] -fn valid_environment() { - unsafe { - let t = TestCases::new(); - - lock_env!(); - - set_default_env(); - set_locales_env("everything_valid"); - - t.pass("./tests/integration/translation/language/pass*.rs"); - t.compile_fail("./tests/integration/translation/language/fail*.rs"); - - t.pass("./tests/integration/translation/path/pass*.rs"); - t.compile_fail("./tests/integration/translation/path/fail*.rs"); - - t.pass("./tests/integration/translation/templates/pass*.rs"); - t.compile_fail("./tests/integration/translation/templates/fail*.rs"); - - t.pass("./tests/integration/context/pass*.rs"); - t.compile_fail("./tests/integration/context/fail*.rs"); - } -} - -#[test] -fn invalid_tests_path() { - unsafe { - let t = TestCases::new(); - - lock_env!(); - - set_default_env(); - set_var(PATH_ENV, "something_invalid"); - - // invalid path in configuration. - t.compile_fail("./tests/integration/config/fail_config_path_missmatch.rs"); - } -} - -#[test] -fn invalid_config_value() { - unsafe { - let t = TestCases::new(); - - lock_env!(); - - set_default_env(); - set_locales_env("everything_valid"); - set_var(OVERLAP_ENV, "49854835093459fjkdjfkj"); - - // invalid enum value in configuration. - t.compile_fail("./tests/integration/config/fail_config_invalid_enums.rs"); - } -} - -#[test] -fn translations_malformed() { - unsafe { - let t = TestCases::new(); - - lock_env!(); - - set_default_env(); - set_locales_env("translations_malformed"); - - // translation file rule broken. - t.compile_fail("./tests/integration/config/fail_translations_malformed.rs"); - } -} diff --git a/translatable/tests/test.rs b/translatable/tests/test.rs deleted file mode 100644 index 60e7785..0000000 --- a/translatable/tests/test.rs +++ /dev/null @@ -1,33 +0,0 @@ -use translatable::{Language, translation}; - -const NAME: &str = "John"; -const SURNAME: &str = "Doe"; -const RESULT: &str = "¡Hola John Doe! Mi nombre es John Doe {{hola}}"; - -#[test] -fn both_static() { - let result = translation!("es", static common::greeting, name = NAME, surname = SURNAME); - assert!(result == RESULT); -} - -#[test] -fn language_static_path_dynamic() { - let result = translation!("es", vec!["common", "greeting"], name = NAME, surname = SURNAME); - assert!(result.unwrap() == RESULT); -} - -#[test] -fn language_dynamic_path_static() { - let name = NAME; - let surname = SURNAME; - - let result = translation!(Language::ES, static common::greeting, name, surname); - assert!(result.unwrap() == RESULT); -} - -#[test] -fn both_dynamic() { - let result = - translation!(Language::ES, vec!["common", "greeting"], name = NAME, surname = SURNAME); - assert!(result.unwrap() == RESULT) -} diff --git a/translatable/tests/unitary/collection_generation.rs b/translatable/tests/unitary/collection_generation.rs deleted file mode 100644 index 40b2f83..0000000 --- a/translatable/tests/unitary/collection_generation.rs +++ /dev/null @@ -1,38 +0,0 @@ -use std::collections::HashMap; - -use quote::quote; -use translatable_shared::macros::collections::{map_to_tokens, map_transform_to_tokens}; - -#[test] -pub fn map_to_tokens_has_literals() { - let tokens = map_to_tokens(&{ - let mut map = HashMap::new(); - map.insert("key1", 1); - map.insert("key2", 2); - - map - }) - .to_string(); - - assert!(tokens.contains("\"key1\"")); - assert!(tokens.contains("1")); - assert!(tokens.contains("\"key2\"")); - assert!(tokens.contains("2")); -} - -#[test] -pub fn map_transform_to_tokens_has_literals() { - let tokens = map_transform_to_tokens( - &{ - let mut map = HashMap::new(); - map.insert("key1", 1i32); - - map - }, - |key, value| quote! { (#key, #value.to_string()) }, - ) - .to_string() - .replace(" ", ""); // normalize - - assert!(tokens.contains("vec![(\"key1\",1i32.to_string())]")); -} diff --git a/translatable/tests/unitary/display_to_error_tokens.rs b/translatable/tests/unitary/display_to_error_tokens.rs deleted file mode 100644 index a6f4000..0000000 --- a/translatable/tests/unitary/display_to_error_tokens.rs +++ /dev/null @@ -1,15 +0,0 @@ -use translatable_shared::macros::errors::IntoCompileError; - -#[test] -pub fn display_to_error_tokens() { - let display = "test".to_string(); - - let to_out_compile_error = display - .to_out_compile_error() - .to_string() - .replace(" ", ""); // normalize - - assert!(to_out_compile_error.contains("fn")); - assert!(to_out_compile_error.contains("__()")); - assert!(to_out_compile_error.contains("std::compile_error!")); -} diff --git a/translatable/tests/unitary/language_enum.rs b/translatable/tests/unitary/language_enum.rs deleted file mode 100644 index ffd40bd..0000000 --- a/translatable/tests/unitary/language_enum.rs +++ /dev/null @@ -1,26 +0,0 @@ -use quote::ToTokens; -use translatable::Language; - -#[test] -pub fn language_enum_parsing_case_insensitive() { - let language_lower = "es".parse::(); - let language_upper = "ES".parse::(); - - assert!(language_lower.is_ok()); - assert!(language_upper.is_ok()); -} - -#[test] -pub fn language_enum_to_tokens() { - let language_tokens = Language::ES - .into_token_stream() - .to_string() - .replace(" ", ""); // normalize the path. - - assert!(language_tokens.contains("translatable::shared::misc::language::Language::ES")); -} - -#[test] -pub fn display_matches() { - assert_eq!(Language::ES.to_string(), "Spanish"); -} diff --git a/translatable/tests/unitary/mod.rs b/translatable/tests/unitary/mod.rs deleted file mode 100644 index 336c0f5..0000000 --- a/translatable/tests/unitary/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod collection_generation; -pub mod language_enum; -pub mod runtime_error; -pub mod templating; -pub mod translation_collection; -pub mod display_to_error_tokens; diff --git a/translatable/tests/unitary/runtime_error.rs b/translatable/tests/unitary/runtime_error.rs deleted file mode 100644 index 7cab73a..0000000 --- a/translatable/tests/unitary/runtime_error.rs +++ /dev/null @@ -1,14 +0,0 @@ -use translatable::{Error, Language}; - -#[test] -pub fn runtime_error_outputs() { - assert_eq!( - Error::PathNotFound("path::to::translation".into()).cause(), - "The path 'path::to::translation' could not be found" - ); - - assert_eq!( - Error::LanguageNotAvailable(Language::ES, "path::to::translation".into()).cause(), - "The language 'ES' ('Spanish') is not available for the path 'path::to::translation'" - ) -} diff --git a/translatable/tests/unitary/templating.rs b/translatable/tests/unitary/templating.rs deleted file mode 100644 index b633ce8..0000000 --- a/translatable/tests/unitary/templating.rs +++ /dev/null @@ -1,67 +0,0 @@ -use std::collections::HashMap; -use std::str::FromStr; - -use translatable_shared::misc::templating::FormatString; - -#[test] -pub fn does_not_replace_not_found() { - let result = FormatString::from_str("Hello {name}") - .expect("Format string to be valid.") - .replace_with(&HashMap::new()); - - assert_eq!(result, "Hello {name}"); -} - -#[test] -pub fn replaces_single_template() { - let result = FormatString::from_str("Hello {name}") - .expect("Format string to be valid.") - .replace_with(&HashMap::from([("name".into(), "Josh".into())])); - - assert_eq!(result, "Hello Josh"); -} - -#[test] -pub fn replaces_multiple_templates() { - let result = FormatString::from_str("Hello {name} how are you doing {day}?") - .expect("Format string to be valid.") - .replace_with(&HashMap::from([ - ("name".into(), "Josh".into()), - ("day".into(), "today".into()), - ])); - - assert_eq!(result, "Hello Josh how are you doing today?"); -} - -#[test] -pub fn replaces_mix_found_not_found() { - let result = FormatString::from_str("Hello {name} how are you doing {day}?") - .expect("Format string to be valid.") - .replace_with(&HashMap::from([("name".into(), "Josh".into())])); - - assert_eq!(result, "Hello Josh how are you doing {day}?"); -} - -#[test] -pub fn fails_unclosed_template() { - let result = FormatString::from_str("Hello {"); - - assert!(result.is_err()); -} - -#[test] -pub fn escapes_templates() { - let result = FormatString::from_str("You write escaped templates like {{ this }}.") - .expect("Format string to be valid.") - .replace_with(&HashMap::from([("this".into(), "not replaced".into())])); - - assert_eq!(result, "You write escaped templates like {{ this }}.") -} - -#[test] -pub fn gives_original_string() { - let result = FormatString::from_str("Hello {name} how are you doing {day}?") - .expect("Format string to be valid."); - - assert_eq!(result.original(), "Hello {name} how are you doing {day}?"); -} diff --git a/translatable/tests/unitary/translation_collection.rs b/translatable/tests/unitary/translation_collection.rs deleted file mode 100644 index 33740d8..0000000 --- a/translatable/tests/unitary/translation_collection.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::collections::HashMap; - -use toml_edit::DocumentMut; -use translatable::Language; -use translatable_shared::translations::collection::TranslationNodeCollection; -use translatable_shared::translations::node::TranslationNode; - -const FILE_1: &str = r#" -[greetings.formal] -es = "Hola" -en = "Hello" -"#; - -const FILE_2: &str = r#" -[greetings.informal] -es = "Que haces?" -en = "Wyd?" -"#; - -#[test] -pub fn loads_and_finds_collection() { - let collection = TranslationNodeCollection::new(HashMap::from([ - ( - "a".into(), - TranslationNode::try_from( - FILE_1 - .parse::() - .expect("TOML to be parsed correctly.") - .as_table(), - ) - .expect("TOML to follow the translation rules."), - ), - ( - "b".into(), - TranslationNode::try_from( - FILE_2 - .parse::() - .expect("TOML to be parsed correctly.") - .as_table(), - ) - .expect("TOML to follow the translation rules."), - ), - ])); - - let translation = collection - .find_path( - &"greetings.formal" - .split(".") - .collect(), - ) - .expect("Translation to be found.") - .get(&Language::ES) - .expect("Language to be available.") - .replace_with(&HashMap::new()); - - assert_eq!(translation, "Hola"); -} diff --git a/translatable/tests/unitary_tests.rs b/translatable/tests/unitary_tests.rs deleted file mode 100644 index 9f69699..0000000 --- a/translatable/tests/unitary_tests.rs +++ /dev/null @@ -1,3 +0,0 @@ -// Errors are not tested. - -mod unitary; diff --git a/translatable_proc/Cargo.toml b/translatable_proc/Cargo.toml index 350a23b..04c197c 100644 --- a/translatable_proc/Cargo.toml +++ b/translatable_proc/Cargo.toml @@ -13,9 +13,5 @@ proc-macro = true [dependencies] proc-macro2 = "1.0.95" -quote = "1.0.38" -strum = { version = "0.27.1", features = ["derive"] } -syn = { version = "2.0.98", features = ["full"] } -thiserror = "2.0.11" -toml_edit = "0.22.26" -translatable_shared = { version = "1", path = "../translatable_shared/" } +quote = "1.0.40" +syn = { version = "2.0.101", features = ["full"] } diff --git a/translatable_proc/src/data/config.rs b/translatable_proc/src/data/config.rs deleted file mode 100644 index c11b367..0000000 --- a/translatable_proc/src/data/config.rs +++ /dev/null @@ -1,265 +0,0 @@ -//! User configuration module. -//! -//! This module defines the structures and -//! helper functions for parsing and loading -//! user configuration files. - -use std::env::var; -use std::fs::read_to_string; -use std::io::Error as IoError; -use std::sync::OnceLock; - -use strum::EnumString; -use thiserror::Error; -use toml_edit::{DocumentMut, TomlError}; - -/// Configuration error enum. -/// -/// Used for compile-time configuration -/// errors, such as errors while opening -/// files or parsing a file format. -/// -/// The errors from this enum are directly -/// shown in rust-analyzer. -#[derive(Error, Debug)] -pub enum ConfigError { - /// IO error derivations. - /// - /// Usually errors while interacting - /// with the file system. - /// - /// [`Display`] forwards the inner error [`Display`] - /// value with some prefix text. - /// - /// The enum implements [`From`] to - /// allow conversion from [`std::io::Error`]. - /// - /// **Parameters** - /// * `0` - The IO error derivation. - /// - /// [`Display`]: std::fmt::Display - /// [`From`]: std::io::Error - #[error("IO error reading configuration: {0:#}")] - Io(#[from] IoError), - - /// TOML deserialization error derivations. - /// - /// The configuration file contents could - /// not be parsed as TOML. - /// - /// The error is formatted displaying - /// the file name hardcoded as `./translatable.toml` - /// and appended with the line and character. - /// - /// The enum implements [`From`] to - /// allow conversion from [`toml::de::Error`] - /// - /// **Parameters** - /// * `0` - The TOML deserialization error derivation. - /// - /// [`From`]: toml::de::Error - #[error( - "TOML parse error '{}'{}", - .0.message(), - .0.span() - .map(|l| format!(" in ./translatable.toml:{}:{}", l.start, l.end)) - .unwrap_or_else(|| "".into()) - )] - ParseToml(#[from] TomlError), - - /// Parse value error. - /// - /// There was an error while parsing - /// a specific configuration entry, - /// since these are mapped to enums in - /// most cases. - /// - /// The error has a custom format - /// displaying the key and value - /// that should have been parsed. - /// - /// **Parameters** - /// * `0` - The configuration key for which the entry - /// could not be parsed. - /// * `1` - The configuration value that couldn't be - /// parsed. - #[error("Couldn't parse configuration entry '{1}' for '{0}'")] - InvalidValue(String, String), -} - -/// Defines the search strategy for configuration files. -/// -/// Represents the possible values of the parsed `seek_mode` -/// field, which determine the order in which file paths -/// are considered when opening configuration files. -#[derive(Default, Clone, Copy, EnumString)] -pub enum SeekMode { - /// Alphabetical order (default) - #[default] - Alphabetical, - - /// Reverse alphabetical order - Unalphabetical, -} - -/// Strategy for resolving translation conflicts. -/// -/// This enum defines how overlapping translations -/// are handled when multiple sources provide values -/// for the same key. The selected strategy determines -/// whether newer translations replace existing ones or -/// if the first encountered translation is preserved. -#[derive(Default, Clone, Copy, EnumString)] -pub enum TranslationOverlap { - /// Last found translation overwrites previous ones (default) - #[default] - Overwrite, - - /// First found translation is preserved - Ignore, -} - -/// Main configuration structure for the translation system. -/// -/// Holds all the core parameters used to control how translation files are -/// located, processed, and how conflicts are resolved between overlapping -/// translations. -pub struct MacroConfig { - /// Path to the directory containing translation files. - /// - /// Specifies the base location where the system will search for - /// translation files. - /// - /// # Example - /// ```toml - /// path = "./locales" - /// ``` - path: String, - - /// File processing order strategy. - /// - /// Defines the order in which translation files are processed. - /// Default: alphabetical order. - seek_mode: SeekMode, - - /// Translation conflict resolution strategy. - /// - /// Determines the behavior when multiple files contain the same - /// translation key. - overlap: TranslationOverlap, -} - -impl MacroConfig { - /// Get reference to the configured locales path. - /// - /// **Returns** - /// The path to the directory where translation files are expected - /// to be located. - pub fn path(&self) -> &str { - &self.path - } - - /// Get the current seek mode strategy. - /// - /// **Returns** - /// The configured strategy used to determine the order in which - /// translation files are processed. - pub fn seek_mode(&self) -> SeekMode { - self.seek_mode - } - - /// Get the current overlap resolution strategy. - /// - /// **Returns** - /// The configured strategy for resolving translation conflicts - /// when multiple files define the same key. - pub fn overlap(&self) -> TranslationOverlap { - self.overlap - } -} - -/// Global configuration cache. -/// -/// Stores the initialized [`MacroConfig`] instance, which holds the -/// configuration for the translation system. The [`OnceLock`] ensures the -/// configuration is initialized only once and can be safely accessed across -/// multiple threads after that initialization. -static TRANSLATABLE_CONFIG: OnceLock = OnceLock::new(); - -/// Load the global translation configuration. -/// -/// Initializes and returns a reference to the shared [`MacroConfig`] instance. -/// Configuration values are loaded in the following priority order: -/// environment variables override `translatable.toml`, and missing values fall -/// back to hardcoded defaults. -/// -/// The configuration is cached after the first successful load, and reused on -/// subsequent calls. -/// -/// **Returns** -/// A `Result` containing either: -/// * [`Ok(&MacroConfig)`] — The loaded configuration as a reference to the -/// cached macro configuration. -/// * [`Err(ConfigError)`] — An error because environment couldn't be read or -/// `translatable.toml` couldn't be read. -/// -/// [`Ok(&MacroConfig)`]: MacroConfig -/// [`Err(ConfigError)`]: ConfigError -pub fn load_config() -> Result<&'static MacroConfig, ConfigError> { - if let Some(config) = TRANSLATABLE_CONFIG.get() { - return Ok(config); - } - - let toml_content = read_to_string("./translatable.toml") - .unwrap_or_default() - .parse::()?; - - macro_rules! config_value { - ($env_var:expr, $key:expr, $default:expr) => { - var($env_var) - .ok() - .or_else(|| { - toml_content - .get($key) - .and_then(|v| v.as_str()) - .map(|v| v.to_string()) - }) - .unwrap_or_else(|| $default.into()) - }; - - (parse($env_var:expr, $key:expr, $default:expr)) => {{ - let value = var($env_var) - .ok() - .or_else(|| { - toml_content - .get($key) - .and_then(|v| v.as_str()) - .map(|v| v.to_string()) - }); - - if let Some(value) = value { - value - .parse() - .map_err(|_| ConfigError::InvalidValue($key.into(), value.into())) - } else { - Ok($default) - } - }}; - } - - let config = MacroConfig { - path: config_value!("TRANSLATABLE_LOCALES_PATH", "path", "./translations"), - overlap: config_value!(parse( - "TRANSLATABLE_OVERLAP", - "overlap", - TranslationOverlap::Ignore - ))?, - seek_mode: config_value!(parse( - "TRANSLATABLE_SEEK_MODE", - "seek_mode", - SeekMode::Alphabetical - ))?, - }; - - Ok(TRANSLATABLE_CONFIG.get_or_init(|| config)) -} diff --git a/translatable_proc/src/data/mod.rs b/translatable_proc/src/data/mod.rs deleted file mode 100644 index af291da..0000000 --- a/translatable_proc/src/data/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! External data obtention module. -//! -//! This module contains the sub-modules -//! to obtain the translation data and -//! related configuration. -//! -//! The only thing that should possibly -//! be used outside is the [`translations`] -//! module, as the config is mostly -//! to read the translations from the files. - -pub mod config; -pub mod translations; diff --git a/translatable_proc/src/data/translations.rs b/translatable_proc/src/data/translations.rs deleted file mode 100644 index 6998af4..0000000 --- a/translatable_proc/src/data/translations.rs +++ /dev/null @@ -1,219 +0,0 @@ -//! Translation obtention module. -//! -//! This module is used to obtain -//! translations from their respective files. -//! -//! This module uses `crate::data::config` to -//! to load the translations and order them -//! based on the configuration provided -//! by the module. - -use std::fs::{read_dir, read_to_string}; -use std::io::Error as IoError; -use std::sync::OnceLock; - -use thiserror::Error; -use toml_edit::{DocumentMut, TomlError}; -use translatable_shared::translations::collection::TranslationNodeCollection; -use translatable_shared::translations::node::{TranslationNode, TranslationNodeError}; - -use super::config::{ConfigError, SeekMode, TranslationOverlap, load_config}; - -/// Translation retrieval error enum. -/// -/// Represents errors that can occur during compile-time translation -/// retrieval. This includes I/O issues, configuration loading failures, -/// TOML deserialization errors, and translation node parsing errors. -/// -/// The errors from this enum are directly surfaced in `rust-analyzer` -/// to assist with early detection and debugging. -#[derive(Error, Debug)] -pub enum TranslationDataError { - /// I/O error derivation. - /// - /// Raised when an I/O operation fails during translation - /// retrieval, typically caused by filesystem-level issues. - /// - /// [`Display`] will forward the inner [`std::io::Error`] - /// representation prefixed with additional context. - /// - /// The enum implements [`From`] to allow - /// automatic conversion from `IoError`. - /// - /// **Parameters** - /// * `0` — The underlying I/O error. - /// - /// [`From`]: std::io::Error - /// [`Display`]: std::fmt::Display - #[error("IO Error: \"{0:#}\". Please check the specified path in your configuration file.")] - Io(#[from] IoError), - - /// Configuration loading failure. - /// - /// Raised when the translation configuration cannot be loaded - /// successfully, typically due to invalid values or missing - /// configuration data. - /// - /// [`Display`] will forward the inner [`ConfigError`] message. - /// - /// The enum implements [`From`] to allow automatic - /// conversion from the underlying error. - /// - /// **Parameters** - /// * `0` — The configuration error encountered. - /// - /// [`Display`]: std::fmt::Display - #[error("{0:#}")] - LoadConfig(#[from] ConfigError), - - /// Invalid Unicode path. - /// - /// Raised when a filesystem path cannot be processed due to - /// invalid Unicode characters. - /// - /// This error signals that the translation system cannot proceed - /// with a non-Unicode-compatible path. - #[error("Couldn't open path, found invalid unicode characters")] - InvalidUnicode, - - /// TOML deserialization failure. - /// - /// Raised when the contents of a translation file cannot be - /// parsed as valid TOML data. - /// - /// The formatted error message includes the deserialization reason, - /// the location within the file (if available), and the file path. - /// - /// **Parameters** - /// * `0` — The [`toml::de::Error`] carrying the underlying deserialization - /// error. - /// * `1` — The file path of the TOML file being parsed. - #[error( - "TOML Deserialization error '{reason}' {span} in {1}", - reason = _0.message(), - span = _0 - .span() - .map(|range| format!("on {}:{}", range.start, range.end)) - .unwrap_or_else(String::new) - )] - ParseToml(TomlError, String), - - /// Translation node parsing failure. - /// - /// Raised when the translation system cannot correctly parse - /// a translation node, typically due to invalid formatting - /// or missing expected data. - /// - /// The enum implements [`From`] for - /// seamless conversion. - /// - /// **Parameters** - /// * `0` — The translation node error encountered. - #[error("{0:#}")] - Node(#[from] TranslationNodeError), -} - -/// Global thread-safe cache for loaded translations. -/// -/// Stores all parsed translations in memory after the first -/// successful load. Uses [`OnceLock`] to ensure that the translation -/// data is initialized only once in a thread-safe manner. -static TRANSLATIONS: OnceLock = OnceLock::new(); - -/// Recursively walks the target directory to discover all translation files. -/// -/// Uses an iterative traversal strategy to avoid recursion depth limitations. -/// Paths are returned as [`String`] values, ready for processing. -/// -/// Any filesystem errors, invalid paths, or read failures are reported -/// via `TranslationDataError`. -/// -/// **Arguments** -/// * `path` — Root directory to scan for translation files. -/// -/// **Returns** -/// A `Result` containing either: -/// * [`Ok(Vec)`] — A flat list of discovered file paths. -/// * [`Err(TranslationDataError)`] — If traversal fails at any point. -/// -/// [`Ok(Vec)`]: std::vec::Vec -/// [`Err(TranslationDataError)`]: TranslationDataError -fn walk_dir(path: &str) -> Result, TranslationDataError> { - let mut stack = vec![path.to_string()]; - let mut result = Vec::new(); - - while let Some(current_path) = stack.pop() { - let directory = read_dir(¤t_path)?.collect::, _>>()?; - - for entry in directory { - let path = entry.path(); - if path.is_dir() { - stack.push( - path.to_str() - .ok_or(TranslationDataError::InvalidUnicode)? - .to_string(), - ); - } else { - result.push( - path.to_string_lossy() - .to_string(), - ); - } - } - } - - Ok(result) -} - -/// Loads and caches translations from the configured directory. -/// -/// On the first invocation, this function: -/// - Reads the translation directory path from the loaded configuration. -/// - Recursively walks the directory to discover all translation files. -/// - Sorts the file list according to the configured `seek_mode`. -/// - Parses each file and validates its content. -/// -/// Once successfully loaded, the parsed translations are stored -/// in a global [`OnceLock`]-backed cache and reused for the lifetime -/// of the process. -/// -/// This function will return a reference to the cached translations -/// on every subsequent call. -/// -/// **Returns** -/// A [`Result`] containing either: -/// * [`Ok(&TranslationNodeCollection)`] — The parsed and cached translations. -/// * [`Err(TranslationDataError)`] — An error because any of the translation -/// files couldn't be read. -/// -/// [`Ok(&TranslationNodeCollection)`]: TranslationNodeCollection -/// [`Err(TranslationDataError)`]: TranslationDataError -pub fn load_translations() -> Result<&'static TranslationNodeCollection, TranslationDataError> { - if let Some(translations) = TRANSLATIONS.get() { - return Ok(translations); - } - - let config = load_config()?; - let mut translation_paths = walk_dir(config.path())?; - - // Apply sorting based on configuration - translation_paths.sort_by_key(|path| path.to_lowercase()); - if matches!(config.seek_mode(), SeekMode::Unalphabetical) - || matches!(config.overlap(), TranslationOverlap::Overwrite) - { - translation_paths.reverse(); - } - - let translations = translation_paths - .iter() - .map(|path| { - let table = read_to_string(path)? - .parse::() - .map_err(|err| TranslationDataError::ParseToml(err, path.clone()))?; - - Ok((path.clone(), TranslationNode::try_from(table.as_table())?)) - }) - .collect::>()?; - - Ok(TRANSLATIONS.get_or_init(|| translations)) -} diff --git a/translatable_proc/src/generation/config.rs b/translatable_proc/src/generation/config.rs new file mode 100644 index 0000000..e69de29 diff --git a/translatable_proc/src/generation/context.rs b/translatable_proc/src/generation/context.rs new file mode 100644 index 0000000..e69de29 diff --git a/translatable/tests/integration/mod.rs b/translatable_proc/src/generation/mod.rs similarity index 69% rename from translatable/tests/integration/mod.rs rename to translatable_proc/src/generation/mod.rs index f374719..311a8a9 100644 --- a/translatable/tests/integration/mod.rs +++ b/translatable_proc/src/generation/mod.rs @@ -1,2 +1,4 @@ + +pub mod config; pub mod context; pub mod translation; diff --git a/translatable_proc/src/generation/translation.rs b/translatable_proc/src/generation/translation.rs new file mode 100644 index 0000000..e69de29 diff --git a/translatable_proc/src/lib.rs b/translatable_proc/src/lib.rs index 547b82f..ebd405b 100644 --- a/translatable_proc/src/lib.rs +++ b/translatable_proc/src/lib.rs @@ -1,107 +1,19 @@ -//! Macro declarations for the `translatable` crate. -//! -//! This crate shouldn't be used by itself, -//! since the macros generate code with the context -//! of the `translatable` library. -//! -//! The `translatable` library re-exports the macros -//! declared in this crate. - -#![warn(missing_docs)] - -use macro_generation::context::context_macro; -use macro_generation::translation::translation_macro; -use macro_input::context::{ContextMacroArgs, ContextMacroStruct}; -use macro_input::translation::TranslationMacroArgs; use proc_macro::TokenStream; -use syn::parse_macro_input; -mod data; -mod macro_generation; -mod macro_input; +mod generation; +mod parsing; -/// # Translation obtention macro. -/// -/// This macro generates the way to obtain a translation -/// from the translation files in the directory defined -/// in the `translatable.toml` file. -/// -/// **Parameters** -/// * `language` - A string literal for static inference or an instance of -/// `translatable::Language` for dynamic inference. -/// * `path` - A pat prefixed with `static` for static inference or a `Vec` -/// for dynamic inference. -/// * `replacements` - Arguments similar to python's `kwargs` for the -/// translation replacements. -/// -/// This macro provides optimizations depending on the dynamism -/// of the parameters while calling the macro. -/// -/// The optimizations are described the following way -/// - If path is static, no runtime lookup will be required -/// - If the path is dynamic, the file structure will be hardcoded. -/// -/// - If the language is static, the validation will be reported by -/// `rust-analyzer`. -/// - If the language is dynamic the validation will be reported in runtime in -/// the `Err` branch. -/// -/// - If both are dynamic a single [`String`] will be generated. -/// -/// Independently of any other parameter, the `replacements` parameter -/// is always dynamic (context based). -/// -/// You can shorten it's invocation if a similar identifier is on scope, -/// for example `x = x` can be shortened with `x`. -/// -/// Replacement parameters are not validated, if a parameter exists it will be -/// replaced otherwise it won't. -/// -/// **Returns** -/// A `Result` containing either: -/// * `Ok(String)` - If the invocation is successful. -/// * `Err(translatable::Error)` - If the invocation fails with a runtime error. #[proc_macro] -pub fn translation(input: TokenStream) -> TokenStream { - translation_macro(parse_macro_input!(input as TranslationMacroArgs).into()).into() +pub fn translation(tokens: TokenStream) -> TokenStream { + todo!() } -/// # Translation context macro -/// -/// This macro converts a struct into a translation context. -/// -/// By definition that struct shouldn't be used for anything else, -/// but nothing stops you from doing so. -/// -/// This macro applies a rule to the struct. All fields must be -/// a `String` or `&str`. -/// -/// You can configure some parameters as a punctuated [`MetaNameValue`], -/// these are -/// - `base_path`: A path that gets prepended to all fields. -/// - `fallback_language`: A language that must be available for all -/// paths and changes the return type of the `load_translations` method. -/// -/// All the fields on the struct now point to paths in your translation -/// files, you can extend these paths applying the `#[path()]` attribute -/// with a [`TranslationPath`]. Otherwise the path will be appended as -/// the field identifier. -/// -/// The field and struct visibility are kept as original. -/// -/// This macro also generates a method called `load_translations` dynamically -/// that loads all translations and returns an instance of the struct, -/// optionally wrapped on a result depending on the `fallback_language` -/// parameter value. -/// -/// [`MetaNameValue`]: syn::MetaNameValue -/// [`TranslationPath`]: macro_input::utils::translation_path::TranslationPath -#[proc_macro_attribute] -pub fn translation_context(attr: TokenStream, item: TokenStream) -> TokenStream { - context_macro( - parse_macro_input!(attr as ContextMacroArgs), - parse_macro_input!(item as ContextMacroStruct), - ) - .into() +#[proc_macro_derive(TranslationContext, attributes(path, base_path))] +pub fn translation_context(structure: TokenStream) -> TokenStream { + todo!() +} + +#[proc_macro] +pub fn translatable_config(tokens: TokenStream) -> TokenStream { + todo!() } diff --git a/translatable_proc/src/macro_generation/context.rs b/translatable_proc/src/macro_generation/context.rs deleted file mode 100644 index d78e6f6..0000000 --- a/translatable_proc/src/macro_generation/context.rs +++ /dev/null @@ -1,178 +0,0 @@ -//! [`#\[translation_context\]`] macro output module. -//! -//! This module contains the required for -//! the generation of the [`#\[translation_context\]`] macro tokens -//! with intrinsics from `macro_input::context`. -//! -//! [`#\[translation_context\]`]: crate::translation_context - -use proc_macro2::TokenStream as TokenStream2; -use quote::{ToTokens, quote}; -use thiserror::Error; -use translatable_shared::handle_macro_result; -use translatable_shared::macros::collections::map_to_tokens; - -use crate::data::translations::load_translations; -use crate::macro_input::context::{ContextMacroArgs, ContextMacroStruct}; - -/// Macro compile-time translation resolution error. -/// -/// Represents errors that can occur while compiling the -/// [`#\[translation_context\]`] macro. This includes cases where a translation -/// path cannot be found or fallback is not available for all the translations -/// in the context. -/// -/// These errors are reported at compile-time by `rust-analyzer` -/// for immediate feedback while invoking the [`#\[translation_context\]`] -/// macro. -/// -/// [`#\[translation_context\]`]: crate::translation_context -#[derive(Error, Debug)] -enum MacroCompileError { - /// The requested translation path could not be found. - /// - /// **Parameters** - /// * `0` — The translation path, displayed in `::` notation. - #[error("A translation with the path '{0}' could not be found")] - TranslationNotFound(String), - - /// A fallback is not available for a specified translation path. - #[error("One of the translations doesn't have the fallback language available")] - FallbackNotAvailable, - - /// One of the fields type is not a &str or String. - #[error("Only String' and '&str' is allowed for translation contexts")] - TypeNotAllowed, -} - -/// [`#\[translation_context\]`] macro output generation. -/// -/// Expands into a struct that implements structured translation -/// loading. -/// -/// If there is a fallback language configured, this is checked -/// with all the paths and then the `load_translations` generated -/// method will return the same structure instead of a Result. -/// -/// **Arguments** -/// * `macro_args` - The parsed arguments for the macro invocation. -/// * `macro_input` - The parsed macro tokens themselves. -/// -/// **Returns** -/// A TokenStream representing the implementation. -/// -/// [`#\[translation_context\]`]: crate::translation_context -pub fn context_macro( - macro_args: ContextMacroArgs, - macro_input: ContextMacroStruct, -) -> TokenStream2 { - let translations = handle_macro_result!(out load_translations()); - let base_path = macro_args.base_path(); - - let struct_pub = macro_input.visibility(); - let struct_ident = macro_input.ident(); - - let struct_fields = handle_macro_result!(out - macro_input - .fields() - .iter() - .map(|field| { - let field_ty = field.ty().to_token_stream().to_string(); - if matches!(field_ty.as_str(), "String" | "&str") { - Ok(field) - } else { - Err(MacroCompileError::TypeNotAllowed) - } - }) - .collect::, _>>() - ); - - let loadable_translations = handle_macro_result!(out - macro_input - .fields() - .iter() - .map(|field| { - let path_segments = base_path - .merge(&field.path()); - - let path_segments_display = path_segments - .join("::"); - - let translation = translations - .find_path(&path_segments) - .ok_or(MacroCompileError::TranslationNotFound(path_segments.join("::")))?; - - let translation_tokens = map_to_tokens(translation); - let ident = field.ident(); - - let handler = if let Some(fallback_language) = macro_args.fallback_language() { - if let Some(translation) = translation.get(&fallback_language) { - quote! { - .unwrap_or(&#translation) - } - } else { - return Err(MacroCompileError::FallbackNotAvailable); - } - } else { - quote! { - .ok_or_else(|| translatable::Error::LanguageNotAvailable( - language.clone(), - #path_segments_display.to_string() - ))? - } - }; - - Ok(quote! { - #ident: #translation_tokens - .get(&language) - #handler - .replace_with(&replacements) - }) - }) - .collect::, MacroCompileError>>() - ); - - let is_lang_some = macro_args - .fallback_language() - .is_some(); - - let load_ret_ty = if is_lang_some { - quote! { Self } - } else { - quote! { Result } - }; - - let load_ret_stmnt = if is_lang_some { - quote! { - Self { - #(#loadable_translations),* - } - } - } else { - quote! { - Ok(Self { - #(#loadable_translations),* - }) - } - }; - - quote! { - #struct_pub struct #struct_ident { - #(#struct_fields),* - } - - impl #struct_ident { - #struct_pub fn load_translations( - language: translatable::Language, - replacements: &std::collections::HashMap - ) -> #load_ret_ty { - let replacements = replacements - .iter() - .map(|(key, value)| (key.to_string(), value.to_string())) - .collect::>(); - - #load_ret_stmnt - } - } - } -} diff --git a/translatable_proc/src/macro_generation/mod.rs b/translatable_proc/src/macro_generation/mod.rs deleted file mode 100644 index eeddfdf..0000000 --- a/translatable_proc/src/macro_generation/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! Macro generation module. -//! -//! This module contains the sub-modules -//! to generate any kind of macro, in the -//! `lib.rs` file, a call to any of this -//! modules may be issued with intrinsics -//! from the [`macro_input`] module. -//! -//! Each module represents a single macro. -//! -//! [`macro_input`]: crate::macro_input - -pub mod context; -pub mod translation; diff --git a/translatable_proc/src/macro_generation/translation.rs b/translatable_proc/src/macro_generation/translation.rs deleted file mode 100644 index 21263a5..0000000 --- a/translatable_proc/src/macro_generation/translation.rs +++ /dev/null @@ -1,163 +0,0 @@ -//! [`translation!()`] macro output module. -//! -//! This module contains the required for -//! the generation of the [`translation!()`] macro tokens -//! with intrinsics from [`macro_input::translation`]. -//! -//! [`translation!()`]: crate::translation -//! [`macro_input::translation`]: super::super::macro_input::translation - -use proc_macro2::TokenStream as TokenStream2; -use quote::{ToTokens, quote}; -use thiserror::Error; -use translatable_shared::handle_macro_result; -use translatable_shared::macros::collections::{map_to_tokens, map_transform_to_tokens}; -use translatable_shared::misc::language::Language; - -use crate::data::translations::load_translations; -use crate::macro_input::translation::TranslationMacroArgs; -use crate::macro_input::utils::input_type::InputType; - -/// Macro compile-time translation resolution error. -/// -/// Represents errors that can occur while compiling the [`translation!()`] -/// macro. This includes cases where a translation path cannot be found or -/// a language variant is unavailable at the specified path. -/// -/// These errors are reported at compile-time by `rust-analyzer` -/// for immediate feedback while invoking the [`translation!()`] macro. -/// -/// [`translation!()`]: crate::translation -#[derive(Error, Debug)] -enum MacroCompileError { - /// The requested translation path could not be found. - /// - /// **Parameters** - /// * `0` — The translation path, displayed in `::` notation. - #[error("The path '{0}' could not be found")] - PathNotFound(String), - - /// The requested language is not available for the provided translation - /// path. - /// - /// **Parameters** - /// * `0` — The requested `Language`. - /// * `1` — The translation path where the language was expected. - #[error("The language '{0:?}' ('{0:#}') is not available for the path '{1}'")] - LanguageNotAvailable(Language, String), -} - -/// [`translation!()`] macro output generation. -/// -/// Expands into code that resolves a translation string based on the input -/// language and translation path, performing placeholder substitutions -/// if applicable. -/// -/// If the language and path are fully static, the translation will be resolved -/// during macro expansion. Otherwise, the generated code will include runtime -/// resolution logic. -/// -/// If the path or language is invalid at compile time, an appropriate -/// `MacroCompileError` will be reported. -/// -/// **Arguments** -/// * `input` — Structured arguments defining the translation path, language, -/// and any placeholder replacements obtained from [`macro_input::translation`]. -/// -/// **Returns** -/// Generated `TokenStream2` representing the resolved translation string or -/// runtime lookup logic. -/// -/// [`macro_input::translation`]: super::super::macro_input::translation -/// [`translation!()`]: crate::translation -pub fn translation_macro(input: TranslationMacroArgs) -> TokenStream2 { - let translations = handle_macro_result!(load_translations()); - - let template_replacements = map_transform_to_tokens( - input.replacements(), - |key, value| quote! { (stringify!(#key).to_string(), #value.to_string()) }, - ); - - if let InputType::Static(language) = input.language() { - if let InputType::Static(path) = input.path() { - let path_segments = path.segments(); - let static_path_display = path_segments.join("::"); - - let translation_object = translations - .find_path(path_segments) - .ok_or_else(|| MacroCompileError::PathNotFound(static_path_display.clone())); - - let translation = handle_macro_result!( - handle_macro_result!(translation_object) - .get(language) - .ok_or_else(|| { - MacroCompileError::LanguageNotAvailable( - language.clone(), - static_path_display.clone(), - ) - }) - ); - - return quote! { - #translation - .replace_with(&#template_replacements) - }; - } - } - - let language = match input.language() { - InputType::Static(language) => language - .clone() - .to_token_stream(), - InputType::Dynamic(language) => quote! { - translatable::shared::misc::language::Language::from(#language) - }, - }; - - let translation_object = match input.path() { - InputType::Static(path) => { - let path_segments = path.segments(); - let static_path_display = path_segments.join("::"); - - let translation_object = translations - .find_path(path_segments) - .ok_or_else(|| MacroCompileError::PathNotFound(static_path_display.clone())); - - let translations_tokens = map_to_tokens(handle_macro_result!(translation_object)); - - quote! { - #[doc(hidden)] - let path: Vec<_> = vec![#(#path_segments.to_string()),*]; - - #translations_tokens - } - }, - - InputType::Dynamic(path) => { - let translations_tokens = translations.to_token_stream(); - - quote! { - #[doc(hidden)] - let path: Vec<_> = #path; - - #translations_tokens - .find_path(&path) - .ok_or_else(|| translatable::Error::PathNotFound(path.join("::")))? - } - }, - }; - - quote! { - (|| -> Result { - std::result::Result::Ok({ - #[doc(hidden)] - let language = #language; - - #translation_object - .get(&language) - .ok_or_else(|| translatable::Error::LanguageNotAvailable(language, path.join("::")))? - .replace_with(&#template_replacements) - }) - })() - } -} diff --git a/translatable_proc/src/macro_input/context.rs b/translatable_proc/src/macro_input/context.rs deleted file mode 100644 index f36e6a2..0000000 --- a/translatable_proc/src/macro_input/context.rs +++ /dev/null @@ -1,393 +0,0 @@ -//! [`#\[translation_context\]`] input parsing module. -//! -//! This module declares a structure that implements -//! [`Parse`] for it to be used with [`parse_macro_input`]. -//! -//! [`#\[translation_context\]`]: crate::translation_context -//! [`parse_macro_input`]: syn::parse_macro_input - -use std::str::FromStr; - -use proc_macro2::TokenStream; -use quote::{ToTokens, TokenStreamExt, quote}; -use syn::parse::{Parse, ParseStream}; -use syn::{ - Error as SynError, - Expr, - ExprLit, - Field, - Ident, - ItemStruct, - Lit, - MetaNameValue, - Result as SynResult, - Token, - Type, - Visibility, - parse2, -}; -use thiserror::Error; -use translatable_shared::macros::errors::IntoCompileError; -use translatable_shared::misc::language::Language; - -use super::utils::translation_path::TranslationPath; - -/// Parse error for [`ContextMacroArgs`] and [`ContextMacroStruct`]. -/// -/// Represents errors that can occur while parsing the -/// [`#\[translation_context\]`] macro input. This error is only used while -/// parsing compile-time input, as runtime input is validated in runtime. -/// -/// [`#\[translation_context\]`]: crate::translation_context -#[derive(Error, Debug)] -enum MacroArgsError { - /// Invalid field type error. - /// - /// Usually from using an invalid struct type, such - /// as tuple or unit. - #[error("Only named fields are allowed")] - InvalidFieldType, - - /// Invalid language parameter for fallback. - /// - /// Fallback only supports static language, same - /// as the [`translation!()`] macro static language - /// parameter. - /// - /// [`translation!()`]: crate::translation - #[error("Only a language literal is allowed")] - OnlyLangLiteralAllowed, - - /// Invalid ISO-639-1 language literal. - /// - /// Language literals must be ISO-639-1 compliant. - /// - /// **Parameters** - /// * `0` - The invalid language literal. - #[error("Invalid language literal '{0}' is not a valid ISO-639-1 language")] - InvalidLanguageLiteral(String), - - /// Invalid macro parameter. - /// - /// **Parameters** - /// * `0` - The unknown parameter key. - #[error("Unknown key '{0}', allowed keys are 'fallback_language' and 'base_path'")] - UnknownKey(String), -} - -/// The arguments passed to the context macro. -/// -/// These arguments are passed literally as a punctuated -/// [`MetaNameValue`] separated by `Token![,]`. -/// -/// These act as configuration overrides for each context -/// struct. -pub struct ContextMacroArgs { - /// Field base path. - /// - /// A base path to be prepended to all - /// field paths. - base_path: TranslationPath, - - /// Context fallback language. - /// - /// The fallback should be available - /// in all the specified paths, removes - /// the need to handle errors if a language - /// is not available for a specific translation. - fallback_language: Option, -} - -/// A field inside a translation context struct. -/// -/// Fields are parsed independently and moved -/// to a [`ContextMacroStruct`], this contains -/// data about how to load a translation. -pub struct ContextMacroField { - /// The translation path. - /// - /// This path is appended to the - /// path passed to the struct configuration. - path: Option, - - /// The field visibility. - /// - /// This gets literally rendered as is. - visibility: Visibility, - - /// The field name. - /// - /// This gets literally rendered as is. - ident: Ident, - - /// The field type. - /// - /// Validated but rendered as is. - ty: Type, -} - -/// Translation context struct data. -/// -/// This parses the struct necessary data -/// to re-generate it preparated to load -/// translations, loading [`ContextMacroField`]s -/// too. -pub struct ContextMacroStruct { - /// The struct visibility. - /// - /// This gets literally rendered as is. - visibility: Visibility, - - /// The struct name. - /// - /// This gets literally rendered as is. - ident: Ident, - - /// The struct fields. - /// - /// Get rendered as specified in the - /// [`ContextMacroField::to_tokens`] implementation. - fields: Vec, -} - -impl ContextMacroArgs { - /// Base path getter. - /// - /// **Returns** - /// A reference to the `base_path`. - #[inline] - #[allow(unused)] - pub fn base_path(&self) -> &TranslationPath { - &self.base_path - } - - /// Fallback language getter. - /// - /// **Returns** - /// A reference o the `fallback_language`. - #[inline] - #[allow(unused)] - pub fn fallback_language(&self) -> Option { - self.fallback_language - .clone() - } -} - -/// [`Parse`] implementation for [`ContextMacroArgs`]. -/// -/// This implementation is to be used within [`parse_macro_input!()`] -/// and parses the macro arguments to modify the macro behavior. -/// -/// [`parse_macro_input!()`]: syn::parse_macro_input -impl Parse for ContextMacroArgs { - fn parse(input: ParseStream) -> SynResult { - let values = input.parse_terminated(MetaNameValue::parse, Token![,])?; - let mut base_path = None; - let mut fallback_language = None; - - for kvp in values { - let key = kvp - .path - .to_token_stream() - .to_string(); - - match key.as_str() { - "base_path" => { - base_path = Some(parse2::( - kvp.value - .to_token_stream(), - )?); - }, - - "fallback_language" => { - if let Expr::Lit(ExprLit { lit: Lit::Str(lit), .. }) = kvp.value { - fallback_language = Some( - Language::from_str( - lit.value() - .as_str(), - ) - .map_err(|_| { - MacroArgsError::InvalidLanguageLiteral(lit.value()) - .to_syn_error(lit) - })?, - ); - } else { - return Err(MacroArgsError::OnlyLangLiteralAllowed.to_syn_error(kvp.value)); - } - }, - - key => { - return Err(MacroArgsError::UnknownKey(key.to_string()).to_syn_error(kvp.path)); - }, - } - } - - let base_path = base_path.unwrap_or_else(|| TranslationPath::default()); - - Ok(Self { base_path, fallback_language }) - } -} - -impl ContextMacroField { - /// Path getter. - /// - /// The path specified in the attribute - /// otherwise a path with a single segment - /// as the attribute ident. Alternative lazily - /// evaluated. - /// - /// **Returns** - /// The corresponding translation path for the field. - #[inline] - #[allow(unused)] - pub fn path(&self) -> TranslationPath { - self.path - .clone() - .unwrap_or_else(|| { - TranslationPath::new( - vec![ - self.ident - .to_string(), - ], - self.ident - .span(), - ) - }) - } - - /// Visibility getter. - /// - /// **Returns** - /// A reference to this field's visibility. - #[inline] - #[allow(unused)] - pub fn visibility(&self) -> &Visibility { - &self.visibility - } - - /// Identifier getter. - /// - /// **Returns** - /// A reference to this field's identifier. - #[inline] - #[allow(unused)] - pub fn ident(&self) -> &Ident { - &self.ident - } - - /// Type getter. - /// - /// **Returns** - /// A reference to this field's type. - #[inline] - #[allow(unused)] - pub fn ty(&self) -> &Type { - &self.ty - } -} - -/// [`ToTokens`] implementation for [`ContextMacroField`]. -/// -/// This implementation is used to convert the -/// data stored in this struct to the tokens -/// it represnets. -impl ToTokens for ContextMacroField { - fn to_tokens(&self, tokens: &mut TokenStream) { - let visibility = self.visibility(); - let ident = self.ident(); - let ty = self.ty(); - - tokens.append_all(quote! { - #visibility #ident: #ty - }); - } -} - -/// [`TryFrom`] implementation for [`ContextMacroField`]. -/// -/// This implementation is used to parse -/// the custom metadata from a struct field. -impl TryFrom for ContextMacroField { - type Error = SynError; - - fn try_from(field: Field) -> Result { - let path = field - .attrs - .iter() - .find(|field| { - field - .path() - .is_ident("path") - }) - .map(|field| field.parse_args::()) - .transpose()?; - - let is_pub = field - .vis - .clone(); - - let ident = field - .ident - .clone() - .ok_or(MacroArgsError::InvalidFieldType.to_syn_error(&field))?; - - let ty = field.ty; - - Ok(Self { path, visibility: is_pub, ident, ty }) - } -} - -impl ContextMacroStruct { - /// Visibility getter. - /// - /// **Returns** - /// A reference to this struct's visibility. - #[inline] - #[allow(unused)] - pub fn visibility(&self) -> &Visibility { - &self.visibility - } - - /// Identifier getter. - /// - /// **Returns** - /// A reference o this idenitifer visibility. - #[inline] - #[allow(unused)] - pub fn ident(&self) -> &Ident { - &self.ident - } - - /// Fields getter. - /// - /// **Returns** - /// A slice to all the fields in this struct. - #[inline] - #[allow(unused)] - pub fn fields(&self) -> &[ContextMacroField] { - &self.fields - } -} - -/// [`Parse`] implementation for [`ContextMacroStruct`]. -/// -/// This implementation is used to parse the struct -/// trough [`parse_macro_input!()`]. -/// -/// [`parse_macro_input!()`]: syn::parse_macro_input -impl Parse for ContextMacroStruct { - fn parse(input: ParseStream) -> SynResult { - let structure = input.parse::()?; - - let is_pub = structure.vis; - let ident = structure.ident; - - let fields = structure - .fields - .into_iter() - .map(|field| ContextMacroField::try_from(field)) - .collect::, _>>()?; - - Ok(Self { visibility: is_pub, ident, fields }) - } -} diff --git a/translatable_proc/src/macro_input/mod.rs b/translatable_proc/src/macro_input/mod.rs deleted file mode 100644 index 1ff13d5..0000000 --- a/translatable_proc/src/macro_input/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -//! Macro input parsing module. -//! -//! This module contains the sub-modules -//! to parse macro input for specific -//! macros, the parsed input is usually -//! fed to [`macro_generation`] as intrinsics. -//! -//! Each sub-module represents a different macro, -//! except for separated utils. -//! -//! [`macro_generation`]: crate::macro_generation - -pub mod context; -pub mod translation; -pub mod utils; diff --git a/translatable_proc/src/macro_input/translation.rs b/translatable_proc/src/macro_input/translation.rs deleted file mode 100644 index 7b93b6c..0000000 --- a/translatable_proc/src/macro_input/translation.rs +++ /dev/null @@ -1,169 +0,0 @@ -//! [`translation!()`] input parsing module. -//! -//! This module declares a structure that implements -//! [`Parse`] for it to be used with [`parse_macro_input`]. -//! -//! [`translation!()`]: crate::translation -//! [`parse_macro_input`]: syn::parse_macro_input - -use std::collections::HashMap; - -use proc_macro2::TokenStream as TokenStream2; -use quote::ToTokens; -use syn::parse::{Parse, ParseStream}; -use syn::token::Static; -use syn::{Expr, ExprLit, Ident, Lit, Result as SynResult, Token}; -use thiserror::Error; -use translatable_shared::macros::errors::IntoCompileError; -use translatable_shared::misc::language::Language; - -use super::utils::input_type::InputType; -use super::utils::translation_path::TranslationPath; - -/// Parse error for [`TranslationMacroArgs`]. -/// -/// Represents errors that can occur while parsing the [`translation!()`] -/// macro input. This error is only used while parsing compile-time input, -/// as runtime input is validated in runtime. -/// -/// [`translation!()`]: crate::translation -#[derive(Error, Debug)] -enum MacroArgsError { - /// An error while parsing a compile-time String value - /// was found. - #[error("The literal '{0}' is an invalid ISO 639-1 string, and cannot be parsed")] - InvalidIsoLiteral(String), -} - -/// [`translation!()`] macro input arguments. -/// -/// This structure implements [`Parse`] to parse -/// [`translation!()`] macro arguments using -/// [`parse_macro_input`], to later be used -/// in the [`translation_macro`] function. -/// -/// [`translation!()`]: crate::translation -/// [`parse_macro_input`]: syn::parse_macro_input -/// [`translation_macro`]: crate::macro_generation::translation::translation_macro -pub struct TranslationMacroArgs { - /// Represents the user specified language - /// which may be static if the specified language - /// is a string literal or a `Language` enum tagged - /// union instance, otherwise dynamic and represented - /// as a `TokenStream`. - language: InputType, - - /// Represents a toml path to find the translation - /// object in the previously parsed TOML from the - /// translation files, this can be static if specified - /// as `static path::to::translation` or dynamic if - /// it's another expression, this way represented as a - /// [`TokenStream2`]. - path: InputType, - - /// Stores the replacement arguments for the translation - /// templates such as `Hello {name}` if found on a translation. - /// - /// If a call such as `a` is found, it will be implicitly - /// converted to `a = a` thus stored like so in the hash map. - replacements: HashMap, -} - -/// [`translation!()`] macro args parsing implementation. -/// -/// This implementation's purpose is to parse [`TokenStream`] -/// with the [`parse_macro_input`] macro. -impl Parse for TranslationMacroArgs { - fn parse(input: ParseStream) -> SynResult { - let parsed_language_arg = - match input.parse::()? { - Expr::Lit(ExprLit { lit: Lit::Str(literal), .. }) => { - match literal - .value() - .parse::() - { - Ok(language) => InputType::Static(language), - - Err(_) => Err(MacroArgsError::InvalidIsoLiteral(literal.value()) - .to_syn_error(literal))?, - } - }, - - other => InputType::Dynamic(other.into_token_stream()), - }; - - input.parse::()?; - - let parsed_path_arg = match input.parse::() { - Ok(_) => InputType::Static(input.parse::()?), - - Err(_) => InputType::Dynamic( - input - .parse::()? - .to_token_stream(), - ), - }; - - let mut replacements = HashMap::new(); - if input.peek(Token![,]) { - while !input.is_empty() { - input.parse::()?; - - if input.is_empty() { - break; - } - - let key = input.parse::()?; - let value = match input.parse::() { - Ok(_) => input - .parse::()? - .to_token_stream(), - - Err(_) => key - .clone() - .into_token_stream(), - }; - - replacements.insert(key, value); - } - } - - Ok(Self { - language: parsed_language_arg, - path: parsed_path_arg, - replacements, - }) - } -} - -impl TranslationMacroArgs { - /// `self.language` reference getter. - /// - /// **Returns** - /// A reference to `self.language` as [`InputType`]. - #[inline] - #[allow(unused)] - pub fn language(&self) -> &InputType { - &self.language - } - - /// `self.path` reference getter. - /// - /// **Returns** - /// A reference to `self.path` as [`InputType>`] - #[inline] - #[allow(unused)] - pub fn path(&self) -> &InputType { - &self.path - } - - /// `self.replacements` reference getter. - /// - /// **Returns** - /// A reference to `self.replacements` as [`HashMap`] - #[inline] - #[allow(unused)] - pub fn replacements(&self) -> &HashMap { - &self.replacements - } -} diff --git a/translatable_proc/src/macro_input/utils/input_type.rs b/translatable_proc/src/macro_input/utils/input_type.rs deleted file mode 100644 index 2754ac2..0000000 --- a/translatable_proc/src/macro_input/utils/input_type.rs +++ /dev/null @@ -1,61 +0,0 @@ -//! Input type abstraction for macro argument separation. -//! -//! This module defines the [`InputType`] enum, -//! which is used to distinguish between static -//! and dynamic values during macro input parsing. - -use proc_macro2::TokenStream as TokenStream2; -use quote::ToTokens; - -/// Input type differentiation enum. -/// -/// Represents whether an input is a static, -/// compile-time known value or a dynamic, -/// runtime expression. This differentiation -/// allows the translation system to apply -/// optimizations based on the input nature. -pub enum InputType { - /// Statically known value. - /// - /// The input is fully resolved at compile time, which allows - /// the macro system to optimize for constant substitution and - /// code simplification. - /// - /// **Parameters** - /// * `0` — The static value. - Static(T), - - /// Dynamically evaluated input. - /// - /// The input is represented as a [`TokenStream2`] expression, - /// which is evaluated at runtime rather than compile time. - /// - /// **Parameters** - /// * `0` — The dynamic [`TokenStream2`] expression. - Dynamic(TokenStream2), -} - -/// [`InputType`] runtime normalization implementation. -/// -/// This implementation is used to convert [`InputType`] -/// into normalized runtime values in many aspects, only -/// if T implements [`ToTokens`]. -impl InputType { - /// [`InputType`] to [`TokenStream2`] conversion. - /// - /// This method takes an [`InputType`] and converts - /// any of it's branches to a [`TokenStream2`] if - /// available. - /// - /// **Returns** - /// A [`TokenStream2`] representation of whatever the value - /// is in the [`InputType`]. - #[inline] - #[allow(unused)] - fn dynamic(self) -> TokenStream2 { - match self { - Self::Static(value) => value.to_token_stream(), - Self::Dynamic(value) => value, - } - } -} diff --git a/translatable_proc/src/macro_input/utils/mod.rs b/translatable_proc/src/macro_input/utils/mod.rs deleted file mode 100644 index d4a7ad5..0000000 --- a/translatable_proc/src/macro_input/utils/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod input_type; -pub mod translation_path; diff --git a/translatable_proc/src/macro_input/utils/translation_path.rs b/translatable_proc/src/macro_input/utils/translation_path.rs deleted file mode 100644 index 560ca30..0000000 --- a/translatable_proc/src/macro_input/utils/translation_path.rs +++ /dev/null @@ -1,152 +0,0 @@ -//! [`TranslationPath`] module. -//! -//! This module declares an abstraction -//! to parse [`syn::Path`] disallowing -//! generic type arguments. -//! -//! This module doesn't have anything -//! to do with [`std::path`]. - -use proc_macro2::Span; -use syn::parse::{Parse, ParseStream}; -use syn::spanned::Spanned; -use syn::{Error as SynError, Path, PathArguments, Result as SynResult}; - -/// Static translation path parser. -/// -/// This parser structure is an abstraction -/// of [`syn::Path`] but disallowing generic -/// types. -/// -/// The structure is spanned preserving -/// the original path unless defaulted, otherwise -/// the span is callsite. -/// -/// The structure is completly immutable. -#[derive(Clone)] -pub struct TranslationPath { - /// The path segments. - /// - /// The segments are translated - /// from a `syn::Path` as - /// x::y -> vec!["x", "y"]. - segments: Vec, - - /// The path original span - /// unless default, then empty. - span: Span, -} - -/// [`TranslationPath`] macro parsing implementation. -/// -/// Used to parse arguments with [`parse2`] or [`parse_macro_input!`] -/// within attribute arguments. -/// -/// [`parse2`]: syn::parse2 -/// [`parse_macro_input!`]: syn::parse_macro_input -impl Parse for TranslationPath { - fn parse(input: ParseStream) -> SynResult { - let path = input.parse::()?; - - let span = path.span(); - let segments = path - .segments - .into_iter() - .map(|segment| match segment.arguments { - PathArguments::None => Ok(segment - .ident - .to_string()), - - error => Err(SynError::new_spanned( - error, - "A translation path can't contain generic arguments.", - )), - }) - .collect::>()?; - - Ok(Self { segments, span }) - } -} - -/// Default implementation for [`TranslationPath`]. -/// -/// Used to create empty translation paths usually -/// for fallbacks with `Option::::unwrap_or_else()`. -/// -/// The span generated for a [`TranslationPath::default`] call is -/// [`Span::call_site`]. -impl Default for TranslationPath { - fn default() -> Self { - Self { - segments: Vec::new(), - span: Span::call_site(), - } - } -} - -impl TranslationPath { - /// Constructor function for [`TranslationPath`]. - /// - /// This constructor function should be called with - /// partial arguments from another function. Nothing - /// happens if it's not. - /// - /// **Arguments** - /// * `segments` - The segments this path is made of x::y -> vec!["x", "y"]. - /// * `span` - The original location or where this path should return errors - /// if it may. - /// - /// **Returns** - /// A constructed instance of [`TranslationPath`]. - #[inline] - pub fn new(segments: Vec, span: Span) -> Self { - Self { segments, span } - } - - /// Path merging helper method. - /// - /// This method takes both internal path segments and appends - /// both making a vector out of the merge. - /// - /// Since spans cannot be split or we may not have multiple - /// spans without having a complex structure then the span - /// is directly not preserved. - /// - /// **Arguments** - /// * `other` - The path this instance should be merged with. - /// - /// **Returns** - /// A single vector with both internal paths merged. - pub fn merge(&self, other: &Self) -> Vec { - // TODO: merge spans (not yet in #19) - [ - self.segments() - .to_vec(), - other - .segments() - .to_vec(), - ] - .concat() - } - - /// Internal segments getter. - /// - /// **Returns** - /// The internal segments. - #[inline] - #[allow(unused)] - pub fn segments(&self) -> &Vec { - &self.segments - } - - /// Internal span getter. - /// - /// **Returns** - /// The internal span. - #[inline] - #[allow(unused)] - pub fn span(&self) -> Span { - // TODO: possibly implement Spanned - self.span - } -} diff --git a/translatable_proc/src/parsing/config.rs b/translatable_proc/src/parsing/config.rs new file mode 100644 index 0000000..e69de29 diff --git a/translatable_proc/src/parsing/context.rs b/translatable_proc/src/parsing/context.rs new file mode 100644 index 0000000..e69de29 diff --git a/translatable_proc/src/parsing/mod.rs b/translatable_proc/src/parsing/mod.rs new file mode 100644 index 0000000..311a8a9 --- /dev/null +++ b/translatable_proc/src/parsing/mod.rs @@ -0,0 +1,4 @@ + +pub mod config; +pub mod context; +pub mod translation; diff --git a/translatable_proc/src/parsing/translation.rs b/translatable_proc/src/parsing/translation.rs new file mode 100644 index 0000000..e69de29 diff --git a/translatable_shared/Cargo.toml b/translatable_shared/Cargo.toml index de7b9b5..c0da7f1 100644 --- a/translatable_shared/Cargo.toml +++ b/translatable_shared/Cargo.toml @@ -9,9 +9,29 @@ edition = "2024" authors = ["Esteve Autet ", "Chiko "] [dependencies] -proc-macro2 = "1.0.95" -quote = "1.0.40" -strum = { version = "0.27.1", features = ["derive", "strum_macros"] } -syn = { version = "2.0.100", features = ["full"] } +edit-distance = "2.1.3" +strum = { version = "0.27.1", features = ["derive"] } thiserror = "2.0.12" -toml_edit = "0.22.26" +toml_edit = "0.22.27" + +# Preparsing +dyn-clone = { version = "1.0.19", optional = true } + +# Internal features +dyn_path = { version = "1.0.7", optional = true } +quote = { version = "1.0.40", optional = true } +syn = { version = "2.0.101", features = ["full"], optional = true } +proc-macro2 = { version = "1.0.95", optional = true } +walkdir = { version = "2.5.0", optional = true } + +[features] +default = [] +preparsing = ["dyn-clone"] + +internal = [ + "dyn_path", + "proc-macro2", + "quote", + "syn", + "walkdir" +] diff --git a/translatable_shared/src/lib.rs b/translatable_shared/src/lib.rs index ff03a65..97538ec 100644 --- a/translatable_shared/src/lib.rs +++ b/translatable_shared/src/lib.rs @@ -1,15 +1,9 @@ -//! Shared util declarations for `translatable` and `translatable_proc` +//! Translatable Shared Library //! -//! This crate shouldn't be used by itself, -//! since it contains macro generation code which -//! relies on references from the `translatable` library. -//! -//! The `translatable` library re-exports the utils -//! declared in this crate and exposes the necessary -//! ones. - -#![warn(missing_docs)] +//! This library works with the translatable crate. +//! It reexports common modules between the runtime crate +//! and the macro generation crate. -pub mod macros; -pub mod misc; -pub mod translations; +pub mod sources; +pub mod structures; +pub(crate) mod utils; diff --git a/translatable_shared/src/macros/collections.rs b/translatable_shared/src/macros/collections.rs deleted file mode 100644 index c1b09b3..0000000 --- a/translatable_shared/src/macros/collections.rs +++ /dev/null @@ -1,79 +0,0 @@ -//! Shared collection utils module. -//! -//! This module declares functions used by `translatable_proc` -//! and `translatable_shared` together, mostly used to convert -//! compile-time structures into runtime representations of -//! the same structures. - -use std::collections::HashMap; - -use proc_macro2::TokenStream as TokenStream2; -use quote::{ToTokens, quote}; - -/// [`HashMap`] runtime conversion. -/// -/// This function converts a [`HashMap`] into a [`TokenStream2`] -/// that when generated on a macro contains the same values as the initial -/// map. -/// -/// The type of the keys and values of the map must implement [`ToTokens`]. -/// -/// **Parameters** -/// * `map` - The map to convert into tokens. -/// -/// **Returns** -/// The provided `map` parameter represented as [`TokenStream2`]. -#[inline] -pub fn map_to_tokens(map: &HashMap) -> TokenStream2 { - let map = map - .iter() - .map(|(key, value)| { - let key = key.into_token_stream(); - let value = value.into_token_stream(); - - quote! { (#key, #value) } - }); - - quote! { - vec![#(#map),*] - .into_iter() - .collect::>() - } -} - -/// [`HashMap`] runtime conversion and mapping. -/// -/// Similarly to [`map_to_tokens`] this function converts a [`HashMap`] -/// into a [`TokenStream2`] that when generated on a macro contains the same -/// values as the original map. The difference is that in this function the keys -/// and values types don't need to implement [`ToTokens`], as this takes a -/// predicate which lets you modify values before converting it to tokens. -/// -/// The predicate must return a [`TokenStream2`] containing tuples, the internal -/// conversion is as `vec![$($converted),*]` collected into a [`HashMap`] -/// in runtime. -/// -/// **Parameters** -/// * `map` - The map to convert into tokens. -/// * `predicate` - A predicate taking a key and a value that should return a -/// [`TokenStream2`] -/// containing a tuple of the key and the value transformed in any way. -/// -/// **Returns** -/// The provided `map` parameter mutated with the `predicate` and converted to a -/// [`TokenStream2`]. -#[inline] -pub fn map_transform_to_tokens(map: &HashMap, predicate: F) -> TokenStream2 -where - F: Fn(&K, &V) -> TokenStream2, -{ - let processed = map - .iter() - .map(|(key, value)| predicate(key, value)); - - quote! { - vec![#(#processed),*] - .into_iter() - .collect::>() - } -} diff --git a/translatable_shared/src/macros/errors.rs b/translatable_shared/src/macros/errors.rs deleted file mode 100644 index 73be469..0000000 --- a/translatable_shared/src/macros/errors.rs +++ /dev/null @@ -1,111 +0,0 @@ -//! Error utils module. -//! -//! This module declares blanket implementations -//! for error utils such as conversion to tokens -//! or other errors. - -use std::fmt::Display; - -use proc_macro2::TokenStream as TokenStream2; -use quote::{ToTokens, quote}; -use syn::Error as SynError; - -/// Error implementations for macro outputs. -/// -/// This trait is meant to be implemented -/// as a blanket where every type that -/// implements [`Display`] can be converted -/// or either to a compile error or a [`SynError`]. -pub trait IntoCompileError -where - Self: Display + Sized, -{ - /// Convert error reference to runtime. - /// - /// Transforms the value into a string - /// and wraps [`compile_error!`] into it - /// for it to be returned when an error - /// happens. - /// - /// The invocation happens inside a method - /// for compatibility in both outside and - /// inside functions. - /// - /// **Returns** - /// A [`compile_error!`] wrapped `&str`. - #[cold] - fn to_compile_error(&self) -> TokenStream2 { - let message = self.to_string(); - quote! { std::compile_error!(#message) } - } - - /// Convert error reference to runtime. - /// - /// Calls [`to_compile_error`] but wraps that - /// invocation in a function, so errors outside - /// functions only target that specific error. - /// - /// **Returns** - /// A `fn __() {}` wrapped [`to_compile_error`] invocation. - /// - /// [`to_compile_error`]: IntoCompileError::to_compile_error - fn to_out_compile_error(&self) -> TokenStream2 { - let invocation = self.to_compile_error(); - quote! { fn __() { #invocation } } - } - - /// Convert error reference to a spanned [`SynError`]. - /// - /// Transforms the value into a string - /// and creates a spanned [`SynError`] - /// with the user provided span. - /// - /// **Parameters** - /// * `span` - the error span for the `rust-analyzer` report. - /// - /// **Returns** - /// A [`SynError`] with the value as a message and the provided `span`. - #[cold] - fn to_syn_error(&self, span: T) -> SynError { - SynError::new_spanned(span, self.to_string()) - } -} - -/// [`IntoCompileError`] blanket implementation -/// for values that implement [`Display`]. -impl IntoCompileError for T {} - -/// [`to_compile_error`] conversion helper macro. -/// -/// This macro takes a [`Result`] where -/// `E` implements [`Display`] and generates -/// a match branch which directly returns the error -/// as a compile error. -/// -/// This macro is meant to be called from a macro -/// generation function. -/// -/// If you prepend `out` to the value this will -/// call [`to_out_compile_error`] instead. -/// -/// [`to_compile_error`]: IntoCompileError::to_compile_error -/// [`to_out_compile_error`]: IntoCompileError::to_out_compile_error -#[macro_export] -macro_rules! handle_macro_result { - ($method:ident; $val:expr) => {{ - use $crate::macros::errors::IntoCompileError; - - match $val { - std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => return error.$method(), - } - }}; - - ($val:expr) => { - $crate::handle_macro_result!(to_compile_error; $val) - }; - - (out $val:expr) => { - $crate::handle_macro_result!(to_out_compile_error; $val) - }; -} diff --git a/translatable_shared/src/macros/mod.rs b/translatable_shared/src/macros/mod.rs deleted file mode 100644 index 885c4fc..0000000 --- a/translatable_shared/src/macros/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! Macro helpers module. -//! -//! This module contains sub-modules -//! which or either help converting -//! compile-time structures into their -//! runtime representations with [`TokenStream2`] -//! or any other utils to generate -//! runtime code. -//! -//! [`TokenStream2`]: proc_macro2::TokenStream - -pub mod collections; -pub mod errors; diff --git a/translatable_shared/src/misc/language.rs b/translatable_shared/src/misc/language.rs deleted file mode 100644 index d7fc925..0000000 --- a/translatable_shared/src/misc/language.rs +++ /dev/null @@ -1,583 +0,0 @@ -//! [`Language`] declaration module. -//! -//! This module declares all the implementations -//! required for parsing and validating ISO-639-1 -//! language strings from user input. - -use proc_macro2::{Span, TokenStream as TokenStream2}; -use quote::{ToTokens, TokenStreamExt, quote}; -use strum::{Display, EnumIter, EnumString}; -use syn::Ident; - -/// This implementation converts the tagged union -/// to an equivalent call from the runtime context. -/// -/// This is exclusively meant to be used from the -/// macro generation context. -impl ToTokens for Language { - fn to_tokens(&self, tokens: &mut TokenStream2) { - let ident = Ident::new(&format!("{self:?}"), Span::call_site()); - - tokens.append_all(quote! { translatable::shared::misc::language::Language::#ident }) - } -} - -/// ISO 639-1 language code implementation with validation -/// -/// Provides two-way mapping between language codes and names with: -/// - Case-insensitive parsing -/// - Strict validation -/// - Complete ISO 639-1 coverage -#[derive(Debug, Clone, EnumIter, Display, EnumString, Eq, Hash, PartialEq)] -#[strum(ascii_case_insensitive)] -pub enum Language { - #[allow(missing_docs)] - #[strum(serialize = "Abkhazian", serialize = "ab")] - AB, - #[allow(missing_docs)] - #[strum(serialize = "Afar", serialize = "aa")] - AA, - #[allow(missing_docs)] - #[strum(serialize = "Afrikaans", serialize = "af")] - AF, - #[allow(missing_docs)] - #[strum(serialize = "Akan", serialize = "ak")] - AK, - #[allow(missing_docs)] - #[strum(serialize = "Albanian", serialize = "sq")] - SQ, - #[allow(missing_docs)] - #[strum(serialize = "Amharic", serialize = "am")] - AM, - #[allow(missing_docs)] - #[strum(serialize = "Arabic", serialize = "ar")] - AR, - #[allow(missing_docs)] - #[strum(serialize = "Aragonese", serialize = "an")] - AN, - #[allow(missing_docs)] - #[strum(serialize = "Armenian", serialize = "hy")] - HY, - #[allow(missing_docs)] - #[strum(serialize = "Assamese", serialize = "as")] - AS, - #[allow(missing_docs)] - #[strum(serialize = "Avaric", serialize = "av")] - AV, - #[allow(missing_docs)] - #[strum(serialize = "Avestan", serialize = "ae")] - AE, - #[allow(missing_docs)] - #[strum(serialize = "Aymara", serialize = "ay")] - AY, - #[allow(missing_docs)] - #[strum(serialize = "Azerbaijani", serialize = "az")] - AZ, - #[allow(missing_docs)] - #[strum(serialize = "Bambara", serialize = "bm")] - BM, - #[allow(missing_docs)] - #[strum(serialize = "Bashkir", serialize = "ba")] - BA, - #[allow(missing_docs)] - #[strum(serialize = "Basque", serialize = "eu")] - EU, - #[allow(missing_docs)] - #[strum(serialize = "Belarusian", serialize = "be")] - BE, - #[allow(missing_docs)] - #[strum(serialize = "Bengali", serialize = "bn")] - BN, - #[allow(missing_docs)] - #[strum(serialize = "Bislama", serialize = "bi")] - BI, - #[allow(missing_docs)] - #[strum(serialize = "Bosnian", serialize = "bs")] - BS, - #[allow(missing_docs)] - #[strum(serialize = "Breton", serialize = "br")] - BR, - #[allow(missing_docs)] - #[strum(serialize = "Bulgarian", serialize = "bg")] - BG, - #[allow(missing_docs)] - #[strum(serialize = "Burmese", serialize = "my")] - MY, - #[allow(missing_docs)] - #[strum(serialize = "Catalan", serialize = "ca")] - CA, - #[allow(missing_docs)] - #[strum(serialize = "Chamorro", serialize = "ch")] - CH, - #[allow(missing_docs)] - #[strum(serialize = "Chechen", serialize = "ce")] - CE, - #[allow(missing_docs)] - #[strum(serialize = "Chichewa", serialize = "ny")] - NY, - #[allow(missing_docs)] - #[strum(serialize = "Chinese", serialize = "zh")] - ZH, - #[allow(missing_docs)] - #[strum(serialize = "Church Slavonic", serialize = "cu")] - CU, - #[allow(missing_docs)] - #[strum(serialize = "Chuvash", serialize = "cv")] - CV, - #[allow(missing_docs)] - #[strum(serialize = "Cornish", serialize = "kw")] - KW, - #[allow(missing_docs)] - #[strum(serialize = "Corsican", serialize = "co")] - CO, - #[allow(missing_docs)] - #[strum(serialize = "Cree", serialize = "cr")] - CR, - #[allow(missing_docs)] - #[strum(serialize = "Croatian", serialize = "hr")] - HR, - #[allow(missing_docs)] - #[strum(serialize = "Czech", serialize = "cs")] - CS, - #[allow(missing_docs)] - #[strum(serialize = "Danish", serialize = "da")] - DA, - #[allow(missing_docs)] - #[strum(serialize = "Divehi", serialize = "dv")] - DV, - #[allow(missing_docs)] - #[strum(serialize = "Dutch", serialize = "nl")] - NL, - #[allow(missing_docs)] - #[strum(serialize = "Dzongkha", serialize = "dz")] - DZ, - #[allow(missing_docs)] - #[strum(serialize = "English", serialize = "en")] - EN, - #[allow(missing_docs)] - #[strum(serialize = "Esperanto", serialize = "eo")] - EO, - #[allow(missing_docs)] - #[strum(serialize = "Estonian", serialize = "et")] - ET, - #[allow(missing_docs)] - #[strum(serialize = "Ewe", serialize = "ee")] - EE, - #[allow(missing_docs)] - #[strum(serialize = "Faroese", serialize = "fo")] - FO, - #[allow(missing_docs)] - #[strum(serialize = "Fijian", serialize = "fj")] - FJ, - #[allow(missing_docs)] - #[strum(serialize = "Finnish", serialize = "fi")] - FI, - #[allow(missing_docs)] - #[strum(serialize = "French", serialize = "fr")] - FR, - #[allow(missing_docs)] - #[strum(serialize = "Western Frisian", serialize = "fy")] - FY, - #[allow(missing_docs)] - #[strum(serialize = "Fulah", serialize = "ff")] - FF, - #[allow(missing_docs)] - #[strum(serialize = "Gaelic", serialize = "gd")] - GD, - #[allow(missing_docs)] - #[strum(serialize = "Galician", serialize = "gl")] - GL, - #[allow(missing_docs)] - #[strum(serialize = "Ganda", serialize = "lg")] - LG, - #[allow(missing_docs)] - #[strum(serialize = "Georgian", serialize = "ka")] - KA, - #[allow(missing_docs)] - #[strum(serialize = "German", serialize = "de")] - DE, - #[allow(missing_docs)] - #[strum(serialize = "Greek", serialize = "el")] - EL, - #[allow(missing_docs)] - #[strum(serialize = "Kalaallisut", serialize = "kl")] - KL, - #[allow(missing_docs)] - #[strum(serialize = "Guarani", serialize = "gn")] - GN, - #[allow(missing_docs)] - #[strum(serialize = "Gujarati", serialize = "gu")] - GU, - #[allow(missing_docs)] - #[strum(serialize = "Haitian", serialize = "ht")] - HT, - #[allow(missing_docs)] - #[strum(serialize = "Hausa", serialize = "ha")] - HA, - #[allow(missing_docs)] - #[strum(serialize = "Hebrew", serialize = "he")] - HE, - #[allow(missing_docs)] - #[strum(serialize = "Herero", serialize = "hz")] - HZ, - #[allow(missing_docs)] - #[strum(serialize = "Hindi", serialize = "hi")] - HI, - #[allow(missing_docs)] - #[strum(serialize = "Hiri Motu", serialize = "ho")] - HO, - #[allow(missing_docs)] - #[strum(serialize = "Hungarian", serialize = "hu")] - HU, - #[allow(missing_docs)] - #[strum(serialize = "Icelandic", serialize = "is")] - IS, - #[allow(missing_docs)] - #[strum(serialize = "Ido", serialize = "io")] - IO, - #[allow(missing_docs)] - #[strum(serialize = "Igbo", serialize = "ig")] - IG, - #[allow(missing_docs)] - #[strum(serialize = "Indonesian", serialize = "id")] - ID, - #[allow(missing_docs)] - #[strum(serialize = "Interlingua", serialize = "ia")] - IA, - #[allow(missing_docs)] - #[strum(serialize = "Interlingue", serialize = "ie")] - IE, - #[allow(missing_docs)] - #[strum(serialize = "Inuktitut", serialize = "iu")] - IU, - #[allow(missing_docs)] - #[strum(serialize = "Inupiaq", serialize = "ik")] - IK, - #[allow(missing_docs)] - #[strum(serialize = "Irish", serialize = "ga")] - GA, - #[allow(missing_docs)] - #[strum(serialize = "Italian", serialize = "it")] - IT, - #[allow(missing_docs)] - #[strum(serialize = "Japanese", serialize = "ja")] - JA, - #[allow(missing_docs)] - #[strum(serialize = "Javanese", serialize = "jv")] - JV, - #[allow(missing_docs)] - #[strum(serialize = "Kannada", serialize = "kn")] - KN, - #[allow(missing_docs)] - #[strum(serialize = "Kanuri", serialize = "kr")] - KR, - #[allow(missing_docs)] - #[strum(serialize = "Kashmiri", serialize = "ks")] - KS, - #[allow(missing_docs)] - #[strum(serialize = "Kazakh", serialize = "kk")] - KK, - #[allow(missing_docs)] - #[strum(serialize = "Central Khmer", serialize = "km")] - KM, - #[allow(missing_docs)] - #[strum(serialize = "Kikuyu", serialize = "ki")] - KI, - #[allow(missing_docs)] - #[strum(serialize = "Kinyarwanda", serialize = "rw")] - RW, - #[allow(missing_docs)] - #[strum(serialize = "Kyrgyz", serialize = "ky")] - KY, - #[allow(missing_docs)] - #[strum(serialize = "Komi", serialize = "kv")] - KV, - #[allow(missing_docs)] - #[strum(serialize = "Kongo", serialize = "kg")] - KG, - #[allow(missing_docs)] - #[strum(serialize = "Korean", serialize = "ko")] - KO, - #[allow(missing_docs)] - #[strum(serialize = "Kuanyama", serialize = "kj")] - KJ, - #[allow(missing_docs)] - #[strum(serialize = "Kurdish", serialize = "ku")] - KU, - #[allow(missing_docs)] - #[strum(serialize = "Lao", serialize = "lo")] - LO, - #[allow(missing_docs)] - #[strum(serialize = "Latin", serialize = "la")] - LA, - #[allow(missing_docs)] - #[strum(serialize = "Latvian", serialize = "lv")] - LV, - #[allow(missing_docs)] - #[strum(serialize = "Limburgan", serialize = "li")] - LI, - #[allow(missing_docs)] - #[strum(serialize = "Lingala", serialize = "ln")] - LN, - #[allow(missing_docs)] - #[strum(serialize = "Lithuanian", serialize = "lt")] - LT, - #[allow(missing_docs)] - #[strum(serialize = "Luba-Katanga", serialize = "lu")] - LU, - #[allow(missing_docs)] - #[strum(serialize = "Luxembourgish", serialize = "lb")] - LB, - #[allow(missing_docs)] - #[strum(serialize = "Macedonian", serialize = "mk")] - MK, - #[allow(missing_docs)] - #[strum(serialize = "Malagasy", serialize = "mg")] - MG, - #[allow(missing_docs)] - #[strum(serialize = "Malay", serialize = "ms")] - MS, - #[allow(missing_docs)] - #[strum(serialize = "Malayalam", serialize = "ml")] - ML, - #[allow(missing_docs)] - #[strum(serialize = "Maltese", serialize = "mt")] - MT, - #[allow(missing_docs)] - #[strum(serialize = "Manx", serialize = "gv")] - GV, - #[allow(missing_docs)] - #[strum(serialize = "Maori", serialize = "mi")] - MI, - #[allow(missing_docs)] - #[strum(serialize = "Marathi", serialize = "mr")] - MR, - #[allow(missing_docs)] - #[strum(serialize = "Marshallese", serialize = "mh")] - MH, - #[allow(missing_docs)] - #[strum(serialize = "Mongolian", serialize = "mn")] - MN, - #[allow(missing_docs)] - #[strum(serialize = "Nauru", serialize = "na")] - NA, - #[allow(missing_docs)] - #[strum(serialize = "Navajo", serialize = "nv")] - NV, - #[allow(missing_docs)] - #[strum(serialize = "North Ndebele", serialize = "nd")] - ND, - #[allow(missing_docs)] - #[strum(serialize = "South Ndebele", serialize = "nr")] - NR, - #[allow(missing_docs)] - #[strum(serialize = "Nepali", serialize = "ng")] - NG, - #[allow(missing_docs)] - #[strum(serialize = "Nepali", serialize = "ne")] - NE, - #[allow(missing_docs)] - #[strum(serialize = "Norwegian", serialize = "no")] - NO, - #[allow(missing_docs)] - #[strum(serialize = "Norwegian Bokmål", serialize = "nb")] - NB, - #[allow(missing_docs)] - #[strum(serialize = "Norwegian Nynorsk", serialize = "nn")] - NN, - #[allow(missing_docs)] - #[strum(serialize = "Occitan", serialize = "oc")] - OC, - #[allow(missing_docs)] - #[strum(serialize = "Ojibwa", serialize = "oj")] - OJ, - #[allow(missing_docs)] - #[strum(serialize = "Oriya", serialize = "or")] - OR, - #[allow(missing_docs)] - #[strum(serialize = "Oromo", serialize = "om")] - OM, - #[allow(missing_docs)] - #[strum(serialize = "Ossetian", serialize = "os")] - OS, - #[allow(missing_docs)] - #[strum(serialize = "Pali", serialize = "pi")] - PI, - #[allow(missing_docs)] - #[strum(serialize = "Pashto", serialize = "ps")] - PS, - #[allow(missing_docs)] - #[strum(serialize = "Persian", serialize = "fa")] - FA, - #[allow(missing_docs)] - #[strum(serialize = "Polish", serialize = "pl")] - PL, - #[allow(missing_docs)] - #[strum(serialize = "Portuguese", serialize = "pt")] - PT, - #[allow(missing_docs)] - #[strum(serialize = "Punjabi", serialize = "pa")] - PA, - #[allow(missing_docs)] - #[strum(serialize = "Quechua", serialize = "qu")] - QU, - #[allow(missing_docs)] - #[strum(serialize = "Romanian", serialize = "ro")] - RO, - #[allow(missing_docs)] - #[strum(serialize = "Romansh", serialize = "rm")] - RM, - #[allow(missing_docs)] - #[strum(serialize = "Rundi", serialize = "rn")] - RN, - #[allow(missing_docs)] - #[strum(serialize = "Russian", serialize = "ru")] - RU, - #[allow(missing_docs)] - #[strum(serialize = "North Sami", serialize = "se")] - SE, - #[allow(missing_docs)] - #[strum(serialize = "Samoan", serialize = "sm")] - SM, - #[allow(missing_docs)] - #[strum(serialize = "Sango", serialize = "sg")] - SG, - #[allow(missing_docs)] - #[strum(serialize = "Sanskrit", serialize = "sa")] - SA, - #[allow(missing_docs)] - #[strum(serialize = "Sardinian", serialize = "sc")] - SC, - #[allow(missing_docs)] - #[strum(serialize = "Serbian", serialize = "sr")] - SR, - #[allow(missing_docs)] - #[strum(serialize = "Shona", serialize = "sn")] - SN, - #[allow(missing_docs)] - #[strum(serialize = "Sindhi", serialize = "sd")] - SD, - #[allow(missing_docs)] - #[strum(serialize = "Sinhala", serialize = "si")] - SI, - #[allow(missing_docs)] - #[strum(serialize = "Slovak", serialize = "sk")] - SK, - #[allow(missing_docs)] - #[strum(serialize = "Slovenian", serialize = "sl")] - SL, - #[allow(missing_docs)] - #[strum(serialize = "Somali", serialize = "so")] - SO, - #[allow(missing_docs)] - #[strum(serialize = "Southern Sotho", serialize = "st")] - ST, - #[allow(missing_docs)] - #[strum(serialize = "Spanish", serialize = "es")] - ES, - #[allow(missing_docs)] - #[strum(serialize = "Sundanese", serialize = "su")] - SU, - #[allow(missing_docs)] - #[strum(serialize = "Swahili", serialize = "sw")] - SW, - #[allow(missing_docs)] - #[strum(serialize = "Swati", serialize = "ss")] - SS, - #[allow(missing_docs)] - #[strum(serialize = "Swedish", serialize = "sv")] - SV, - #[allow(missing_docs)] - #[strum(serialize = "Tagalog", serialize = "tl")] - TL, - #[allow(missing_docs)] - #[strum(serialize = "Tahitian", serialize = "ty")] - TY, - #[allow(missing_docs)] - #[strum(serialize = "Tajik", serialize = "tg")] - TG, - #[allow(missing_docs)] - #[strum(serialize = "Tamil", serialize = "ta")] - TA, - #[allow(missing_docs)] - #[strum(serialize = "Tatar", serialize = "tt")] - TT, - #[allow(missing_docs)] - #[strum(serialize = "Telugu", serialize = "te")] - TE, - #[allow(missing_docs)] - #[strum(serialize = "Thai", serialize = "th")] - TH, - #[allow(missing_docs)] - #[strum(serialize = "Tibetan", serialize = "bo")] - BO, - #[allow(missing_docs)] - #[strum(serialize = "Tigrinya", serialize = "ti")] - TI, - #[allow(missing_docs)] - #[strum(serialize = "Tonga", serialize = "to")] - TO, - #[allow(missing_docs)] - #[strum(serialize = "Tsonga", serialize = "ts")] - TS, - #[allow(missing_docs)] - #[strum(serialize = "Tswana", serialize = "tn")] - TN, - #[allow(missing_docs)] - #[strum(serialize = "Turkish", serialize = "tr")] - TR, - #[allow(missing_docs)] - #[strum(serialize = "Turkmen", serialize = "tk")] - TK, - #[allow(missing_docs)] - #[strum(serialize = "Twi", serialize = "tw")] - TW, - #[allow(missing_docs)] - #[strum(serialize = "Uighur", serialize = "ug")] - UG, - #[allow(missing_docs)] - #[strum(serialize = "Ukrainian", serialize = "uk")] - UK, - #[allow(missing_docs)] - #[strum(serialize = "Urdu", serialize = "ur")] - UR, - #[allow(missing_docs)] - #[strum(serialize = "Uzbek", serialize = "uz")] - UZ, - #[allow(missing_docs)] - #[strum(serialize = "Venda", serialize = "ve")] - VE, - #[allow(missing_docs)] - #[strum(serialize = "Vietnamese", serialize = "vi")] - VI, - #[allow(missing_docs)] - #[strum(serialize = "Volapük", serialize = "vo")] - VO, - #[allow(missing_docs)] - #[strum(serialize = "Walloon", serialize = "wa")] - WA, - #[allow(missing_docs)] - #[strum(serialize = "Welsh", serialize = "cy")] - CY, - #[allow(missing_docs)] - #[strum(serialize = "Wolof", serialize = "wo")] - WO, - #[allow(missing_docs)] - #[strum(serialize = "Xhosa", serialize = "xh")] - XH, - #[allow(missing_docs)] - #[strum(serialize = "Sichuan Yi", serialize = "ii")] - II, - #[allow(missing_docs)] - #[strum(serialize = "Yiddish", serialize = "yi")] - YI, - #[allow(missing_docs)] - #[strum(serialize = "Yoruba", serialize = "yo")] - YO, - #[allow(missing_docs)] - #[strum(serialize = "Zhuang", serialize = "za")] - ZA, - #[allow(missing_docs)] - #[strum(serialize = "Zulu", serialize = "zu")] - ZU, -} diff --git a/translatable_shared/src/misc/mod.rs b/translatable_shared/src/misc/mod.rs deleted file mode 100644 index 778f7c6..0000000 --- a/translatable_shared/src/misc/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -//! Uncategorized item module. -//! -//! This module contains sub-modules with miscellaneous structures, -//! or items that don't fit into an existing category — typically -//! because there aren’t enough related modules to justify their own group. - -pub mod language; -pub mod templating; diff --git a/translatable_shared/src/misc/templating.rs b/translatable_shared/src/misc/templating.rs deleted file mode 100644 index 08c6bee..0000000 --- a/translatable_shared/src/misc/templating.rs +++ /dev/null @@ -1,236 +0,0 @@ -//! String template generation module. -//! -//! This module declares the [`FormatString`] -//! which is a structure to parse templates -//! and generate strings of them with replaced -//! parameters. - -use std::collections::HashMap; -use std::ops::Range; -use std::str::FromStr; - -use proc_macro2::TokenStream as TokenStream2; -use quote::{ToTokens, TokenStreamExt, quote}; -use syn::{Ident, parse_str}; -use thiserror::Error; - -/// Template parsing errors. -/// -/// This error is used within [`FormatString`] -/// to represent parsing errors such as unclosed -/// unescaped tags or invalid identifiers. -#[derive(Error, Debug)] -pub enum TemplateError { - /// Unclosed brace error. - /// - /// This error is returned when a brace - /// that was considered unescaped - /// was not closed after reaching the - /// last character of the string. - #[error("Found unclosed brace at index {0}")] - Unclosed(usize), - - /// Invalid ident error. - /// - /// This error is returned when a key - /// inside the braces couldn't be parsed - /// as an [`Ident`], invalid identifiers - /// are checked because of macro parsing. - #[error("Found template with key '{0}' which is an invalid identifier")] - InvalidIdent(String), -} - -/// Format string wrapper struct. -/// -/// This struct wraps a string and has -/// a counter of each template it has -/// with each respective position for -/// the sake of replacing these positions -/// with read data. -pub struct FormatString { - /// Original templated string. - /// - /// This field contains the original - /// string that aligns it's keyed templates - /// with `self.spans`. - /// - /// This should never be mutated for the sake - /// of keeping the alignment with `self.spans`. - original: String, - - /// Template spans. - /// - /// This vector contains the spans - /// of all the ranges containing a template - /// in the original string. - /// - /// This is stored in a vector because we - /// want to allow multiple templates with - /// the same key. - spans: Vec<(String, Range)>, -} - -impl FormatString { - /// Compile-time to runtime transformation function. - /// - /// This function takes data that may be generated - /// from a macro output and constructs an instance - /// of [`FormatString`] keeping its fields - /// private an immutable. - /// - /// If you use this to construct the instance manually - /// there is no promise that the string and spans - /// are aligned, thus the replacements are going - /// to work. - /// - /// **Parameters** - /// * `original` - What belongs to the `original` field. - /// * `spans` - What belongs to the `spans` field. - /// - /// **Returns** - /// An instance of self based on the provided parameters. - pub fn from_data(original: &str, spans: Vec<(String, Range)>) -> Self { - Self { original: original.to_string(), spans } - } - - /// Creates replaced original string copy. - /// - /// This method takes the original string, and replaces - /// it's templates with the values of the values provided - /// as a hashmap. - /// - /// **Parameters** - /// * `values` - The values to replace the templates with. - /// - /// **Returns** - /// A copy of the original string with it's templates replaced. - pub fn replace_with(&self, values: &HashMap) -> String { - let mut original = self - .original - .clone(); - - let mut spans = self - .spans - .clone(); - spans.sort_by_key(|(_key, range)| range.start); - - let mut offset = 0isize; - - for (key, range) in spans { - if let Some(value) = values.get(&key) { - let start = (range.start as isize + offset) as usize; - let end = (range.end as isize + offset) as usize; - - original.replace_range(start..end, value); - - offset += value.len() as isize - (range.end - range.start) as isize; - } - } - - original - } - - /// Original string getter. - /// - /// **Returns** - /// A shared slice to the original string. - pub fn original(&self) -> &str { - &self.original - } -} - -/// Parse method implementation. -/// -/// This implementation leads to the implementation -/// of the `parse` method for [`FormatString`] which -/// parses all the templates on the string and stores -/// them in a structure along the original string for -/// future replacement. -impl FromStr for FormatString { - type Err = TemplateError; - - fn from_str(s: &str) -> Result { - let original = s.to_string(); - let mut spans = Vec::new(); - - let char_to_byte = s - .char_indices() - .map(|(i, _)| i) - .collect::>(); - - let mut last_bracket_idx = None; - let mut current_tmpl_key = String::new(); - for (char_idx, c) in original - .chars() - .enumerate() - { - match (c, last_bracket_idx) { - // if last template index is the last character - // ignore current as is escaped. - ('{', Some(prev)) if prev == char_idx.saturating_sub(1) => last_bracket_idx = None, - // if last template index is anything but the last character - // set it as last index. - ('{', _) => last_bracket_idx = Some(char_idx), - - // if last template index is not 0 and we find - // a closing bracket complete a range. - ('}', Some(open_idx)) => { - let key = current_tmpl_key.clone(); - - spans.push(( - parse_str::(&key) - .map_err(|_| TemplateError::InvalidIdent(key))? - .to_string(), - char_to_byte[open_idx] - ..char_to_byte - .get(char_idx + 1) - .copied() - .unwrap_or_else(|| s.len()), - )); - - last_bracket_idx = None; - current_tmpl_key.clear(); - }, - - (c, Some(_)) => current_tmpl_key.push(c), - - _ => {}, - } - } - - if let Some(lbi) = last_bracket_idx { - Err(TemplateError::Unclosed(lbi)) - } else { - Ok(FormatString { original, spans }) - } - } -} - -/// Compile-time to runtime conversion implementation. -/// -/// This implementation generates a call to the [`from_data`] -/// function in [`FormatString`]. -/// -/// [`from_data`]: FormatString::from_data -impl ToTokens for FormatString { - fn to_tokens(&self, tokens: &mut TokenStream2) { - let original = &self.original; - - let span_map = self - .spans - .iter() - .map(|(key, range)| { - let start = range.start; - let end = range.end; - - quote! { (#key.to_string(), #start..#end) } - }); - - tokens.append_all(quote! { - translatable::shared::misc::templating::FormatString::from_data( - #original, - vec![#(#span_map),*] - ) - }); - } -} diff --git a/translatable_shared/src/sources/config.rs b/translatable_shared/src/sources/config.rs new file mode 100644 index 0000000..5a7666b --- /dev/null +++ b/translatable_shared/src/sources/config.rs @@ -0,0 +1,137 @@ +#![cfg(feature = "internal")] + +use std::fs::read_to_string; +use std::env::var; +use std::path::Path; +use std::sync::OnceLock; +use std::io::Error as IoError; + +use thiserror::Error; +use toml_edit::{TomlError, DocumentMut}; +use dyn_path::{dyn_access, dyn_path}; + +use crate::structures::file_related_error::FileRelatedError; +use crate::structures::file_position::FileLocation; +use crate::structures::language::{Language, LanguageError}; + +#[derive(Error, Debug)] +pub enum ConfigErrorDescription { + #[error("Couldn't open configuration file.\n{0:#}")] + SystemIo(IoError), + + #[error("Error while parsing configuration TOML file.\n{0:#}")] + ParseToml(TomlError), + + #[error( + r#" + Couldn't parse configuration, expected "{item_path}" + to be of a type that could be parsed into a '{expected_type}', + but found a value which can't be parsed to that type. + {error_display} + "#, + )] + ParseValue { + item_path: String, + expected_type: String, + error_display: String + } +} + +pub(super) type ConfigError = FileRelatedError; + +pub struct Config { + // sources section + sources_path: String, // where to get the translations from. + + // fallback section + fallback_language: Option, // if the language does not exist, use this. + fallback_translation: Option // if unavailable language or translation use this. +} + +impl Config { + pub fn load() -> Result { + let file_path = Path::new( + &var("TRANSLATABLE_CONFIG_PATH") + .unwrap_or_else(|_| "./translatable.toml".into()) + ) + .to_path_buf(); + + let file_path = file_path + .canonicalize() + .map_err(|error| ConfigError { + description: ConfigErrorDescription::SystemIo(error), + file_path: Some(file_path), + at_character: None + })?; + + let file_contents = read_to_string(&file_path) + .map_err(|error| ConfigError { + description: ConfigErrorDescription::SystemIo(error), + file_path: Some(file_path.to_path_buf()), + at_character: None + })?; + + let table = file_contents + .parse::() + .map_err(|error| ConfigError { + description: ConfigErrorDescription::ParseToml(error.clone()), + file_path: Some(file_path.to_path_buf()), + at_character: Some( + FileLocation::from_optional_range(&file_contents, error.span()) + ) + })?; + + Ok(Self { + sources_path: dyn_access!(table.sources.path) + .and_then(|path| path.as_str()) + .unwrap_or("./translations") + .to_string(), + + fallback_language: (|| { + let (raw_str, raw_span) = dyn_access!(table.fallbacks.language) + .and_then(|raw| Some((raw.as_str()?, raw.span())))?; + + Some(raw_str + .parse() + .map_err(|error: LanguageError| ConfigError { + description: ConfigErrorDescription::ParseValue { + item_path: dyn_path!(fallbacks.language), + expected_type: "::translatable::prelude::Language".into(), + error_display: error.to_string() + }, + file_path: Some(file_path.to_path_buf()), + at_character: Some(FileLocation::from_optional_range(&file_contents, raw_span)) + })) + })() + .transpose()?, + + fallback_translation: dyn_access!(table.fallbacks.translation) + .and_then(|raw| raw.as_str()) + .map(ToString::to_string) + }) + } + + pub fn load_cached() -> Result<&'static Self, ConfigError> { + static CACHED: OnceLock = OnceLock::new(); + + if let Some(cached) = CACHED.get() { + return Ok(cached); + } + + let loaded = Self::load()?; + + Ok(CACHED.get_or_init(|| loaded)) + } + + pub fn sources_path(&self) -> &str { + &self.sources_path + } + + pub const fn fallback_language(&self) -> Option { + self.fallback_language + } + + pub const fn fallback_translation(&self) -> Option<&String> { + self.fallback_translation.as_ref() + } +} diff --git a/translatable_shared/src/sources/mod.rs b/translatable_shared/src/sources/mod.rs new file mode 100644 index 0000000..d40b299 --- /dev/null +++ b/translatable_shared/src/sources/mod.rs @@ -0,0 +1,2 @@ +pub mod config; +pub mod translations; diff --git a/translatable_shared/src/sources/translations.rs b/translatable_shared/src/sources/translations.rs new file mode 100644 index 0000000..623cb7b --- /dev/null +++ b/translatable_shared/src/sources/translations.rs @@ -0,0 +1,100 @@ +use std::collections::hash_map::Entry; +use std::path::PathBuf; +use std::collections::HashMap; + +#[cfg(feature = "internal")] +use std::sync::OnceLock; + +#[cfg(feature = "internal")] +use walkdir::WalkDir; + +use crate::structures::translation_tree::TranslationTree; + +#[cfg(feature = "internal")] +use crate::{ + sources::config::{Config, ConfigError}, + structures::translation_tree::TranslationTreeParseError +}; + +enum TranslationTreeSource { + File(PathBuf), + Raw(String), + + #[cfg(feature = "preparsing")] + Error(TranslationTreeParseError) +} + +pub struct TranslationTreeBuilder { + sources: HashMap, TranslationTreeSource> +} + +impl TranslationTreeBuilder { + #[inline(always)] + pub fn new() -> Self { + Self { + sources: HashMap::new() + } + } + + pub fn with_file_source(&mut self, path: Vec, file: PathBuf) -> &mut Self { + self + .sources + .insert(path, TranslationTreeSource::File(file)); + + self + } + + pub fn with_raw_source(&mut self, path: Vec, contents: String) -> &mut Self { + self + .sources + .insert(path, TranslationTreeSource::Raw(contents)); + + self + } + + #[cfg(feature = "preparsing")] + pub fn with_error_source(&mut self, path: Vec, error: TranslationTreeParseError) -> &mut Self { + unimplemented!() + } + + pub fn build(self) -> TranslationTree { + let mut root = HashMap::::new(); + + for (path, source) in self.sources { + let mut cursor = &mut root; + let mut segments = path.iter().peekable(); + let tree = match source { + #[cfg(feature = "preparsing")] + TranslationTreeSource::Error(error) => TranslationTree::NestingError(error), + + TranslationTreeSource::File(path) => TranslationTree::from(path.as_path()), + TranslationTreeSource::Raw(raw) => TranslationTree::from(raw.as_str()), + }; + + while let Some(segment) = segments.next() { + if segments.peek().is_some() { + cursor = match cursor.entry(segment.clone()) { + Entry::Occupied(entry) => { + match entry.into_mut() { + TranslationTree::Nesting(map) => map, + _ => unreachable!("Non-nesting node found when guaranted.") + } + } + + Entry::Vacant(entry) => { + match entry.insert(TranslationTree::empty_nesting()) { + TranslationTree::Nesting(map) => map, + _ => unreachable!("Non-nesting node found when guaranted.") + } + } + } + } else { + cursor.insert(segment.clone(), tree); + break; + } + } + } + + TranslationTree::Nesting(root) + } +} diff --git a/translatable_shared/src/structures/file_position.rs b/translatable_shared/src/structures/file_position.rs new file mode 100644 index 0000000..a9c9577 --- /dev/null +++ b/translatable_shared/src/structures/file_position.rs @@ -0,0 +1,101 @@ +use std::ops::Range; +use std::fmt::{Display, Formatter, Result as FmtResult}; + +#[cfg(feature = "internal")] +use ::{ + proc_macro2::TokenStream, + quote::{quote, ToTokens} +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct FileLocation { + line: usize, + column: usize +} + +impl FileLocation { + #[inline(always)] + pub const fn new(line: usize, column: usize) -> Self { + Self { line, column } + } + + pub const fn zero() -> Self { + Self { line: 0, column: 0 } + } + + #[inline(always)] + pub const fn line(&self) -> usize { + self.line + } + + #[inline(always)] + pub const fn column(&self) -> usize { + self.column + } + + pub fn from_byte_index(text: &str, index: usize) -> Option { + if index >= text.len() { + return None; + } + + text.lines() + .enumerate() + .fold((0, None), |(offset, result), (line_num, line)| { + if result.is_some() { + return (offset, result); + } + + let line_len = line.len() + 1; + if index < offset + line_len { + (offset, Some((line_num, index - offset))) + } else { + (offset + line_len, None) + } + }) + .1 + .map(Into::into) + } + + pub fn from_optional_range(text: &str, span: Option>) -> Self { + Self::from_byte_index( + text, + span.map_or(0, |span| span.start) + ) + .unwrap_or_else(Self::zero) + } +} + +impl, C: Into> From<(L, C)> for FileLocation { + #[inline(always)] + fn from((line, column): (L, C)) -> Self { + Self { + line: line.into(), + column: column.into() + } + } +} + +impl Into<(usize, usize)> for FileLocation { + #[inline(always)] + fn into(self) -> (usize, usize) { + (self.line, self.column) + } +} + +impl Display for FileLocation { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!(f, "{}:{}", self.line, self.column) + } +} + +#[cfg(feature = "internal")] +impl ToTokens for FileLocation { + fn to_tokens(&self, tokens: &mut TokenStream) { + let line = self.line; + let column = self.column; + + tokens.extend(quote! { + ::translatable::prelude::LocationInFile::new(#line, #column) + }) + } +} diff --git a/translatable_shared/src/structures/file_related_error.rs b/translatable_shared/src/structures/file_related_error.rs new file mode 100644 index 0000000..c730343 --- /dev/null +++ b/translatable_shared/src/structures/file_related_error.rs @@ -0,0 +1,137 @@ +use std::path::PathBuf; +use std::fmt::{Debug, Display, Formatter, Result as FmtResult}; +use std::error::Error; + +#[cfg(feature = "internal")] +use ::{ + proc_macro2::TokenStream, + quote::{ToTokens, quote}, +}; + +use crate::structures::file_position::FileLocation; + +#[cfg(feature = "internal")] +use crate::utils::internal::{option_stream, path_to_tokens}; + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct FileRelatedError { + pub(crate) description: TDesc, + pub(crate) file_path: Option, + pub(crate) at_character: Option +} + +impl FileRelatedError { + #[inline(always)] + #[doc(hidden)] + pub const fn from_data( + description: TDesc, + file_path: Option, + at_character: Option + ) -> Self { + Self { + description, + file_path, + at_character + } + } + + #[cfg(feature = "preparsing")] + #[inline(always)] + pub const fn with_desc_only(description: TDesc) -> Self { + Self::from_data(description, None, None) + } + + #[cfg(feature = "preparsing")] + #[inline(always)] + pub const fn with_desc_and_path(description: TDesc, file_path: Option) -> Self { + Self::from_data(description, file_path, None) + } + + // NOTE: binding for public API + #[cfg(feature = "preparsing")] + #[inline(always)] + pub const fn complete( + description: TDesc, + file_path: Option, + at_character: Option + ) -> Self { + Self::from_data( + description, + file_path, + at_character + ) + } + + #[inline(always)] + pub fn description(&self) -> &TDesc { + &self.description + } + + #[inline(always)] + pub fn file_path(&self) -> Option<&PathBuf> { + self.file_path.as_ref() + } + + #[inline(always)] + pub fn at_character(&self) -> &Option { + &self.at_character + } +} + +impl Display for FileRelatedError { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!( + f, + "{:#}", + self.description + )?; + + let file_path = self.file_path() + .and_then(|path| path.to_str()); + let at_character = self.at_character + .map(|position| position.to_string()); + + match (file_path, at_character) { + (Some(file_path), Some(at_character)) => write!( + f, "\nAt {file_path}:{at_character}." + )?, + + (Some(file_path), None) => write!( + f, "\nAt {file_path}." + )?, + + _ => {} + }; + + Ok(()) + } +} + +#[cfg(feature = "internal")] +impl ToTokens for FileRelatedError { + fn to_tokens(&self, tokens: &mut TokenStream) { + let description = &self.description; + let file_path = option_stream(&self.file_path().map(|p| path_to_tokens(p))); + let at_character = option_stream(&self.at_character); + + tokens.extend(quote! { + ::translatable::prelude::TranslationparseError::new( + #description, + ::std::path::PathBuf::from(#file_path.to_string()), + #at_character + ) + }) + } +} + +impl Error for FileRelatedError {} + +impl Clone for FileRelatedError { + fn clone(&self) -> Self { + Self { + description: self.description.clone(), + file_path: self.file_path.clone(), + at_character: self.at_character + } + } +} diff --git a/translatable_shared/src/structures/inline_meta_variable.rs b/translatable_shared/src/structures/inline_meta_variable.rs new file mode 100644 index 0000000..c94dba4 --- /dev/null +++ b/translatable_shared/src/structures/inline_meta_variable.rs @@ -0,0 +1,40 @@ +#![cfg(feature = "internal")] + +use proc_macro2::TokenStream; +use quote::ToTokens; +use syn::{parse::{Parse, ParseStream}, parse2, Expr, Ident, Result as SynResult, Token}; + +pub struct InlineMetaVariable { + key: String, + value: TokenStream +} + +impl InlineMetaVariable { + #[inline(always)] + pub fn key(&self) -> &str { + &self.key + } + + #[inline(always)] + pub fn value(&self) -> TokenStream { + self.value.clone() + } + + #[inline(always)] + pub fn value_as(&self) -> SynResult { + parse2(self.value.clone()) + } +} + +impl Parse for InlineMetaVariable { + fn parse(input: ParseStream) -> SynResult { + let key = input.parse::()?; + let value = if input.parse::().is_ok() { + input.parse::()?.to_token_stream() + } else { + key.to_token_stream() + }; + + Ok(Self { key: key.to_string(), value }) + } +} diff --git a/translatable_shared/src/structures/item_type.rs b/translatable_shared/src/structures/item_type.rs new file mode 100644 index 0000000..d05d5ea --- /dev/null +++ b/translatable_shared/src/structures/item_type.rs @@ -0,0 +1,18 @@ +#![cfg(feature = "internal")] + +use proc_macro2::TokenStream; +use quote::ToTokens; + +pub enum ItemType { + Static(T), + Dynamic(TokenStream) +} + +impl ItemType { + pub fn dynamic(self) -> TokenStream { + match self { + Self::Static(s) => s.into_token_stream(), + Self::Dynamic(d) => d + } + } +} diff --git a/translatable_shared/src/structures/language.rs b/translatable_shared/src/structures/language.rs new file mode 100644 index 0000000..89ca440 --- /dev/null +++ b/translatable_shared/src/structures/language.rs @@ -0,0 +1,749 @@ +use std::str::FromStr; +use std::fmt::{Display, Formatter, Result as FmtResult}; +use std::error::Error; + +#[cfg(feature = "internal")] +use std::fmt::Write; + +use edit_distance::edit_distance; +use strum::{EnumIter, EnumProperty, IntoEnumIterator}; + +#[cfg(feature = "internal")] +use ::{ + proc_macro2::{Span, TokenStream, TokenTree}, + quote::{format_ident, quote, ToTokens}, + syn::parse::{Parse, ParseStream}, + syn::{Result as SynResult, Error as SynError} +}; + +#[cfg(feature = "internal")] +use crate::utils::internal::option_stream; + +#[derive(Debug, Clone)] +pub struct LanguageError { + attempt: String, + closest: Option +} + +impl LanguageError { + pub fn from_data(attempt: String, closest: Option) -> Self { + Self { + attempt, + closest + } + } + + #[inline(always)] + #[cold] + pub fn attempt(&self) -> &str { + &self.attempt + } + + #[inline(always)] + #[cold] + pub const fn closest(&self) -> Option<&String> { + self.closest.as_ref() + } +} + +#[cfg(feature = "internal")] +impl ToTokens for LanguageError { + fn to_tokens(&self, tokens: &mut TokenStream) { + let attempt = &self.attempt; + let closest = option_stream(&self.closest); + + tokens.extend( + quote! { + ::translatable::prelude::LanguageError::from_data( + #attempt.to_string(), + #closest + ) + } + ) + } +} + +impl Display for LanguageError { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!(f, r#""{}" is not a valid language"#, self.attempt)?; + + match &self.closest { + Some(closest) => write!(f, r#", perhaps you meant "{closest}"?"#), + None => write!(f, ".") + } + } +} + +impl Error for LanguageError {} + +impl Language { + fn possible_idents(&self) -> Vec { + let mut result = self.get_str("Alternatives") + .map(|alts| alts + .split(",") + .map(|a| a.trim().to_string()) + .collect() + ) + .unwrap_or_else(Vec::new); + + if let Some(name) = self.get_str("Name") { + result.push(name.to_string()); + } + + result.push(format!("{self:?}")); + + result + } + + #[cold] + fn close_to(attempt: &str) -> Option { + Self::iter() + .flat_map(|curr| curr.possible_idents()) + .min_by_key(|candidate| edit_distance(attempt, &candidate)) + } +} + +#[cfg(feature = "internal")] +impl Parse for Language { + fn parse(input: ParseStream) -> SynResult { + let mut raw = String::new(); + let mut spans = Vec::new(); + + while !input.is_empty() { + match input.parse::()? { + TokenTree::Ident(ident) => { + let _ = write!(raw, "{ident:#}"); + spans.push(ident.span()); + }, + + TokenTree::Punct(punct) => match punct.as_char() { + ',' | ';' => break, + other => { + let _ = write!(raw, "{other:#}"); + spans.push(punct.span()); + } + }, + + other => { + return Err(SynError::new_spanned( + other, + "Only sequences of identifiers and punctuations are allowed while parsing a Language." + )) + } + } + } + + match raw.parse::() { + Ok(language) => Ok(language), + Err(err) => { + let overall_span = { + let mut span_iter = spans.into_iter(); + + span_iter.next() + .map(|first| span_iter + .fold(first, |acc, span| acc.join(span).unwrap_or(acc)) + ) + .unwrap_or(Span::call_site()) + }; + + Err(SynError::new(overall_span, err)) + } + } + } +} + +impl FromStr for Language { + type Err = LanguageError; + + fn from_str(s: &str) -> Result { + Self::iter() + .find(|item| item + .possible_idents() + .iter() + .any(|ident| ident == &s) + ) + .ok_or_else(|| LanguageError { + attempt: s.to_string(), + closest: Language::close_to(s) + }) + } +} + +#[cfg(feature = "internal")] +impl ToTokens for Language { + #[inline(always)] + fn to_tokens(&self, tokens: &mut TokenStream) { + let mut target_lang = String::with_capacity(2); + let _ = write!(&mut target_lang, "{self:?}"); + + let ident = format_ident!("{target_lang:?}"); + + tokens.extend( + quote! { ::translatable::prelude::Language::#ident } + ); + } +} + +impl Display for Language { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self.get_str("Name") { + Some(name) => write!(f, "{name}"), + None => write!(f, "{self:?}") + } + } +} + +#[derive(EnumProperty, EnumIter, Debug, Clone, Copy, Eq, Hash, PartialEq)] +pub enum Language { + #[strum(props(Name = "Afar", Alternatives = "Afaraf"))] + Aa, + + #[strum(props(Name = "Abkhaz", Alternatives = "аҧсшәа"))] + Ab, + + #[strum(props(Name = "Avestan", Alternatives = "Avesta"))] + Ae, + + #[strum(props(Name = "Afrikaans"))] + Af, + + #[strum(props(Name = "Akan"))] + Ak, + + #[strum(props(Name = "Amharic", Alternatives = "አማርኛ"))] + Am, + + #[strum(props(Name = "Aragonese", Alternatives = "aragonés"))] + An, + + #[strum(props(Name = "Arabic", Alternatives = "العربية"))] + Ar, + + #[strum(props(Name = "Assamese", Alternatives = "অসমীয়া"))] + As, + + #[strum(props(Name = "Avaric", Alternatives = "авар мацӀ, магӀарул мацӀ"))] + Av, + + #[strum(props(Name = "Aymara", Alternatives = "aymar aru"))] + Ay, + + #[strum(props(Name = "Azerbaijani", Alternatives = "azərbaycan dili"))] + Az, + + #[strum(props(Name = "Bashkir", Alternatives = "башҡорт теле"))] + Ba, + + #[strum(props(Name = "Belarusian", Alternatives = "беларуская мова"))] + Be, + + #[strum(props(Name = "Bulgarian", Alternatives = "български език"))] + Bg, + + #[strum(props(Name = "Bihari", Alternatives = "भोजपुरी"))] + Bh, + + #[strum(props(Name = "Bislama"))] + Bi, + + #[strum(props(Name = "Bambara", Alternatives = "bamanankan"))] + Bm, + + #[strum(props(Name = "Bengali", Alternatives = "Bangla, বাংলা"))] + Bn, + + #[strum(props(Name = "Tibetan", Alternatives = "Tibetan Standard, བོད་ཡིག"))] + Bo, + + #[strum(props(Name = "Breton", Alternatives = "brezhoneg"))] + Br, + + #[strum(props(Name = "Bosnian", Alternatives = "bosanski jezik"))] + Bs, + + #[strum(props(Name = "Catalan", Alternatives = "català"))] + Ca, + + #[strum(props(Name = "Chechen", Alternatives = "нохчийн мотт"))] + Ce, + + #[strum(props(Name = "Chamorro", Alternatives = "Chamoru"))] + Ch, + + #[strum(props(Name = "Corsican", Alternatives = "corsu, lingua corsa"))] + Co, + + #[strum(props(Name = "Cree", Alternatives = "ᓀᐦᐃᔭᐍᐏᐣ"))] + Cr, + + #[strum(props(Name = "Czech", Alternatives = "čeština, český jazyk"))] + Cs, + + #[strum(props(Name = "Church Slavonic", Alternatives = "Old Church Slavonic, Old Bulgarian, ѩзыкъ словѣньскъ"))] + Cu, + + #[strum(props(Name = "Chuvash", Alternatives = "чӑваш чӗлхи"))] + Cv, + + #[strum(props(Name = "Welsh", Alternatives = "Cymraeg"))] + Cy, + + #[strum(props(Name = "Danish", Alternatives = "dansk"))] + Da, + + #[strum(props(Name = "German", Alternatives = "Deutsch"))] + De, + + #[strum(props(Name = "Divehi", Alternatives = "Dhivehi, Maldivian, ދިވެހި"))] + Dv, + + #[strum(props(Name = "Dzongkha", Alternatives = "རྫོང་ཁ"))] + Dz, + + #[strum(props(Name = "Ewe", Alternatives = "Eʋegbe"))] + Ee, + + #[strum(props(Name = "Greek", Alternatives = "modern, ελληνικά"))] + El, + + #[strum(props(Name = "English"))] + En, + + #[strum(props(Name = "Esperanto"))] + Eo, + + #[strum(props(Name = "Spanish", Alternatives = "Español"))] + Es, + + #[strum(props(Name = "Estonian", Alternatives = "eesti, eesti keel"))] + Et, + + #[strum(props(Name = "Basque", Alternatives = "euskara, euskera"))] + Eu, + + #[strum(props(Name = "Persian", Alternatives = "Farsi, فارسی"))] + Fa, + + #[strum(props(Name = "Fula", Alternatives = "Fulah, Pulaar, Pular, Fulfulde"))] + Ff, + + #[strum(props(Name = "Finnish", Alternatives = "suomi, suomen kieli"))] + Fi, + + #[strum(props(Name = "Fijian", Alternatives = "vosa Vakaviti"))] + Fj, + + #[strum(props(Name = "Faroese", Alternatives = "føroyskt"))] + Fo, + + #[strum(props(Name = "French", Alternatives = "français"))] + Fr, + + #[strum(props(Name = "Western Frisian", Alternatives = "Frysk"))] + Fy, + + #[strum(props(Name = "Irish", Alternatives = "Gaeilge"))] + Ga, + + #[strum(props(Name = "Scottish Gaelic", Alternatives = "Gaelic, Gàidhlig"))] + Gd, + + #[strum(props(Name = "Galician", Alternatives = "galego"))] + Gl, + + #[strum(props(Name = "Guaraní", Alternatives = "Avañe'ẽ"))] + Gn, + + #[strum(props(Name = "Gujarati", Alternatives = "ગુજરાતી"))] + Gu, + + #[strum(props(Name = "Manx", Alternatives = "Gaelg, Gailck"))] + Gv, + + #[strum(props(Name = "Hausa", Alternatives = "Hausa, هَوُسَ"))] + Ha, + + #[strum(props(Name = "Hebrew", Alternatives = "עברית"))] + He, + + #[strum(props(Name = "Hindi", Alternatives = "हिन्दी, हिंदी"))] + Hi, + + #[strum(props(Name = "Hiri Motu"))] + Ho, + + #[strum(props(Name = "Croatian", Alternatives = "hrvatski jezik"))] + Hr, + + #[strum(props(Name = "Haitian", Alternatives = "Haitian Creole, Kreyòl ayisyen"))] + Ht, + + #[strum(props(Name = "Hungarian", Alternatives = "magyar"))] + Hu, + + #[strum(props(Name = "Armenian", Alternatives = "Հայերեն"))] + Hy, + + #[strum(props(Name = "Herero", Alternatives = "Otjiherero"))] + Hz, + + #[strum(props(Name = "Interlingua"))] + Ia, + + #[strum(props(Name = "Indonesian", Alternatives = "Bahasa Indonesia"))] + Id, + + #[strum(props(Name = "Interlingue", Alternatives = "Occidental"))] + Ie, + + #[strum(props(Name = "Igbo", Alternatives = "Asụsụ Igbo"))] + Ig, + + #[strum(props(Name = "Nuosu", Alternatives = "ꆈꌠ꒿ Nuosuhxop"))] + Ii, + + #[strum(props(Name = "Inupiaq", Alternatives = "Iñupiaq, Iñupiatun"))] + Ik, + + #[strum(props(Name = "Ido"))] + Io, + + #[strum(props(Name = "Icelandic", Alternatives = "Íslenska"))] + Is, + + #[strum(props(Name = "Italian", Alternatives = "Italiano"))] + It, + + #[strum(props(Name = "Inuktitut", Alternatives = "ᐃᓄᒃᑎᑐᑦ"))] + Iu, + + #[strum(props(Name = "Japanese", Alternatives = "日本語, にほんご"))] + Ja, + + #[strum(props(Name = "Javanese", Alternatives = "ꦧꦱꦗꦮ, Basa Jawa"))] + Jv, + + #[strum(props(Name = "Georgian", Alternatives = "ქართული"))] + Ka, + + #[strum(props(Name = "Kongo", Alternatives = "Kikongo"))] + Kg, + + #[strum(props(Name = "Kikuyu", Alternatives = "Gikuyu, Gĩkũyũ"))] + Ki, + + #[strum(props(Name = "Kwanyama", Alternatives = "Kuanyama, Kuanyama"))] + Kj, + + #[strum(props(Name = "Kazakh", Alternatives = "қазақ тілі"))] + Kk, + + #[strum(props(Name = "Kalaallisut", Alternatives = "Greenlandic, kalaallisut, kalaallit oqaasii"))] + Kl, + + #[strum(props(Name = "Khmer", Alternatives = "ខ្មែរ, ខេមរភាសា, ភាសាខ្មែរ"))] + Km, + + #[strum(props(Name = "Kannada", Alternatives = "ಕನ್ನಡ"))] + Kn, + + #[strum(props(Name = "Korean", Alternatives = "한국어"))] + Ko, + + #[strum(props(Name = "Kanuri"))] + Kr, + + #[strum(props(Name = "Kashmiri", Alternatives = "कश्मीरी, كشميري"))] + Ks, + + #[strum(props(Name = "Kurdish", Alternatives = "Kurdî, كوردی"))] + Ku, + + #[strum(props(Name = "Komi", Alternatives = "коми кыв"))] + Kv, + + #[strum(props(Name = "Cornish", Alternatives = "Kernewek"))] + Kw, + + #[strum(props(Name = "Kyrgyz", Alternatives = "Кыргызча, Кыргыз тили"))] + Ky, + + #[strum(props(Name = "Latin", Alternatives = "latine, lingua latina"))] + La, + + #[strum(props(Name = "Luxembourgish", Alternatives = "Letzeburgesch, Lëtzebuergesch"))] + Lb, + + #[strum(props(Name = "Ganda", Alternatives = "Luganda"))] + Lg, + + #[strum(props(Name = "Limburgish", Alternatives = "Limburgan, Limburger, Limburgs"))] + Li, + + #[strum(props(Name = "Lingala", Alternatives = "Lingála"))] + Ln, + + #[strum(props(Name = "Lao", Alternatives = "ພາສາລາວ"))] + Lo, + + #[strum(props(Name = "Lithuanian", Alternatives = "lietuvių kalba"))] + Lt, + + #[strum(props(Name = "Luba-Katanga", Alternatives = "Tshiluba"))] + Lu, + + #[strum(props(Name = "Latvian", Alternatives = "latviešu valoda"))] + Lv, + + #[strum(props(Name = "Malagasy", Alternatives = "fiteny malagasy"))] + Mg, + + #[strum(props(Name = "Marshallese", Alternatives = "Kajin M̧ajeļ"))] + Mh, + + #[strum(props(Name = "Māori", Alternatives = "te reo Māori"))] + Mi, + + #[strum(props(Name = "Macedonian", Alternatives = "македонски јазик"))] + Mk, + + #[strum(props(Name = "Malayalam", Alternatives = "മലയാളം"))] + Ml, + + #[strum(props(Name = "Mongolian", Alternatives = "Монгол хэл"))] + Mn, + + #[strum(props(Name = "Marathi", Alternatives = "Marāṭhī, मराठी"))] + Mr, + + #[strum(props(Name = "Malay", Alternatives = "bahasa Melayu, بهاس ملايو"))] + Ms, + + #[strum(props(Name = "Maltese", Alternatives = "Malti"))] + Mt, + + #[strum(props(Name = "Burmese", Alternatives = "ဗမာစာ"))] + My, + + #[strum(props(Name = "Nauruan", Alternatives = "Dorerin Naoero"))] + Na, + + #[strum(props(Name = "Norwegian Bokmål", Alternatives = "Norsk bokmål"))] + Nb, + + #[strum(props(Name = "Northen Ndbele", Alternatives = "isiNdebele"))] + Nd, + + #[strum(props(Name = "Nepali", Alternatives = "नेपाली"))] + Ne, + + #[strum(props(Name = "Ndonga", Alternatives = "Owambo"))] + Ng, + + #[strum(props(Name = "Dutch", Alternatives = "Nederlands, Vlaams"))] + Nl, + + #[strum(props(Name = "Norwegian Nynorsk", Alternatives = "Norsk nynorsk"))] + Nn, + + #[strum(props(Name = "Norwegian", Alternatives = "Norsk"))] + No, + + #[strum(props(Name = "Southen Ndebele", Alternatives = "isiNdebele"))] + Nr, + + #[strum(props(Name = "Navajo", Alternatives = "Navaho, Diné bizaad"))] + Nv, + + #[strum(props(Name = "Chichewa", Alternatives = "Chewa, Nyanja, chiCheŵa, chinyanja"))] + Ny, + + #[strum(props(Name = "Occitan", Alternatives = "occitan, lenga d'òc"))] + Oc, + + #[strum(props(Name = "Ojibwe", Alternatives = "Ojibwa, ᐊᓂᔑᓈᐯᒧᐎᓐ"))] + Oj, + + #[strum(props(Name = "Oromo", Alternatives = "Afaan Oromoo"))] + Om, + + #[strum(props(Name = "Oriya", Alternatives = "ଓଡ଼ିଆ"))] + Or, + + #[strum(props(Name = "ossetian", Alternatives = "Ossetic, ирон æвзаг"))] + Os, + + #[strum(props(Name = "Eastern Punjabi", Alternatives = "ਪੰਜਾਬੀ"))] + Pa, + + #[strum(props(Name = "Pali", Alternatives = "Pāli, पाऴि"))] + Pi, + + #[strum(props(Name = "Polish", Alternatives = "język polski, polszczyzna"))] + Pl, + + #[strum(props(Name = "Pashto", Alternatives = "Pushto, پښتو"))] + Ps, + + #[strum(props(Name = "Portuguese", Alternatives = "Português"))] + Pt, + + #[strum(props(Name = "Quechua", Alternatives = "Runa Simi, Kichwa"))] + Qu, + + #[strum(props(Name = "Romansh", Alternatives = "rumantsch grischun"))] + Rm, + + #[strum(props(Name = "Kirundi", Alternatives = "Ikirundi"))] + Rn, + + #[strum(props(Name = "Romanian", Alternatives = "Română"))] + Ro, + + #[strum(props(Name = "Russian", Alternatives = "Русский"))] + Ru, + + #[strum(props(Name = "Kinyarwanda", Alternatives = "Ikinyarwanda"))] + Rw, + + #[strum(props(Name = "Sanskrit", Alternatives = "Saṁskṛta, संस्कृतम्"))] + Sa, + + #[strum(props(Name = "Sardinian", Alternatives = "sardu"))] + Sc, + + #[strum(props(Name = "Sindhi", Alternatives = "सिन्धी, سنڌي، سندھی"))] + Sd, + + #[strum(props(Name = "Northern Sami", Alternatives = "Davvisámegiella"))] + Se, + + #[strum(props(Name = "Sango", Alternatives = "yângâ tî sängö"))] + Sg, + + #[strum(props(Name = "Sinhalese", Alternatives = "Sinhala, සිංහල"))] + Si, + + #[strum(props(Name = "Slovak", Alternatives = "slovenčina, slovenský jazyk"))] + Sk, + + #[strum(props(Name = "Slovene", Alternatives = "slovenski jezik, slovenščina"))] + Sl, + + #[strum(props(Name = "Samoan", Alternatives = "gagana fa'a Samoa"))] + Sm, + + #[strum(props(Name = "Shona", Alternatives = "chiShona"))] + Sn, + + #[strum(props(Name = "Somali", Alternatives = "Soomaaliga, af Soomaali"))] + So, + + #[strum(props(Name = "Albanian", Alternatives = "Shqip"))] + Sq, + + #[strum(props(Name = "Serbian", Alternatives = "српски језик"))] + Sr, + + #[strum(props(Name = "Swati", Alternatives = "SiSwati"))] + Ss, + + #[strum(props(Name = "Southern Sotho", Alternatives = "Sesotho"))] + St, + + #[strum(props(Name = "Sundanese", Alternatives = "Basa Sunda"))] + Su, + + #[strum(props(Name = "Swedish", Alternatives = "svenska"))] + Sv, + + #[strum(props(Name = "Swahili", Alternatives = "Kiswahili"))] + Sw, + + #[strum(props(Name = "Tamil", Alternatives = "தமிழ்"))] + Ta, + + #[strum(props(Name = "Telugu", Alternatives = "తెలుగు"))] + Te, + + #[strum(props(Name = "Tajik", Alternatives = "тоҷикӣ, toçikī, تاجیکی"))] + Tg, + + #[strum(props(Name = "Thai", Alternatives = "ไทย"))] + Th, + + #[strum(props(Name = "Tigrinya", Alternatives = "ትግርኛ"))] + Ti, + + #[strum(props(Name = "Turkmen", Alternatives = "Türkmen, Түркмен"))] + Tk, + + #[strum(props(Name = "Tagalog", Alternatives = "Wikang Tagalog"))] + Tl, + + #[strum(props(Name = "Tswana", Alternatives = "Setswana"))] + Tn, + + #[strum(props(Name = "Tonga", Alternatives = "Tonga Islands, faka Tonga"))] + To, + + #[strum(props(Name = "Turkish", Alternatives = "Türkçe"))] + Tr, + + #[strum(props(Name = "Tsonga", Alternatives = "Xitsonga"))] + Ts, + + #[strum(props(Name = "Tatar", Alternatives = "татар теле, tatar tele"))] + Tt, + + #[strum(props(Name = "Twi"))] + Tw, + + #[strum(props(Name = "Tahitian", Alternatives = "Reo Tahiti"))] + Ty, + + #[strum(props(Name = "Uyghur", Alternatives = "ئۇيغۇرچە, Uyghurche"))] + Ug, + + #[strum(props(Name = "Ukrainian", Alternatives = "Українська"))] + Uk, + + #[strum(props(Name = "Urdu", Alternatives = "اردو"))] + Ur, + + #[strum(props(Name = "Uzbek", Alternatives = "Oʻzbek, Ўзбек, أۇزبېك"))] + Uz, + + #[strum(props(Name = "Venda", Alternatives = "Tshivenḓa"))] + Ve, + + #[strum(props(Name = "Vietnamese", Alternatives = "Tiếng Việt"))] + Vi, + + #[strum(props(Name = "Volapük"))] + Vo, + + #[strum(props(Name = "Walloon", Alternatives = "walon"))] + Wa, + + #[strum(props(Name = "Wolof", Alternatives = "Wollof"))] + Wo, + + #[strum(props(Name = "Xhosa", Alternatives = "isiXhosa"))] + Xh, + + #[strum(props(Name = "Yiddish", Alternatives = "ייִדיש"))] + Yi, + + #[strum(props(Name = "Yoruba", Alternatives = "Yorùbá"))] + Yo, + + #[strum(props(Name = "Zhuang", Alternatives = "Chuang, Saɯ cueŋƅ, Saw cuengh"))] + Za, + + #[strum(props(Name = "Chinese", Alternatives = "中文, 汉语, 漢語"))] + Zh, + + #[strum(props(Name = "Zulu", Alternatives = "isiZulu"))] + Zu, +} diff --git a/translatable_shared/src/structures/mod.rs b/translatable_shared/src/structures/mod.rs new file mode 100644 index 0000000..fab1c32 --- /dev/null +++ b/translatable_shared/src/structures/mod.rs @@ -0,0 +1,8 @@ +pub mod file_position; +pub mod file_related_error; +pub mod inline_meta_variable; +pub mod item_type; +pub mod language; +pub mod templated_string; +pub mod path; +pub mod translation_tree; diff --git a/translatable_shared/src/structures/path.rs b/translatable_shared/src/structures/path.rs new file mode 100644 index 0000000..4437bec --- /dev/null +++ b/translatable_shared/src/structures/path.rs @@ -0,0 +1,53 @@ +#![cfg(feature = "internal")] + +use proc_macro2::Span; +use syn::{Ident, Result as SynResult, Token}; +use syn::parse::{Parse, ParseStream}; + +pub struct Path { + is_root: bool, + segments: Vec, + span: Span +} + +impl Path { + pub fn merge(self, other: Self) -> Option { + Some(Path { + is_root: self.is_root, + segments: [self.segments, other.segments].concat(), + span: self.span.join(other.span)? + }) + } + + #[inline(always)] + pub const fn is_root(&self) -> bool { + self.is_root + } + + #[inline(always)] + pub fn segments(&self) -> &[String] { + &self.segments + } + + #[inline(always)] + pub const fn span(&self) -> Span { + self.span + } +} + +#[cfg(feature = "internal")] +impl Parse for Path { + fn parse(input: ParseStream) -> SynResult { + Ok(Self { + is_root: input + .parse::() + .is_ok(), + segments: input + .parse_terminated(Ident::parse, Token![::])? + .into_iter() + .map(|segment| segment.to_string()) + .collect(), + span: input.span() + }) + } +} diff --git a/translatable_shared/src/structures/templated_string.rs b/translatable_shared/src/structures/templated_string.rs new file mode 100644 index 0000000..2dbe63e --- /dev/null +++ b/translatable_shared/src/structures/templated_string.rs @@ -0,0 +1,161 @@ +use std::str::FromStr; +use std::ops::Range; +use std::collections::HashMap; + +use thiserror::Error; + +#[cfg(feature = "internal")] +use ::{ + proc_macro2::TokenStream, + quote::{quote, ToTokens, TokenStreamExt} +}; + +use crate::utils::is_ident; + +#[derive(Error, Debug, Clone)] +pub enum TemplateParseError { + #[error(r#" + Found unclosed template brace at index {0}. + If you intended to escape the brace, + use "{{{{" instead. + "#)] + Unclosed(usize), + + #[error(r#" + Found a template with key '{0}' which is + an invalid identifier. Identifiers must start with + a letter or underscore, and end with many letters, + digits, or underscores. + "#)] + InvalidIdent(String) +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct TemplatedString { + original: String, + spans: Vec<(String, Range)> +} + +#[cfg(feature = "internal")] +impl ToTokens for TemplateParseError { + fn to_tokens(&self, tokens: &mut TokenStream) { + tokens.extend(quote! { ::translatable::prelude::TemplateParseError:: }); + + tokens.extend( + match self { + Self::Unclosed(index) => quote! { Unclosed(#index) }, + Self::InvalidIdent(ident) => quote! { InvalidIdent(#ident.to_string()) }, + } + ) + } +} + +impl TemplatedString { + #[inline(always)] + pub fn new(original: &str, replacements: Vec<(String, Range)>) -> Self { + Self { original: original.to_string(), spans: replacements } + } + + pub fn replace_with(&self, replacements: &HashMap) -> String { + let mut result = self.original.clone(); + let mut spans = self.spans.clone(); + + spans.sort_by_key(|(_, range)| range.start); + + let mut offset = 0isize; + + for (key, range) in spans { + if let Some(value) = replacements.get(&key) { + let start = (range.start as isize + offset) as usize; + let end = (range.end as isize + offset) as usize; + + result.replace_range(start..end, value); + + offset += value.len() as isize - (range.end - range.start) as isize; + } + } + + result + } + + #[inline(always)] + pub fn original(&self) -> &str { + &self.original + } +} + +impl FromStr for TemplatedString { + type Err = TemplateParseError; + + fn from_str(s: &str) -> Result { + let original = s.to_string(); + let mut spans = Vec::new(); + + let character_bytes = s + .char_indices() + .map(|(i, _)| i) + .collect::>(); + + let mut last_bracket_idx = None; + let mut current_tmpl_key = String::new(); + + for (char_idx, c) in original.chars().enumerate() { + match (c, last_bracket_idx) { + ('{', Some(prev)) if prev == char_idx.saturating_sub(1) => last_bracket_idx = None, + ('{', _) => last_bracket_idx = Some(char_idx), + + ('}', Some(open_idx)) => { + let key = current_tmpl_key.clone(); + + spans.push(( + is_ident(&key) + .then_some(key.clone()) + .ok_or(TemplateParseError::InvalidIdent(key))?, + character_bytes[open_idx] + ..character_bytes + .get(char_idx + 1) + .copied() + .unwrap_or_else(|| s.len()) + )); + + last_bracket_idx = None; + current_tmpl_key.clear(); + } + + (c, Some(_)) => current_tmpl_key.push(c), + + _ => {} + } + } + + if let Some(lbi) = last_bracket_idx { + Err(TemplateParseError::Unclosed(lbi)) + } else { + Ok(Self { original, spans }) + } + } +} + +#[cfg(feature = "internal")] +impl ToTokens for TemplatedString { + fn to_tokens(&self, tokens: &mut TokenStream) { + let original = &self.original; + + let span_map = self + .spans + .iter() + .map(|(key, range)| { + let start = range.start; + let end = range.end; + + quote! { (#key.to_string(), #start..#end) } + }); + + tokens.append_all(quote! { + ::translatable::prelude::LocalizedString::new( + #original, + vec![#(#span_map),*] + ) + }) + } +} diff --git a/translatable_shared/src/structures/translation_tree.rs b/translatable_shared/src/structures/translation_tree.rs new file mode 100644 index 0000000..823b764 --- /dev/null +++ b/translatable_shared/src/structures/translation_tree.rs @@ -0,0 +1,521 @@ +use std::collections::HashMap; +use std::fmt::Display; +use std::fs::read_to_string; +use std::hash::{Hash, Hasher}; +use std::path::Path; + +#[cfg(feature = "preparsing")] +use std::error::Error as StdError; + +use edit_distance::edit_distance; +use thiserror::Error; +use toml_edit::{DocumentMut, Item as TomlItem, Table as TomlTable, Value as TomlValue}; + +#[cfg(feature = "internal")] +use ::{ + proc_macro2::TokenStream, + quote::{quote, ToTokens} +}; + +#[cfg(feature = "preparsing")] +use dyn_clone::{DynClone, clone_trait_object}; + +use crate::structures::language::{Language, LanguageError}; +use crate::structures::file_position::FileLocation; +use crate::structures::templated_string::{TemplateParseError, TemplatedString}; +use crate::structures::file_related_error::{FileRelatedError}; + +#[cfg(feature = "preparsing")] +pub trait GenericTreeError: StdError + DynClone + Display {} + +#[cfg(feature = "preparsing")] +clone_trait_object!(GenericTreeError); + +#[derive(Error, Debug, Clone)] +pub enum TranslationTreeErrorDescription { + #[error(r#" + An invalid value was found in a nesting, nestings only support + other nestings or translations. For which a translation is a map + of languages (valid iso-639-1 strings) and raw strings and a nesting + is a subset of structures containing other structures. + "#)] + InvalidValueInNesting, + + #[error(r#" + An invalid value was found in a translation, translations are maps for + which keys are languages (valid iso-639-1 strings) and raw strings that + represent a version of a string in that specific language. + "#)] + InvalidValueInTranslation, + + #[error(r#" + Found an empty table, can't infer wether it's a translation or an object. + Either remove that empty table or fill it with a valid value. + "#)] + EmptyTable, + + #[error(r#" + Found an invalid language in a translation object key. + {0:#} + "#)] + InvalidLanguageKey(#[from] LanguageError), + + #[error(r#" + An error occurred while opening a file + related to this translation branch. + {0:#} + "#)] + IoError(String), + + #[error(r#" + An error occurred while parsing TOML for + content related to this translation branch. + "{0:#}" + "#)] + // error description + TomlError(String), + + #[error(r#" + An error occurred while parsing templates + for a translation string in this branch. + "#)] + TemplateError(#[from] TemplateParseError), + + #[cfg(all(not(feature = "internal"), feature = "preparsing"))] + #[error("{0:#}")] + GenericError(Box) +} + +pub type TranslationTreeParseError = FileRelatedError; + +#[derive(Error, Debug)] +pub enum TranslationNotFound { + #[error( + r#" + "{attempted_segment}" does not exist in "{accomplished_segments}"{next_possibility} + "#, + accomplished_segments = accomplished_segments.join("::"), + next_possibility = if let Some(next) = closest_next_possibility { + format!(r#", perhaps you meant "{next}"?"#) + } else { + ".".to_string() + } + )] + NotFoundInNode { + accomplished_segments: Vec, + attempted_segment: String, + closest_next_possibility: Option, + }, + + #[error( + r#" + Attempted to access "{attempted_segment}" at "{accomplished_segments}", + but failed because "{accomplished_segments}" leads to a translation object + and not a nesting. + "#, + accomplished_segments = accomplished_segments.join("::") + )] + FoundEarlyTranslation { + accomplished_segments: Vec, + attempted_segment: String, + }, + + #[error( + r#" + The path "{accomplished_segments}" is invalid or may be incomplete, + because "{accomplished_segments}" leads to a nesting, not a translation. + "#, + accomplished_segments = accomplished_segments.join("::") + )] + PathLeadsToNesting { + accomplished_segments: Vec, + }, + + #[error("This node is not accessible because an error occurred while parsing it.\n{0:#}")] + FoundErroredNode(#[from] TranslationTreeParseError) +} + +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct Translation(HashMap); + +#[non_exhaustive] +#[derive(Debug)] +pub enum TranslationTree { + #[non_exhaustive] Nesting(HashMap), + #[non_exhaustive] Translation(Translation), + #[non_exhaustive] NestingError(TranslationTreeParseError) +} + +#[cfg(feature = "internal")] +impl ToTokens for TranslationTreeErrorDescription { + fn to_tokens(&self, tokens: &mut TokenStream) { + tokens.extend(quote! { ::translatable::prelude::TreeErrorDescription:: }); + + tokens.extend( + match self { + Self::InvalidValueInNesting + => quote! { InvalidValueInNesting }, + + Self::InvalidValueInTranslation + => quote! { InvalidValueInTranslation }, + + Self::EmptyTable + => quote! { EmptyTable }, + + Self::InvalidLanguageKey(error) + => quote! { InvalidLanguageKey(#error) }, + + Self::IoError(error) + => quote! { IoError(#error.to_string()) }, + + Self::TomlError(error) + => quote! { TomlError(#error.to_string()) }, + + Self::TemplateError(error) + => quote! { TemplateError(#error) } + } + ); + } +} + +impl Translation { + #[inline(always)] + pub fn empty() -> Self { + Self(HashMap::new()) + } + + #[inline(always)] + pub const fn from_data(raw: HashMap) -> Self { + Self(raw) + } + + pub fn available_languages(&self) -> Vec<&Language> { + self.0 + .keys() + .collect() + } + + #[inline(always)] + pub fn get_language(&self, lang: &Language) -> Option<&TemplatedString> { + self.0 + .get(&lang) + } + + pub fn language_available(&self, lang: &Language) -> bool { + self.0 + .contains_key(&lang) + } + + pub(crate) fn insert(&mut self, key: Language, value: TemplatedString) { + self.0 + .insert(key, value); + } +} + +#[cfg(feature = "internal")] +impl ToTokens for Translation { + fn to_tokens(&self, tokens: &mut TokenStream) { + let possibilities = self.0 + .iter() + .map(|(key, value)| quote! { (#key, #value.to_string()) }); + + tokens.extend( + quote! { + ::translatable::shared::Translation::new( + ::std::collections::HashMap::from([ + #(#possibilities),* + ]) + ) + } + ) + } +} + +impl Hash for Translation { + fn hash(&self, state: &mut H) { + for (key, value) in &self.0 { + state.write(&key.to_string().into_bytes()); + value.hash(state); + } + } +} + +impl TranslationTree { + #[inline(always)] + pub fn empty_translation() -> Self { + Self::Translation(Translation::empty()) + } + + #[inline(always)] + pub fn empty_nesting() -> Self { + Self::Nesting(HashMap::new()) + } + + fn collect_translations(source: Option<&Path>, raw_contents: &str, table: &TomlTable) -> Self { + let mut result = None; + + // HACK: This may be rewritten and should use less nesting + // i.e use error-first logic. + for (key, value) in table { + match value { + TomlItem::Value(TomlValue::String(value)) => { + match result.get_or_insert_with(|| Self::Translation(Translation::default())) { + Self::Translation(translation) => { + let language = match key.parse::() { + Ok(language) => language, + + Err(error) => { + result = Some(Self::NestingError(TranslationTreeParseError { + description: TranslationTreeErrorDescription::InvalidLanguageKey(error), + file_path: source.map(|p| p.to_path_buf()), + at_character: Some( + FileLocation::from_optional_range(raw_contents, value.span()) + ) + })); + + continue; + } + }; + + match value.value().parse() { + Ok(templated_string) => translation.insert( + language, + templated_string + ), + + Err(error) => { + result = Some(Self::NestingError(TranslationTreeParseError { + description: TranslationTreeErrorDescription::TemplateError(error), + file_path: source.map(|p| p.to_path_buf()), + at_character: Some( + FileLocation::from_optional_range(raw_contents, value.span()) + ) + })); + } + } + } + + Self::Nesting(_) => { + result = Some(Self::NestingError(TranslationTreeParseError { + description: TranslationTreeErrorDescription::InvalidValueInTranslation, + file_path: source.map(|p| p.to_path_buf()), + at_character: Some( + FileLocation::from_optional_range(raw_contents, value.span()) + ) + })) + } + + Self::NestingError(_) => { break; } + } + } + + TomlItem::Table(value) => { + match result.get_or_insert_with(|| Self::Nesting(HashMap::new())) { + Self::Nesting(result) => { + result.insert( + key.to_string(), + Self::collect_translations(source, raw_contents, value) + ); + } + + Self::Translation(_) => { + result = Some(Self::NestingError(TranslationTreeParseError { + description: TranslationTreeErrorDescription::InvalidValueInNesting, + file_path: source.map(|p| p.to_path_buf()), + at_character: Some( + FileLocation::from_optional_range(raw_contents, value.span()) + ) + })) + }, + + Self::NestingError(_) => { break; } + } + } + + other => { + result = Some(Self::NestingError(TranslationTreeParseError { + description: TranslationTreeErrorDescription::InvalidValueInNesting, + file_path: source.map(|p| p.to_path_buf()), + at_character: Some( + FileLocation::from_optional_range(raw_contents, other.span()) + ) + })) + } + } + } + + result.unwrap_or_else(|| { + Self::NestingError(TranslationTreeParseError { + description: TranslationTreeErrorDescription::EmptyTable, + file_path: source.map(|p| p.to_path_buf()), + at_character: Some( + FileLocation::from_optional_range(raw_contents, table.span()) + ) + }) + }) + } + + pub fn from_raw(source: Option<&Path>, raw_contents: &str) -> Self { + let toml_contents = match raw_contents.parse::() { + Ok(contents) => contents, + + Err(error) => { + return Self::NestingError(TranslationTreeParseError { + description: TranslationTreeErrorDescription::TomlError(error.to_string()), + file_path: source.map(|s| s.to_path_buf()), + at_character: Some( + FileLocation::from_optional_range(raw_contents, error.span()) + ) + }); + } + }; + + let table = match toml_contents.as_item() { + TomlItem::Table(table) => table, + + other => { + return Self::NestingError(TranslationTreeParseError { + description: TranslationTreeErrorDescription::InvalidValueInNesting, + file_path: source.map(|s| s.to_path_buf()), + at_character: Some( + FileLocation::from_optional_range(raw_contents, other.span()) + ) + }); + } + }; + + Self::collect_translations(source, raw_contents, table) + } + + pub fn fallback_available(&self, language: Language) -> bool { + match self { + Self::Nesting(nesting) => { + for (_, tree) in nesting { + if tree.fallback_available(language) { + return true; + } + } + } + + Self::Translation(translation) => { + if translation.language_available(&language) { + return true; + } + }, + + // errors don't short circuit the lookup. + Self::NestingError(_) => {} + } + + false + } + + fn closest_next_nesting(&self, key: &str) -> Option { + match self { + Self::Nesting(nesting) => { + nesting + .keys() + .min_by_key(|candidate| edit_distance(key, candidate)) + .cloned() + } + + Self::Translation(_) | Self::NestingError(_) => None, + } + } + + pub fn find_path>(&self, mut segments: I) -> Result<&Translation, TranslationNotFound> { + let mut accomplished = Vec::new(); + let mut tree_node = self; + + while let Some(segment) = segments.next() { + match self { + Self::Nesting(nesting) => { + match nesting.get(&segment) { + Some(next) => { + tree_node = next; + accomplished.push(segment.clone()); + } + + None => { + return Err(TranslationNotFound::NotFoundInNode { + accomplished_segments: accomplished, + closest_next_possibility: tree_node.closest_next_nesting(&segment), + attempted_segment: segment + }); + } + } + }, + + Self::Translation(translation) => { + if segments.next().is_some() { + return Err(TranslationNotFound::FoundEarlyTranslation { + accomplished_segments: accomplished, + attempted_segment: segment + }); + } + + return Ok(translation); + }, + + Self::NestingError(error) => { + return Err(error.clone().into()); + } + } + } + + Err(TranslationNotFound::PathLeadsToNesting { + accomplished_segments: accomplished + }) + } +} + +impl From<&Path> for TranslationTree { + fn from(path: &Path) -> Self { + match read_to_string(path) { + Ok(raw_contents) => Self::from_raw(Some(path), &raw_contents), + + Err(error) => { + Self::NestingError(TranslationTreeParseError { + description: TranslationTreeErrorDescription::IoError(error.to_string()), + file_path: Some(path.to_path_buf()), + at_character: None + }) + } + } + } +} + +impl From<&str> for TranslationTree { + fn from(raw_contents: &str) -> Self { + Self::from_raw(None, raw_contents) + } +} + +#[cfg(feature = "internal")] +impl ToTokens for TranslationTree { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + Self::Nesting(nesting) => { + let nesting_tokens = nesting + .into_iter() + .map(|(key, value)| quote! { (#key.to_string(), #value) }); + + tokens.extend(quote! { + ::translatable::prelude::TranslationTree::Nesting( + ::std::collections::HashMap::from([ + #(#nesting_tokens),* + ]) + ) + }) + } + + Self::Translation(translation) => tokens.extend(quote! { + ::translatable::prelude::TranslationTree::Translation(#translation) + }), + + Self::NestingError(error) => tokens.extend(quote! { + ::translatable::prelude::TranslationTree::NestingError(#error) + }), + } + } +} diff --git a/translatable_shared/src/translations/collection.rs b/translatable_shared/src/translations/collection.rs deleted file mode 100644 index 852be95..0000000 --- a/translatable_shared/src/translations/collection.rs +++ /dev/null @@ -1,114 +0,0 @@ -//! Translation file collection module. -//! -//! This module declares [`TranslationNodeCollection`] -//! a representation of each file found in the translations -//! folder defined in the configuration file. - -use std::collections::HashMap; - -use proc_macro2::TokenStream as TokenStream2; -use quote::{ToTokens, TokenStreamExt, quote}; - -use super::node::{TranslationNode, TranslationObject}; -use crate::macros::collections::map_transform_to_tokens; - -/// Translation file collection. -/// -/// This tuple struct wraps a hashmap implementing -/// a lookup trough all the files in ascending order. -/// -/// The internal hashmap contains the original file -/// paths along all the unmerged [`TranslationNode`] -/// found in each file. -pub struct TranslationNodeCollection(HashMap); - -impl TranslationNodeCollection { - /// Create a new [`TranslationNodeCollection`]. - /// - /// By providing a populated hashmap, create a new - /// [`TranslationNodeCollection`] structure. - /// - /// The file paths in the hashmap key aren't validated. This - /// is usually called from a `to-runtime` implementation, if - /// you want to obtain all the translation files use - /// - /// **Arguments** - /// * `collection` - An already populated collection for lookup. - /// - /// **Returns** - /// The provided collection wrapped in a [`TranslationNodeCollection`]. - pub fn new(collection: HashMap) -> Self { - Self(collection) - } - - /// Get a node from a file path. - /// - /// This method may be used to load a translation - /// independently, if you are looking for an independent - /// translation you may want to call find_path instead. - /// - /// **Arguments** - /// * `path` - The OS path where the file was originally found. - /// - /// **Returns** - /// A top level translation node, containing all the translations - /// in that specific file. - #[allow(unused)] - pub fn get_node(&self, path: &str) -> Option<&TranslationNode> { - self.0 - .get(path) - } - - /// Search a path trough all the nodes. - /// - /// This method is used to load a specific translation - /// file agnostic from a "translation path" which consists - /// of the necessary TOML object path to reach a specific - /// translation object. - /// - /// **Arguments** - /// * `path` - The sections of the TOML path in order to access - /// the desired translation object. - /// - /// **Returns** - /// A translation object containing a specific translation - /// in all it's available languages. - pub fn find_path(&self, path: &Vec) -> Option<&TranslationObject> { - self.0 - .values() - .find_map(|node| node.find_path(path)) - } -} - -/// Hashmap wrapper implementation. -/// -/// Abstraction to easily collect a [`HashMap`] and -/// wrap it in a [`TranslationNodeCollection`]. -impl FromIterator<(String, TranslationNode)> for TranslationNodeCollection { - fn from_iter>(iter: T) -> Self { - Self( - iter.into_iter() - .collect(), - ) - } -} - -/// Compile-time to runtime implementation. -/// -/// This implementation generates the call to [`new`] on -/// [`TranslationNodeCollection`] with the data from the current -/// instance to perform a compile-time to runtime conversion. -/// -/// [`new`]: TranslationNodeCollection::new -impl ToTokens for TranslationNodeCollection { - fn to_tokens(&self, tokens: &mut TokenStream2) { - let map = - map_transform_to_tokens(&self.0, |key, value| quote! { (#key.to_string(), #value) }); - - tokens.append_all(quote! { - translatable::shared::translations::collection::TranslationNodeCollection::new( - #map - ) - }); - } -} diff --git a/translatable_shared/src/translations/mod.rs b/translatable_shared/src/translations/mod.rs deleted file mode 100644 index a7e81a7..0000000 --- a/translatable_shared/src/translations/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Translation structures module. -//! -//! This module's sub-modules declare -//! structures to manage deserialized -//! structures from the translation files. -//! These permit searching paths in a more -//! rust-friendly way. - -pub mod collection; -pub mod node; diff --git a/translatable_shared/src/translations/node.rs b/translatable_shared/src/translations/node.rs deleted file mode 100644 index 32ae9b6..0000000 --- a/translatable_shared/src/translations/node.rs +++ /dev/null @@ -1,210 +0,0 @@ -//! Translation node declaration module. -//! -//! This module declares [`TranslationNode`] which -//! is a nested enum that behaves like a n-ary tree -//! for which each branch contains paths that might -//! lead to translation objects or other paths. - -use std::collections::HashMap; - -use proc_macro2::TokenStream as TokenStream2; -use quote::{ToTokens, TokenStreamExt, quote}; -use strum::ParseError; -use thiserror::Error; -use toml_edit::{Item, Table, Value}; - -use crate::macros::collections::{map_to_tokens, map_transform_to_tokens}; -use crate::misc::language::Language; -use crate::misc::templating::{FormatString, TemplateError}; - -/// [`TranslationNode`] errors. -/// -/// This error is agnostic to the runtime, it is used -/// for errors while parsing a [`TranslationNode`] or -/// while trying seeking for it's content. -#[derive(Error, Debug)] -pub enum TranslationNodeError { - /// Invalid object type error. - /// - /// This error signals that the nesting rules were - /// broken, thus the parsing cannot continue. - #[error("A nesting can only contain translation objects or other nestings")] - InvalidNesting, - - /// Template validation error. - /// - /// This means there was an error while validating - /// a translation templates, such as an invalid - /// ident for its keys or unclosed templates. - #[error("Template validation failed: {0:#}")] - TemplateValidation(#[from] TemplateError), - - /// Invalid value found inside a nesting. - /// - /// This error signals that an invalid value was found - /// inside a nesting, such as mixed values. - #[error( - "Mixed values are not allowed, a nesting can't contain strings and objects at the same \ - time" - )] - MixedValues, - - /// Invalid ISO-639-1 translation key. - /// - /// This error signals that an invalid key was found for a - /// translation inside a translation object. - /// - /// Translation keys must follow the ISO-639-1 standard. - #[error("Couldn't parse ISO 639-1 string for translation key")] - LanguageParsing(#[from] ParseError), - - /// Empty translation file. - /// - /// This error signals that a created translation file - /// is empty and cannot be parsed. - #[error("A translation file cannot be empty")] - EmptyTable, -} - -/// Nesting type alias. -/// -/// This is one of the valid objects that might be found -/// on a translation file, this object might contain a translation -/// or another nesting. -pub type TranslationNesting = HashMap; - -/// Object type alias. -/// -/// This is one of the valid objects that might be found -/// on a translation file, this object contains only translations -/// keyed with their respective languages. -pub type TranslationObject = HashMap; - -/// Translation node structure. -/// -/// This enum acts like an n-ary tree which -/// may contain [`TranslationNesting`] or -/// [`TranslationObject`] representing a tree -/// that follows the translation file rules. -pub enum TranslationNode { - /// Branch containing a [`TranslationNesting`]. - /// - /// Read the [`TranslationNesting`] documentation for - /// more information. - Nesting(TranslationNesting), - - /// Branch containing a [`TranslationObject`]. - /// - /// Read the [`TranslationObject`] documentation for - /// more information. - Translation(TranslationObject), -} - -impl TranslationNode { - /// Resolves a translation path through the nesting hierarchy. - /// - /// **Arguments** - /// * `path` - Slice of path segments to resolve. - /// - /// **Returns** - /// A reference to translations if path exists and points to leaf node. - pub fn find_path(&self, path: &Vec) -> Option<&TranslationObject> { - let path = path - .iter() - .map(|i| i.to_string()) - .collect::>(); - - match self { - Self::Nesting(nested) => { - let (first, rest) = path.split_first()?; - nested - .get(first)? - .find_path(&rest.to_vec()) - }, - Self::Translation(translation) => path - .is_empty() - .then_some(translation), - } - } -} - -/// Compile-time to runtime conversion implementation. -/// -/// This implementation converts a [`TranslationNode`] into -/// runtime trough tokens by nesting calls depending on the -/// type inferred in compile-time. -/// -/// This is usually used for dynamic paths. -impl ToTokens for TranslationNode { - fn to_tokens(&self, tokens: &mut TokenStream2) { - match self { - TranslationNode::Nesting(nesting) => { - let map = map_transform_to_tokens( - nesting, - |key, value| quote! { (#key.to_string(), #value) }, - ); - - tokens.append_all(quote! { - translatable::shared::translations::node::TranslationNode::Nesting( - #map - ) - }); - }, - - TranslationNode::Translation(translation) => { - let map = map_to_tokens(translation); - - tokens.append_all(quote! { - translatable::shared::translations::node::TranslationNode::Translation( - #map - ) - }); - }, - } - } -} - -/// TOML table parsing. -/// -/// This implementation parses a TOML table object -/// reference usually taken from a `toml_edit::DocuemntMut` -/// into a [`TranslationNode`] for validation and -/// seeking the translations according to the rules. -impl TryFrom<&Table> for TranslationNode { - type Error = TranslationNodeError; - - // The top level can only contain objects is never enforced. - fn try_from(value: &Table) -> Result { - let mut result = None; - - for (key, value) in value { - match value { - Item::Value(Value::String(translation_value)) => { - match result.get_or_insert_with(|| Self::Translation(HashMap::new())) { - Self::Translation(translation) => { - translation.insert( - key.parse()?, - translation_value - .value() - .parse()?, - ); - }, - Self::Nesting(_) => return Err(TranslationNodeError::MixedValues), - } - }, - - Item::Table(nesting_value) => { - match result.get_or_insert_with(|| Self::Nesting(HashMap::new())) { - Self::Nesting(nesting) => { - nesting.insert(key.to_string(), Self::try_from(nesting_value)?); - }, - Self::Translation(_) => return Err(TranslationNodeError::MixedValues), - } - }, - _ => return Err(TranslationNodeError::InvalidNesting), - } - } - - result.ok_or(TranslationNodeError::EmptyTable) - } -} diff --git a/translatable_shared/src/utils.rs b/translatable_shared/src/utils.rs new file mode 100644 index 0000000..5ddcb75 --- /dev/null +++ b/translatable_shared/src/utils.rs @@ -0,0 +1,31 @@ +pub fn is_ident(candidate: &str) -> bool { + let mut chars = candidate.chars(); + + match chars.next() { + Some(first) if first == '_' || first.is_ascii_alphabetic() => {} + _ => return false + } + + chars.all(|c| c == '_' || c.is_ascii_alphanumeric()) +} + +#[cfg(feature = "internal")] +pub mod internal { + use std::path::Path; + + use proc_macro2::TokenStream; + use quote::{ToTokens, quote}; + + #[inline(always)] + pub fn option_stream(opt: &Option) -> TokenStream { + match opt { + Some(val) => quote! { ::std::option::Option::Some(#val) }, + None => quote! { ::std::option::Option::None } + } + } + + pub fn path_to_tokens(path: &Path) -> TokenStream { + let path = path.to_string_lossy(); + quote! { ::std::path::Path::from(#path) } + } +}