From 4d5c705cf19732421f63aae317902fd2a5596faf Mon Sep 17 00:00:00 2001 From: Chiko Date: Sat, 1 Mar 2025 19:41:12 +0000 Subject: [PATCH 001/228] feat: add missing language codes --- .gitignore | 1 + flake.lock | 46 ++++++++ flake.nix | 86 +++++++++++++++ src/languages.rs | 266 +++++++++++++++++++++++++++++++++++++++++++---- src/macros.rs | 62 ++++++----- 5 files changed, 415 insertions(+), 46 deletions(-) create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/.gitignore b/.gitignore index ea8c4bf..e17ef0a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +.bacon-locations diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..a9e7b0a --- /dev/null +++ b/flake.lock @@ -0,0 +1,46 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1740695751, + "narHash": "sha256-D+R+kFxy1KsheiIzkkx/6L63wEHBYX21OIwlFV8JvDs=", + "rev": "6313551cd05425cd5b3e63fe47dbc324eabb15e4", + "revCount": 760502, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.760502%2Brev-6313551cd05425cd5b3e63fe47dbc324eabb15e4/01954f5d-9aa0-742c-829e-dfa0c472c2db/source.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://flakehub.com/f/NixOS/nixpkgs/0.1.%2A.tar.gz" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1740796337, + "narHash": "sha256-FuoXrXZPoJEZQ3PF7t85tEpfBVID9JQIOnVKMNfTAb0=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "bbac9527bc6b28b6330b13043d0e76eac11720dc", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..dab6dbb --- /dev/null +++ b/flake.nix @@ -0,0 +1,86 @@ +{ + description = "A Nix-flake-based Rust development environment"; + + inputs = { + nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.1.*.tar.gz"; + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = + { + self, + nixpkgs, + rust-overlay, + }: + let + supportedSystems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + forEachSupportedSystem = + f: + nixpkgs.lib.genAttrs supportedSystems ( + system: + f { + pkgs = import nixpkgs { + inherit system; + overlays = [ + rust-overlay.overlays.default + self.overlays.default + ]; + }; + } + ); + in + { + overlays.default = final: prev: { + rustToolchain = + let + rust = prev.rust-bin; + in + if builtins.pathExists ./rust-toolchain.toml then + rust.fromRustupToolchainFile ./rust-toolchain.toml + else if builtins.pathExists ./rust-toolchain then + rust.fromRustupToolchainFile ./rust-toolchain + else + rust.stable.latest.default.override { + extensions = [ + "rust-src" + "rustfmt" + ]; + }; + }; + + devShells = forEachSupportedSystem ( + { pkgs }: + { + default = pkgs.mkShell { + packages = with pkgs; [ + # General + openssl + pkg-config + + # Rust + cargo-deny + cargo-edit + cargo-watch + rust-analyzer + rustToolchain + ]; + + env = { + # Required by rust-analyzer + RUST_SRC_PATH = "${pkgs.rustToolchain}/lib/rustlib/src/rust/library"; + + LAZYVIM_RUST_DIAGNOSTICS = "bacon-ls"; + }; + }; + } + ); + }; +} diff --git a/src/languages.rs b/src/languages.rs index 008092d..cae44ea 100644 --- a/src/languages.rs +++ b/src/languages.rs @@ -1,151 +1,381 @@ -use strum::EnumIter; -use strum::IntoEnumIterator; +use std::str::FromStr; -#[derive(Debug, EnumIter)] +use strum::{Display, EnumIter, EnumString, IntoEnumIterator}; + +#[derive(Debug, EnumIter, Display, EnumString)] +#[strum(ascii_case_insensitive)] pub enum Iso639a { + #[strum(serialize = "Abkhazian", serialize = "ab")] AB, + #[strum(serialize = "Afar", serialize = "aa")] AA, + #[strum(serialize = "Afrikaans", serialize = "af")] AF, + #[strum(serialize = "Akan", serialize = "ak")] + AK, + #[strum(serialize = "Albanian", serialize = "sq")] SQ, + #[strum(serialize = "Amharic", serialize = "am")] AM, + #[strum(serialize = "Arabic", serialize = "ar")] AR, + #[strum(serialize = "Aragonese", serialize = "an")] + AN, + #[strum(serialize = "Armenian", serialize = "hy")] HY, + #[strum(serialize = "Assamese", serialize = "as")] AS, + #[strum(serialize = "Avaric", serialize = "av")] + AV, + #[strum(serialize = "Avestan", serialize = "ae")] + AE, + #[strum(serialize = "Aymara", serialize = "ay")] AY, + #[strum(serialize = "Azerbaijani", serialize = "az")] AZ, + #[strum(serialize = "Bambara", serialize = "bm")] + BM, + #[strum(serialize = "Bashkir", serialize = "ba")] BA, + #[strum(serialize = "Basque", serialize = "eu")] EU, + #[strum(serialize = "Belarusian", serialize = "be")] + BE, + #[strum(serialize = "Bengali", serialize = "bn")] BN, - DZ, - BH, + #[strum(serialize = "Bislama", serialize = "bi")] BI, + #[strum(serialize = "Bosnian", serialize = "bs")] + BS, + #[strum(serialize = "Breton", serialize = "br")] BR, + #[strum(serialize = "Bulgarian", serialize = "bg")] BG, + #[strum(serialize = "Burmese", serialize = "my")] MY, - BE, - KM, + #[strum(serialize = "Catalan", serialize = "ca")] CA, + #[strum(serialize = "Chamorro", serialize = "ch")] + CH, + #[strum(serialize = "Chechen", serialize = "ce")] + CE, + #[strum(serialize = "Chichewa", serialize = "ny")] + NY, + #[strum(serialize = "Chinese", serialize = "zh")] ZH, + #[strum(serialize = "Church Slavonic", serialize = "cu")] + CU, + #[strum(serialize = "Chuvash", serialize = "cv")] + CV, + #[strum(serialize = "Cornish", serialize = "kw")] + KW, + #[strum(serialize = "Corsican", serialize = "co")] CO, + #[strum(serialize = "Cree", serialize = "cr")] + CR, + #[strum(serialize = "Croatian", serialize = "hr")] HR, + #[strum(serialize = "Czech", serialize = "cs")] CS, + #[strum(serialize = "Danish", serialize = "da")] DA, + #[strum(serialize = "Divehi", serialize = "dv")] + DV, + #[strum(serialize = "Dutch", serialize = "nl")] NL, + #[strum(serialize = "Dzongkha", serialize = "dz")] + DZ, + #[strum(serialize = "English", serialize = "en")] EN, + #[strum(serialize = "Esperanto", serialize = "eo")] EO, + #[strum(serialize = "Estonian", serialize = "et")] ET, + #[strum(serialize = "Ewe", serialize = "ee")] + EE, + #[strum(serialize = "Faroese", serialize = "fo")] FO, + #[strum(serialize = "Fijian", serialize = "fj")] FJ, + #[strum(serialize = "Finnish", serialize = "fi")] FI, + #[strum(serialize = "French", serialize = "fr")] FR, + #[strum(serialize = "Western Frisian", serialize = "fy")] FY, + #[strum(serialize = "Fulah", serialize = "ff")] + FF, + #[strum(serialize = "Gaelic", serialize = "gd")] + GD, + #[strum(serialize = "Galician", serialize = "gl")] GL, + #[strum(serialize = "Ganda", serialize = "lg")] + LG, + #[strum(serialize = "Georgian", serialize = "ka")] KA, + #[strum(serialize = "German", serialize = "de")] DE, + #[strum(serialize = "Greek", serialize = "el")] EL, + #[strum(serialize = "Kalaallisut", serialize = "kl")] KL, + #[strum(serialize = "Guarani", serialize = "gn")] GN, + #[strum(serialize = "Gujarati", serialize = "gu")] GU, + #[strum(serialize = "Haitian", serialize = "ht")] + HT, + #[strum(serialize = "Hausa", serialize = "ha")] HA, + #[strum(serialize = "Hebrew", serialize = "he")] HE, + #[strum(serialize = "Herero", serialize = "hz")] + HZ, + #[strum(serialize = "Hindi", serialize = "hi")] HI, + #[strum(serialize = "Hiri Motu", serialize = "ho")] + HO, + #[strum(serialize = "Hungarian", serialize = "hu")] HU, + #[strum(serialize = "Icelandic", serialize = "is")] IS, + #[strum(serialize = "Ido", serialize = "io")] + IO, + #[strum(serialize = "Igbo", serialize = "ig")] + IG, + #[strum(serialize = "Indonesian", serialize = "id")] ID, + #[strum(serialize = "Interlingua", serialize = "ia")] IA, + #[strum(serialize = "Interlingue", serialize = "ie")] IE, + #[strum(serialize = "Inuktitut", serialize = "iu")] IU, + #[strum(serialize = "Inupiaq", serialize = "ik")] IK, + #[strum(serialize = "Irish", serialize = "ga")] GA, + #[strum(serialize = "Italian", serialize = "it")] IT, + #[strum(serialize = "Japanese", serialize = "ja")] JA, + #[strum(serialize = "Javanese", serialize = "jv")] JV, + #[strum(serialize = "Kannada", serialize = "kn")] KN, + #[strum(serialize = "Kanuri", serialize = "kr")] + KR, + #[strum(serialize = "Kashmiri", serialize = "ks")] KS, + #[strum(serialize = "Kazakh", serialize = "kk")] KK, + #[strum(serialize = "Central Khmer", serialize = "km")] + KM, + #[strum(serialize = "Kikuyu", serialize = "ki")] + KI, + #[strum(serialize = "Kinyarwanda", serialize = "rw")] RW, + #[strum(serialize = "Kyrgyz", serialize = "ky")] KY, - RN, + #[strum(serialize = "Komi", serialize = "kv")] + KV, + #[strum(serialize = "Kongo", serialize = "kg")] + KG, + #[strum(serialize = "Korean", serialize = "ko")] KO, + #[strum(serialize = "Kuanyama", serialize = "kj")] + KJ, + #[strum(serialize = "Kurdish", serialize = "ku")] KU, + #[strum(serialize = "Lao", serialize = "lo")] LO, + #[strum(serialize = "Latin", serialize = "la")] LA, + #[strum(serialize = "Latvian", serialize = "lv")] LV, + #[strum(serialize = "Limburgan", serialize = "li")] + LI, + #[strum(serialize = "Lingala", serialize = "ln")] LN, + #[strum(serialize = "Lithuanian", serialize = "lt")] LT, + #[strum(serialize = "Luba-Katanga", serialize = "lu")] + LU, + #[strum(serialize = "Luxembourgish", serialize = "lb")] + LB, + #[strum(serialize = "Macedonian", serialize = "mk")] MK, + #[strum(serialize = "Malagasy", serialize = "mg")] MG, + #[strum(serialize = "Malay", serialize = "ms")] MS, + #[strum(serialize = "Malayalam", serialize = "ml")] ML, + #[strum(serialize = "Maltese", serialize = "mt")] MT, + #[strum(serialize = "Manx", serialize = "gv")] + GV, + #[strum(serialize = "Maori", serialize = "mi")] MI, + #[strum(serialize = "Marathi", serialize = "mr")] MR, - MO, + #[strum(serialize = "Marshallese", serialize = "mh")] + MH, + #[strum(serialize = "Mongolian", serialize = "mn")] MN, + #[strum(serialize = "Nauru", serialize = "na")] NA, + #[strum(serialize = "Navajo", serialize = "nv")] + NV, + #[strum(serialize = "North Ndebele", serialize = "nd")] + ND, + #[strum(serialize = "South Ndebele", serialize = "nr")] + NR, + #[strum(serialize = "Nepali", serialize = "ng")] + NG, + #[strum(serialize = "Nepali", serialize = "ne")] NE, + #[strum(serialize = "Norwegian", serialize = "no")] NO, + #[strum(serialize = "Norwegian BokmΓ₯l", serialize = "nb")] + NB, + #[strum(serialize = "Norwegian Nynorsk", serialize = "nn")] + NN, + #[strum(serialize = "Occitan", serialize = "oc")] OC, + #[strum(serialize = "Ojibwa", serialize = "oj")] + OJ, + #[strum(serialize = "Oriya", serialize = "or")] OR, + #[strum(serialize = "Oromo", serialize = "om")] + OM, + #[strum(serialize = "Ossetian", serialize = "os")] + OS, + #[strum(serialize = "Pali", serialize = "pi")] + PI, + #[strum(serialize = "Pashto", serialize = "ps")] PS, + #[strum(serialize = "Persian", serialize = "fa")] + FA, + #[strum(serialize = "Polish", serialize = "pl")] PL, + #[strum(serialize = "Portuguese", serialize = "pt")] PT, + #[strum(serialize = "Punjabi", serialize = "pa")] PA, + #[strum(serialize = "Quechua", serialize = "qu")] QU, - RM, + #[strum(serialize = "Romanian", serialize = "ro")] RO, + #[strum(serialize = "Romansh", serialize = "rm")] + RM, + #[strum(serialize = "Rundi", serialize = "rn")] + RN, + #[strum(serialize = "Russian", serialize = "ru")] RU, + #[strum(serialize = "North Sami", serialize = "se")] + SE, + #[strum(serialize = "Samoan", serialize = "sm")] SM, + #[strum(serialize = "Sango", serialize = "sg")] SG, + #[strum(serialize = "Sanskrit", serialize = "sa")] SA, + #[strum(serialize = "Sardinian", serialize = "sc")] + SC, + #[strum(serialize = "Serbian", serialize = "sr")] SR, - SH, - ST, - TN, + #[strum(serialize = "Shona", serialize = "sn")] SN, + #[strum(serialize = "Sindhi", serialize = "sd")] SD, + #[strum(serialize = "Sinhala", serialize = "si")] SI, - SS, + #[strum(serialize = "Slovak", serialize = "sk")] SK, + #[strum(serialize = "Slovenian", serialize = "sl")] SL, + #[strum(serialize = "Somali", serialize = "so")] SO, + #[strum(serialize = "Southern Sotho", serialize = "st")] + ST, + #[strum(serialize = "Spanish", serialize = "es")] ES, + #[strum(serialize = "Sundanese", serialize = "su")] SU, + #[strum(serialize = "Swahili", serialize = "sw")] SW, + #[strum(serialize = "Swati", serialize = "ss")] + SS, + #[strum(serialize = "Swedish", serialize = "sv")] SV, + #[strum(serialize = "Tagalog", serialize = "tl")] TL, + #[strum(serialize = "Tahitian", serialize = "ty")] + TY, + #[strum(serialize = "Tajik", serialize = "tg")] TG, + #[strum(serialize = "Tamil", serialize = "ta")] TA, + #[strum(serialize = "Tatar", serialize = "tt")] TT, + #[strum(serialize = "Telugu", serialize = "te")] TE, + #[strum(serialize = "Thai", serialize = "th")] TH, + #[strum(serialize = "Tibetan", serialize = "bo")] BO, + #[strum(serialize = "Tigrinya", serialize = "ti")] TI, + #[strum(serialize = "Tonga", serialize = "to")] TO, + #[strum(serialize = "Tsonga", serialize = "ts")] TS, + #[strum(serialize = "Tswana", serialize = "tn")] + TN, + #[strum(serialize = "Turkish", serialize = "tr")] TR, + #[strum(serialize = "Turkmen", serialize = "tk")] TK, + #[strum(serialize = "Twi", serialize = "tw")] TW, + #[strum(serialize = "Uighur", serialize = "ug")] UG, + #[strum(serialize = "Ukrainian", serialize = "uk")] UK, + #[strum(serialize = "Urdu", serialize = "ur")] UR, + #[strum(serialize = "Uzbek", serialize = "uz")] UZ, + #[strum(serialize = "Venda", serialize = "ve")] + VE, + #[strum(serialize = "Vietnamese", serialize = "vi")] VI, + #[strum(serialize = "VolapΓΌk", serialize = "vo")] VO, + #[strum(serialize = "Walloon", serialize = "wa")] + WA, + #[strum(serialize = "Welsh", serialize = "cy")] CY, + #[strum(serialize = "Wolof", serialize = "wo")] WO, + #[strum(serialize = "Xhosa", serialize = "xh")] XH, + #[strum(serialize = "Sichuan Yi", serialize = "ii")] + II, + #[strum(serialize = "Yiddish", serialize = "yi")] YI, + #[strum(serialize = "Yoruba", serialize = "yo")] YO, + #[strum(serialize = "Zhuang", serialize = "za")] ZA, - ZU + #[strum(serialize = "Zulu", serialize = "zu")] + ZU, } impl Iso639a { pub fn is_valid(lang: &str) -> bool { - Self::languages() - .iter() - .any(|valid_lang| valid_lang == lang) + Iso639a::from_str(lang).is_ok() } pub fn languages() -> Vec { diff --git a/src/macros.rs b/src/macros.rs index 28a7226..22fb329 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -1,28 +1,34 @@ +use std::str::FromStr; + +use crate::{languages::Iso639a, translations::load_translation_static}; use proc_macro::TokenStream; -use syn::{parse::{Parse, ParseStream}, Expr, ExprLit, ExprPath, Lit, Result as SynResult, token::Static, Token}; use quote::quote; -use crate::translations::load_translation_static; +use syn::{ + Expr, ExprLit, ExprPath, Lit, Result as SynResult, Token, + parse::{Parse, ParseStream}, + token::Static, +}; pub struct RawTranslationArgs { language: Expr, _comma: Token![,], static_marker: Option, - path: Expr + path: Expr, } pub enum TranslationPathType { OnScopeExpression(TokenStream), - CompileTimePath(String) + CompileTimePath(String), } pub enum TranslationLanguageType { OnScopeExpression(TokenStream), - CompileTimeLiteral(String) + CompileTimeLiteral(String), } pub struct TranslationArgs { language: TranslationLanguageType, - path: TranslationPathType + path: TranslationPathType, } impl Parse for RawTranslationArgs { @@ -31,7 +37,7 @@ impl Parse for RawTranslationArgs { language: input.parse()?, _comma: input.parse()?, static_marker: input.parse()?, - path: input.parse()? + path: input.parse()?, }) } } @@ -60,34 +66,27 @@ impl Into for RawTranslationArgs { TranslationArgs { language: match self.language { - Expr::Lit(ExprLit { lit: Lit::Str(lit_str), .. }) => { - TranslationLanguageType::CompileTimeLiteral(lit_str.value()) - }, - other => { - TranslationLanguageType::OnScopeExpression(quote!(#other).into()) - } + Expr::Lit(ExprLit { + lit: Lit::Str(lit_str), + .. + }) => TranslationLanguageType::CompileTimeLiteral(lit_str.value()), + other => TranslationLanguageType::OnScopeExpression(quote!(#other).into()), }, path: match self.path { Expr::Path(ExprPath { path, .. }) if is_path_static => { TranslationPathType::CompileTimePath( - path - .segments + path.segments .iter() - .map(|s| s - .ident - .to_string() - ) + .map(|s| s.ident.to_string()) .collect::>() .join(".") - .to_string() + .to_string(), ) - }, - - path => { - TranslationPathType::OnScopeExpression(quote!(#path).into()) } - } + + path => TranslationPathType::OnScopeExpression(quote!(#path).into()), + }, } } } @@ -99,15 +98,22 @@ pub fn translation_macro(args: TranslationArgs) -> TokenStream { Ok(Some(translation)) => quote!(#translation).into(), Ok(None) => { - let error_fmt = format!("The language \'{lang}\' is not available for \'{path}\'"); + let lang_name = Iso639a::from_str(&lang) + .map(|l| l.to_string()) + .unwrap_or_else(|_| lang.clone()); + + let error_fmt = format!( + "The language '{lang_name} ({lang})' is not available for '{path}'" + ); + quote!(compile_error!(#error_fmt)).into() - }, + } Err(err) => { let error_fmt = err.to_string(); quote!(compile_error!(#error_fmt)).into() } - } + }; } } From 489766361a37aa6b783610d6addb661bc6e056e1 Mon Sep 17 00:00:00 2001 From: Chiko Date: Sat, 1 Mar 2025 19:43:56 +0000 Subject: [PATCH 002/228] chore: remove unnecessary blank line --- src/languages.rs | 1 - src/macros.rs | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/languages.rs b/src/languages.rs index cae44ea..29f1ce0 100644 --- a/src/languages.rs +++ b/src/languages.rs @@ -1,5 +1,4 @@ use std::str::FromStr; - use strum::{Display, EnumIter, EnumString, IntoEnumIterator}; #[derive(Debug, EnumIter, Display, EnumString)] diff --git a/src/macros.rs b/src/macros.rs index 22fb329..58dc541 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -1,8 +1,7 @@ -use std::str::FromStr; - use crate::{languages::Iso639a, translations::load_translation_static}; use proc_macro::TokenStream; use quote::quote; +use std::str::FromStr; use syn::{ Expr, ExprLit, ExprPath, Lit, Result as SynResult, Token, parse::{Parse, ParseStream}, From b2d45319289e1ebd21b07df9aeff70a3e0a6ef08 Mon Sep 17 00:00:00 2001 From: Chiko Date: Sat, 1 Mar 2025 21:46:00 +0000 Subject: [PATCH 003/228] feat: Improve error message format in translation macro --- src/macros.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/macros.rs b/src/macros.rs index 58dc541..5d4125e 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -102,7 +102,7 @@ pub fn translation_macro(args: TranslationArgs) -> TokenStream { .unwrap_or_else(|_| lang.clone()); let error_fmt = format!( - "The language '{lang_name} ({lang})' is not available for '{path}'" + "The language '{lang}' ({lang_name}) is not available for '{path}'" ); quote!(compile_error!(#error_fmt)).into() From 214b4e15812ae4fa29e8b2b4ad5d6fccd5122765 Mon Sep 17 00:00:00 2001 From: stifskere Date: Sun, 2 Mar 2025 00:24:46 +0100 Subject: [PATCH 004/228] feat: translation object validation --- src/translations.rs | 91 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 88 insertions(+), 3 deletions(-) diff --git a/src/translations.rs b/src/translations.rs index ead8c71..7ac7491 100644 --- a/src/translations.rs +++ b/src/translations.rs @@ -1,7 +1,7 @@ use std::{fs::{read_dir, read_to_string}, io::Error as IoError, sync::OnceLock}; use proc_macro::TokenStream; use thiserror::Error; -use toml::{Table, de::Error as TomlError}; +use toml::{de::Error as TomlError, Table, Value}; use crate::{config::{load_config, ConfigError, SeekMode, TranslationOverlap}, macros::{TranslationLanguageType, TranslationPathType}, languages::Iso639a}; #[derive(Error, Debug)] @@ -26,7 +26,16 @@ pub enum TranslationError { "'{0}' is not valid ISO 639-1, valid languages include: {valid}", valid = Iso639a::languages().join(", ") )] - InvalidLangauge(String) + 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 } static TRANSLATIONS: OnceLock> = OnceLock::new(); @@ -59,6 +68,78 @@ fn walk_dir(path: &str) -> Result, TranslationError> { 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); @@ -78,7 +159,11 @@ fn load_translations() -> Result<&'static Vec
, TranslationError> { .map(|path| Ok( read_to_string(&path)? .parse::
() - .map_err(|err| TranslationError::ParseToml(err, path.clone()))? + .map_err(|err| TranslationError::ParseToml(err, path.clone())) + .and_then(|table| translations_valid(&table) + .then_some(table) + .ok_or(TranslationError::InvalidTomlFormat) + )? )) .collect::, TranslationError>>()?; From 4b3c35c07b0799652836f195a0d3b0a3d5dc33d5 Mon Sep 17 00:00:00 2001 From: stifskere Date: Sun, 2 Mar 2025 02:17:52 +0100 Subject: [PATCH 005/228] feat: better errors for invalid languages and fix typo --- src/languages.rs | 6 ------ src/translations.rs | 20 +++++++++++++++----- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/languages.rs b/src/languages.rs index 29f1ce0..d4bc00a 100644 --- a/src/languages.rs +++ b/src/languages.rs @@ -376,10 +376,4 @@ impl Iso639a { pub fn is_valid(lang: &str) -> bool { Iso639a::from_str(lang).is_ok() } - - pub fn languages() -> Vec { - Self::iter() - .map(|lang| format!("{:?}", lang).to_lowercase()) - .collect() - } } diff --git a/src/translations.rs b/src/translations.rs index ead8c71..34fb3dd 100644 --- a/src/translations.rs +++ b/src/translations.rs @@ -1,5 +1,6 @@ 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::{Table, de::Error as TomlError}; use crate::{config::{load_config, ConfigError, SeekMode, TranslationOverlap}, macros::{TranslationLanguageType, TranslationPathType}, languages::Iso639a}; @@ -18,15 +19,24 @@ pub enum TranslationError { #[error( "Toml parse error '{}'{}", .0.message(), - .0.span().map(|l| format!(" in {}:{}:{}", .1, l.start, l.end)).unwrap_or("".into()) + .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 include: {valid}", - valid = Iso639a::languages().join(", ") + "'{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") )] - InvalidLangauge(String) + InvalidLanguage(String) } static TRANSLATIONS: OnceLock> = OnceLock::new(); @@ -90,7 +100,7 @@ pub fn load_translation_static(lang: &str, path: &str) -> Result, let config = load_config()?; if !Iso639a::is_valid(lang) { - return Err(TranslationError::InvalidLangauge(lang.into())) + return Err(TranslationError::InvalidLanguage(lang.into())) } let mut choosen_translation = None; From dc8b3fef99429c3f44b18253655649d5f64e16ee Mon Sep 17 00:00:00 2001 From: Chiko Date: Thu, 6 Mar 2025 01:01:57 +0000 Subject: [PATCH 006/228] refactor: improve code quality and add documentation --- README.md | 128 ++++++++++++++++++++++++ src/config.rs | 108 +++++++++++++++----- src/languages.rs | 19 +++- src/lib.rs | 27 +++-- src/macros.rs | 17 ++-- src/translations.rs | 239 +++++++++++++++++++++++--------------------- 6 files changed, 384 insertions(+), 154 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..8245159 --- /dev/null +++ b/README.md @@ -0,0 +1,128 @@ +# Translatable + +[![Crates.io](https://img.shields.io/crates/v/translatable)](https://crates.io/crates/translatable) +[![Docs.rs](https://docs.rs/translatable/badge.svg)](https://docs.rs/translatable) + +A robust internationalization solution for Rust featuring compile-time validation, ISO 639-1 compliance, and TOML-based translation management. + +## Table of Contents + +- [Features](#features) +- [Installation](#installation) +- [Usage](#usage) +- [Configuration](#configuration) +- [Error Handling](#error-handling) +- [Example Structure](#example-structure) + +## Features πŸš€ + +- **ISO 639-1 Standard**: Full support for 180+ language codes/names +- **Compile-Time Safety**: Macro-based translation validation +- **TOML Structure**: Hierarchical translation files with nesting +- **Smart Error Messages**: Context-aware suggestions +- **Template Validation**: Balanced bracket checking +- **Flexible Loading**: Configurable file processing order +- **Conflict Resolution**: Choose between first or last match priority + +## Installation πŸ“¦ + +Add to your `Cargo.toml`: + +```toml +[dependencies] +translatable = "1" +``` + +## Usage + +Basic Macro Usage + +```rust +use translatable::translation; + +fn main() { + let greeting = translation!("en", static common.greeting); + println!("{}", greeting); +} +``` + +Translation File Example (`translations/app.toml`) + +```toml +[home] +welcome_message = { + en = "Welcome to our app!", + es = "Β‘Bienvenido a nuestra aplicaciΓ³n!" +} + +[user] +greeting = { + en = "Hello {name}!", + es = "Β‘Hola {name}!" +} +``` + +## Configuration βš™οΈ + +Create translatable.toml in your project root: + +```toml +path = "./translations" +seek_mode = "alphabetical" +overlap = "overwrite" +``` + +| Option | Default | Description | +| --------- | -------------- | --------------------------------- | +| path | ./translations | Translation directory location | +| seek_mode | alphabetical | File processing order | +| overlap | overwrite | Last file priority vs first found | + +## Error Handling 🚨 + +Invalid Language Code + +```sh +Error: 'e' is not valid ISO 639-1. These are some valid languages including 'e': + ae (Avestan), + eu (Basque), + be (Belarusian), + ce (Chechen), + en (English), + ... (12 more) + --> tests/static.rs:5:5 + | +``` + +Structural Validation + +```sh +Error: Invalid TOML structure in file ./translations/test.toml: Translation files must contain either nested tables or language translations, but not both at the same level. +``` + +Template Validation + +```sh +Error: Toml parse error 'invalid inline table + expected `}`' in ./translations/test.toml:49:50 + --> tests/static.rs:5:5 + | + 5 | translation!("es", static salutation::test); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) +``` + +## Example Structure πŸ“‚ + +```txt +project-root/ +β”œβ”€β”€ Cargo.toml +β”œβ”€β”€ translatable.toml +└── translations/ + β”œβ”€β”€ app.toml + β”œβ”€β”€ errors.toml + └── user/ + β”œβ”€β”€ profile.toml + └── settings.toml +``` diff --git a/src/config.rs b/src/config.rs index 1c6995a..cb919c0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,78 +1,132 @@ -use std::{fs::read_to_string, io::Error as IoError, sync::OnceLock}; +//! Configuration loading and handling for translatable content +//! +//! This module provides functionality to load and manage configuration +//! settings for localization/translation workflows from a TOML file. + use serde::Deserialize; +use std::fs::read_to_string; +use std::io::Error as IoError; +use std::sync::OnceLock; use thiserror::Error; -use toml::{from_str as toml_from_str, de::Error as TomlError}; +use toml::{de::Error as TomlError, from_str as toml_from_str}; +/// Errors that can occur during configuration loading #[derive(Error, Debug)] pub enum ConfigError { - #[error("An IO error occurred: {0:#}")] + /// IO error occurred while reading configuration file + #[error("IO error reading configuration: {0:#}")] Io(#[from] IoError), + /// TOML parsing error with location information #[error( - "Toml parse error '{}'{}", + "TOML parse error '{}'{}", .0.message(), - .0.span().map(|l| format!(" in ./translatable.toml:{}:{}", l.start, l.end)).unwrap_or("".into()) + .0.span().map(|l| format!(" in ./translatable.toml:{}:{}", l.start, l.end)) + .unwrap_or_else(|| "".into()) )] - ParseToml(#[from] TomlError) + ParseToml(#[from] TomlError), } +/// Wrapper type for locales directory path with validation #[derive(Deserialize)] -#[serde(rename = "snake_case")] +pub struct LocalesPath(String); + +impl Default for LocalesPath { + /// Default path to translations directory + fn default() -> Self { + LocalesPath("./translations".into()) + } +} + +/// File search order strategy +#[derive(Deserialize, Default)] +#[serde(rename_all = "snake_case")] pub enum SeekMode { + /// Alphabetical order (default) + #[default] Alphabetical, - Unalphabetical + + /// Reverse alphabetical order + Unalphabetical, } -#[derive(Deserialize)] -#[serde(rename = "snake_case")] +/// Translation conflict resolution strategy +#[derive(Deserialize, Default)] +#[serde(rename_all = "snake_case")] pub enum TranslationOverlap { + /// Last found translation overwrites previous ones (default) + #[default] Overwrite, - Ignore -} -// tracking issue: https://github.com/serde-rs/serde/issues/1030 -#[doc(hidden)] -fn __d_path() -> String { "./translations".into() } -#[doc(hidden)] -fn __d_seek_mode() -> SeekMode { SeekMode::Alphabetical } -#[doc(hidden)] -fn __d_overlap() -> TranslationOverlap { TranslationOverlap::Overwrite } + /// First found translation is preserved + Ignore, +} +/// Main configuration structure for translation system #[derive(Deserialize)] pub struct TranslatableConfig { - #[serde(default = "__d_path")] - path: String, - #[serde(default = "__d_seek_mode")] + /// Path to directory containing translation files + /// + /// # Example + /// ```toml + /// path = "./locales" + /// ``` + #[serde(default)] + path: LocalesPath, + + /// File processing order strategy + /// + /// Default: alphabetical file processing + #[serde(default)] seek_mode: SeekMode, - #[serde(default = "__d_overlap")] - overlap: TranslationOverlap + + /// Translation conflict resolution strategy + /// + /// Determines behavior when multiple files contain the same translation path + #[serde(default)] + overlap: TranslationOverlap, } impl TranslatableConfig { + /// Get reference to configured locales path pub fn path(&self) -> &str { - &self.path + &self.path.0 } + /// Get current seek mode strategy pub fn seek_mode(&self) -> &SeekMode { &self.seek_mode } + /// Get current overlap resolution strategy pub fn overlap(&self) -> &TranslationOverlap { &self.overlap } } +/// Global configuration cache static TRANSLATABLE_CONFIG: OnceLock = OnceLock::new(); +/// Load configuration from file or use defaults +/// +/// # Implementation Notes +/// - Uses `OnceLock` for thread-safe singleton initialization +/// - Missing config file is not considered an error +/// - Config file must be named `translatable.toml` in root directory +/// +/// # Panics +/// Will not panic but returns ConfigError for: +/// - Malformed TOML syntax +/// - Filesystem permission issues pub fn load_config() -> Result<&'static TranslatableConfig, ConfigError> { if let Some(config) = TRANSLATABLE_CONFIG.get() { return Ok(config); } - let config = toml_from_str( + let config: TranslatableConfig = toml_from_str( read_to_string("./translatable.toml") .unwrap_or("".into()) // if no config file is found use defaults. - .as_str() + .as_str(), )?; Ok(TRANSLATABLE_CONFIG.get_or_init(|| config)) diff --git a/src/languages.rs b/src/languages.rs index d4bc00a..9cce374 100644 --- a/src/languages.rs +++ b/src/languages.rs @@ -1,6 +1,12 @@ use std::str::FromStr; -use strum::{Display, EnumIter, EnumString, IntoEnumIterator}; +use strum::{Display, EnumIter, EnumString}; +/// 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, EnumIter, Display, EnumString)] #[strum(ascii_case_insensitive)] pub enum Iso639a { @@ -373,6 +379,17 @@ pub enum Iso639a { } impl Iso639a { + /// Validates if a string represents a valid ISO 639-1 language + /// + /// # Arguments + /// * `lang` - Input string to validate (case-insensitive) + /// + /// # Examples + /// ``` + /// assert!(Iso639a::is_valid("en")); + /// assert!(Iso639a::is_valid("English")); + /// assert!(!Iso639a::is_valid("xx")); + /// ``` pub fn is_valid(lang: &str) -> bool { Iso639a::from_str(lang).is_ok() } diff --git a/src/lib.rs b/src/lib.rs index d97ee3b..518bf48 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,16 +1,31 @@ -use macros::{translation_macro, RawTranslationArgs}; +//! Internationalization library providing compile-time and runtime translation facilities +//! +//! # Features +//! - TOML-based translation files +//! - ISO 639-1 language validation +//! - Configurable loading strategies +//! - Procedural macro for compile-time checking + +use macros::{RawTranslationArgs, translation_macro}; use proc_macro::TokenStream; use syn::parse_macro_input; mod config; +mod languages; mod macros; mod translations; -mod languages; +/// Procedural macro for compile-time translation validation +/// +/// # Usage +/// ``` +/// translation!("en", static some.path) +/// ``` +/// +/// # Parameters +/// - Language code/literal +/// - Translation path (supports static analysis) #[proc_macro] pub fn translation(input: TokenStream) -> TokenStream { - translation_macro( - parse_macro_input!(input as RawTranslationArgs) - .into() - ) + translation_macro(parse_macro_input!(input as RawTranslationArgs).into()) } diff --git a/src/macros.rs b/src/macros.rs index 5d4125e..5d5c801 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -2,16 +2,21 @@ use crate::{languages::Iso639a, translations::load_translation_static}; use proc_macro::TokenStream; use quote::quote; use std::str::FromStr; -use syn::{ - Expr, ExprLit, ExprPath, Lit, Result as SynResult, Token, - parse::{Parse, ParseStream}, - token::Static, -}; - +use syn::parse::{Parse, ParseStream}; +use syn::token::Static; +use syn::{Expr, ExprLit, ExprPath, Lit, Result as SynResult, Token}; + +/// Internal representation of macro arguments before processing +/// +/// Parses input in the format: +/// `(language_expression, static translation_path)` pub struct RawTranslationArgs { + /// Language specification (literal or expression) language: Expr, _comma: Token![,], + /// Static marker for path analysis static_marker: Option, + /// Translation path specification path: Expr, } diff --git a/src/translations.rs b/src/translations.rs index 2fb6fa6..c213402 100644 --- a/src/translations.rs +++ b/src/translations.rs @@ -1,21 +1,33 @@ -use std::{fs::{read_dir, read_to_string}, io::Error as IoError, sync::OnceLock}; +//! Translation handling module that loads and validates translation files, +//! and provides functionality to retrieve translations based on language and path. + +use crate::config::{ConfigError, SeekMode, TranslationOverlap, load_config}; +use crate::languages::Iso639a; +use crate::macros::{TranslationLanguageType, TranslationPathType}; use proc_macro::TokenStream; +use std::fs::{read_dir, read_to_string}; +use std::io::Error as IoError; +use std::sync::OnceLock; 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}; +use toml::{Table, Value, de::Error as TomlError}; +/// Errors that can occur during translation processing. #[derive(Error, Debug)] pub enum TranslationError { - #[error("{0}")] + /// Configuration-related error + #[error("{0:#}")] Config(#[from] ConfigError), + /// IO operation error #[error("An IO Error occurred: {0:#}")] Io(#[from] IoError), + /// Path contains invalid Unicode characters #[error("The path contains invalid unicode characters.")] InvalidUnicode, + /// TOML parsing error with location information #[error( "Toml parse error '{}'{}", .0.message(), @@ -25,59 +37,59 @@ pub enum TranslationError { )] ParseToml(TomlError, String), + /// Invalid language code error with suggestions #[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") + "'{0}' is not valid ISO 639-1. These are some valid languages including '{0}':\n{sorted_list}", + sorted_list = .1.join(",\n") )] - InvalidLanguage(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, Vec), + + /// Invalid TOML structure in specific file + #[error( + "Invalid TOML structure in file {0}: Translation files must contain either nested tables or language translations, but not both at the same level." + )] + InvalidTomlFormat(String), } +/// Global cache for loaded translations static TRANSLATIONS: OnceLock> = OnceLock::new(); +/// Recursively walk a directory and collect all file paths +/// +/// # Implementation Details +/// Uses iterative depth-first search to avoid stack overflow +/// Handles filesystem errors and invalid Unicode paths fn walk_dir(path: &str) -> Result, TranslationError> { - let directory = read_dir(path)? - .into_iter() - .collect::, _>>()?; - + let mut stack = vec![path.to_string()]; 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() - ); + // Use iterative approach to avoid recursion depth limits + 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(TranslationError::InvalidUnicode)? + .to_string(), + ); + } else { + result.push(path.to_string_lossy().to_string()); + } } } Ok(result) } +/// Validate TOML structure for translation files +/// +/// # Validation Rules +/// 1. Nodes must be either all tables or all translations +/// 2. Translation keys must be valid ISO 639-1 codes +/// 3. Template brackets must be balanced in translation values fn translations_valid(table: &Table) -> bool { let mut contains_translation = false; let mut contains_table = false; @@ -85,128 +97,127 @@ fn translations_valid(table: &Table) -> bool { 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) { + if contains_translation || !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) { + if contains_table || !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 { + // Check balanced template delimiters + let balance = translation.chars().fold(0i32, |acc, c| match c { + '{' => acc + 1, + '}' => acc - 1, + _ => acc, + }); + if balance != 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 + } + _ => return false, } } - - // if nothing returns false it means everything - // is valid. true } +/// Load translations from configured directory with thread-safe caching +/// +/// # Returns +/// Reference to loaded translations or TranslationError 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()); + // Sort paths case-insensitively + 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)? + .map(|path| { + let content = read_to_string(path)?; + let table = content .parse::
() - .map_err(|err| TranslationError::ParseToml(err, path.clone())) - .and_then(|table| translations_valid(&table) - .then_some(table) - .ok_or(TranslationError::InvalidTomlFormat) - )? - )) + .map_err(|err| TranslationError::ParseToml(err, path.clone()))?; + + if !translations_valid(&table) { + return Err(TranslationError::InvalidTomlFormat(path.clone())); + } + + Ok(table) + }) .collect::, TranslationError>>()?; Ok(TRANSLATIONS.get_or_init(|| translations)) } +/// Load translation for given language and path +/// +/// # Arguments +/// * `lang` - ISO 639-1 language code +/// * `path` - Dot-separated translation path +/// +/// # Returns +/// Option with translation or TranslationError 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 lang_lower = lang.to_lowercase(); + + let similarities = Iso639a::iter() + .filter(|lang| format!("{lang:?}").to_lowercase().contains(&lang_lower)) + .map(|lang| format!("{} ({lang:#})", format!("{lang:?}").to_lowercase())) + .collect::>(); + + return Err(TranslationError::InvalidLanguage( + lang.to_string(), + similarities, + )); } - let mut choosen_translation = None; + let mut chosen_translation = None; for translation in translations { - choosen_translation = path + if let Some(value) = 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; + .try_fold(translation, |acc, key| acc.get(key)?.as_table()) + .and_then(|table| table.get(lang)) + { + chosen_translation = Some(value.to_string()); + if matches!(config.overlap(), TranslationOverlap::Ignore) { + break; + } } } - Ok(choosen_translation) + Ok(chosen_translation) } -pub fn load_translation_dynamic(lang: TranslationLanguageType, path: TranslationPathType) -> TokenStream { +/// Dynamic translation loading for procedural macros +/// +/// # Arguments +/// * `lang` - Language type from macro +/// * `path` - Path type from macro +/// +/// # Returns +/// TokenStream for generated code +pub fn load_translation_dynamic( + lang: TranslationLanguageType, + path: TranslationPathType, +) -> TokenStream { let lang = lang.dynamic(); let path = path.dynamic(); - - todo!() + todo!("Implement dynamic translation loading") } From 14ab04752b3a6c161427de4b5335f2220b4d64db Mon Sep 17 00:00:00 2001 From: Chiko Date: Thu, 6 Mar 2025 01:07:40 +0000 Subject: [PATCH 007/228] chore: improve error showcase --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 8245159..3455603 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,8 @@ Error: 'e' is not valid ISO 639-1. These are some valid languages including 'e': ... (12 more) --> tests/static.rs:5:5 | + 5 | translation!("e", static salutation::test); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ``` Structural Validation From 53bb4f6afb2193a58c35ba69019d657f4c817e17 Mon Sep 17 00:00:00 2001 From: Chiko Date: Thu, 6 Mar 2025 01:21:05 +0000 Subject: [PATCH 008/228] chore: fix inconsistent title --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3455603..579ad0d 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Add to your `Cargo.toml`: translatable = "1" ``` -## Usage +## Usage πŸ› οΈ Basic Macro Usage From a1963e4b0f358a7228ceed740c78a00e980ab4f4 Mon Sep 17 00:00:00 2001 From: Chiko Date: Sun, 9 Mar 2025 00:01:52 +0000 Subject: [PATCH 009/228] feat: Improve documentation and code examples --- README.md | 162 +++++++++++++++++++++++++++++++++++------------ src/languages.rs | 2 +- src/lib.rs | 4 +- 3 files changed, 126 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 579ad0d..fcefe6c 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,13 @@ A robust internationalization solution for Rust featuring compile-time validatio ## Table of Contents -- [Features](#features) -- [Installation](#installation) -- [Usage](#usage) -- [Configuration](#configuration) -- [Error Handling](#error-handling) -- [Example Structure](#example-structure) +- [Features](#features-πŸš€) +- [Installation](#installation-πŸ“¦) +- [Usage](#usage-πŸ› οΈ) +- [Configuration](#configuration-βš™οΈ) +- [Error Handling](#error-handling-🚨) +- [Example Structure](#example-structure-πŸ“‚) +- [Integration Guide](#integration-guide-πŸ”—) ## Features πŸš€ @@ -26,27 +27,84 @@ A robust internationalization solution for Rust featuring compile-time validatio ## Installation πŸ“¦ -Add to your `Cargo.toml`: +Run the following command in your project directory: -```toml -[dependencies] -translatable = "1" +```sh +cargo add translatable ``` ## Usage πŸ› οΈ -Basic Macro Usage +### Macro Behavior Matrix + +| Parameters | Compile-Time Checks | Return Type | +| --------------------------------- | ---------------------------------------------------------------- | ---------------------------------------- | +| `static path` + `static language` | - Path existence
- Language validity
- Template validation | `&'static str` | +| `static path` + dynamic language | - Path existence
- Template structure | `Result<&'static str, TranslationError>` | +| dynamic path + `static language` | - Language validity | `Result<&'static str, TranslationError>` | +| dynamic path + dynamic language | None (runtime checks only) | `Result<&'static str, TranslationError>` | + +### Key Implications + +- **Static Path** + βœ… Verifies translation path exists in TOML files + ❌ Requires path literal (e.g., `static common::greeting`) + +- **Static Language** + βœ… Validates ISO 639-1 compliance + ❌ Requires language literal (e.g., `"en"`) + +- **Mixed Modes** + + ```rust + // Compile-time path + runtime language + translation!(user_lang, static user::profile::title) + + // Compile-time language + runtime path + translation!("fr", dynamic_path) + ``` + +- **Full Dynamic** + + ```rust + // Runtime checks only + translation!(lang_var, path_var) // Returns Result + ``` + +- **Full Static** + + ```rust + // Compile-time checks only + translation!("en", static common::greeting) // Returns &'static str + ``` + +Optimization Guide ```rust -use translatable::translation; +// Maximum safety - fails compile if any issues +let text = translation!("es", static home::welcome_message); -fn main() { - let greeting = translation!("en", static common.greeting); - println!("{}", greeting); -} +// Balanced approach - compile-time path validation +let result = translation!(user_lang, static user::profile::title); + +// Flexible runtime - handles dynamic inputs +let result = translation!(lang_var, path_var)?; ``` -Translation File Example (`translations/app.toml`) +## Example Structure πŸ“‚ + +```txt +project-root/ +β”œβ”€β”€ Cargo.toml +β”œβ”€β”€ translatable.toml +└── translations/ + β”œβ”€β”€ app.toml + β”œβ”€β”€ errors.toml + └── user/ + β”œβ”€β”€ profile.toml +``` + +### Example Translation File (translations/app.toml) ```toml [home] @@ -55,16 +113,25 @@ welcome_message = { es = "Β‘Bienvenido a nuestra aplicaciΓ³n!" } -[user] +[common] greeting = { en = "Hello {name}!", es = "Β‘Hola {name}!" } ``` +### Translation File Organization + +The `translations/` folder can be structured flexibly. You can organize translations based on features, modules, or locales. +Here are some best practices: + +- Keep related translations in subdirectories (`user/profile.toml`, `errors.toml`) +- Use consistent naming conventions (`common.toml`, `app.toml`) +- Keep files small and manageable to avoid conflicts + ## Configuration βš™οΈ -Create translatable.toml in your project root: +Create `translatable.toml` in your project root: ```toml path = "./translations" @@ -72,15 +139,20 @@ seek_mode = "alphabetical" overlap = "overwrite" ``` -| Option | Default | Description | -| --------- | -------------- | --------------------------------- | -| path | ./translations | Translation directory location | -| seek_mode | alphabetical | File processing order | -| overlap | overwrite | Last file priority vs first found | +| Option | Default | Description | +| --------- | -------------- | ------------------------------------------- | +| path | ./translations | Translation directory location | +| seek_mode | alphabetical | Order in which translation files are loaded | +| overlap | overwrite | Defines conflict resolution strategy | + +### Configuration Options Explained + +- **`seek_mode`**: Controls the order of file processing (e.g., `alphabetical`, `manual`). +- **`overlap`**: Determines priority when duplicate keys exist (`overwrite` replaces existing keys, `first` keeps the first occurrence). ## Error Handling 🚨 -Invalid Language Code +### Invalid Language Code ```sh Error: 'e' is not valid ISO 639-1. These are some valid languages including 'e': @@ -93,16 +165,17 @@ Error: 'e' is not valid ISO 639-1. These are some valid languages including 'e': --> tests/static.rs:5:5 | 5 | translation!("e", static salutation::test); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) ``` -Structural Validation +### Structural Validation ```sh Error: Invalid TOML structure in file ./translations/test.toml: Translation files must contain either nested tables or language translations, but not both at the same level. ``` -Template Validation +### Template Validation ```sh Error: Toml parse error 'invalid inline table @@ -115,16 +188,27 @@ Error: Toml parse error 'invalid inline table = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) ``` -## Example Structure πŸ“‚ +## Integration Guide πŸ”— -```txt -project-root/ -β”œβ”€β”€ Cargo.toml -β”œβ”€β”€ translatable.toml -└── translations/ - β”œβ”€β”€ app.toml - β”œβ”€β”€ errors.toml - └── user/ - β”œβ”€β”€ profile.toml - └── settings.toml +If you're using `translatable` in a web application, here’s how to integrate it: + +### Actix-Web Example + +```rust +use actix_web::{get, web, App, HttpServer, Responder}; +use translatable::translation; + +#[get("/")] +async fn home() -> impl Responder { + let text = translation!("en", static home::welcome_message); + text.to_string() +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + HttpServer::new(|| App::new().service(home)) + .bind("127.0.0.1:8080")? + .run() + .await +} ``` diff --git a/src/languages.rs b/src/languages.rs index 9cce374..a6226a5 100644 --- a/src/languages.rs +++ b/src/languages.rs @@ -385,7 +385,7 @@ impl Iso639a { /// * `lang` - Input string to validate (case-insensitive) /// /// # Examples - /// ``` + /// ```ignore /// assert!(Iso639a::is_valid("en")); /// assert!(Iso639a::is_valid("English")); /// assert!(!Iso639a::is_valid("xx")); diff --git a/src/lib.rs b/src/lib.rs index 518bf48..ae1d818 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,8 +18,8 @@ mod translations; /// Procedural macro for compile-time translation validation /// /// # Usage -/// ``` -/// translation!("en", static some.path) +/// ```ignore +/// translation!("en", static some::path) /// ``` /// /// # Parameters From e70279da6d2f72ba962f69a8d3d00be15fee541d Mon Sep 17 00:00:00 2001 From: stifskere Date: Fri, 14 Mar 2025 18:02:02 +0100 Subject: [PATCH 010/228] feat: refactor --- src/languages.rs | 13 +- src/macros.rs | 49 ++++---- src/translations.rs | 223 --------------------------------- src/translations/errors.rs | 49 ++++++++ src/translations/generation.rs | 127 +++++++++++++++++++ src/translations/mod.rs | 4 + src/translations/utils.rs | 125 ++++++++++++++++++ 7 files changed, 340 insertions(+), 250 deletions(-) delete mode 100644 src/translations.rs create mode 100644 src/translations/errors.rs create mode 100644 src/translations/generation.rs create mode 100644 src/translations/mod.rs create mode 100644 src/translations/utils.rs diff --git a/src/languages.rs b/src/languages.rs index a6226a5..9bf094c 100644 --- a/src/languages.rs +++ b/src/languages.rs @@ -1,5 +1,5 @@ use std::str::FromStr; -use strum::{Display, EnumIter, EnumString}; +use strum::{Display, EnumIter, EnumString, IntoEnumIterator}; /// ISO 639-1 language code implementation with validation /// @@ -393,4 +393,15 @@ impl Iso639a { pub fn is_valid(lang: &str) -> bool { Iso639a::from_str(lang).is_ok() } + + pub fn get_similarities(lang: &str) -> Vec { + Self::iter() + .map(|variant| variant.to_string()) + .filter(|variant| variant.contains(lang)) + .collect() + } + + pub fn eq_insensitive(&self, other: &str) -> bool { + format!("{self:?}").to_lowercase() == other.to_lowercase() + } } diff --git a/src/macros.rs b/src/macros.rs index 5d5c801..b55d3ec 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -1,11 +1,11 @@ -use crate::{languages::Iso639a, translations::load_translation_static}; use proc_macro::TokenStream; use quote::quote; -use std::str::FromStr; use syn::parse::{Parse, ParseStream}; use syn::token::Static; use syn::{Expr, ExprLit, ExprPath, Lit, Result as SynResult, Token}; +use crate::translations::generation::load_translation_static; + /// Internal representation of macro arguments before processing /// /// Parses input in the format: @@ -13,6 +13,7 @@ use syn::{Expr, ExprLit, ExprPath, Lit, Result as SynResult, Token}; pub struct RawTranslationArgs { /// Language specification (literal or expression) language: Expr, + /// Argument seprator. _comma: Token![,], /// Static marker for path analysis static_marker: Option, @@ -20,8 +21,25 @@ pub struct RawTranslationArgs { path: Expr, } +/// A TranslationPathType is a wrapper to the path +/// argument, this provides feedback on how to +/// interact with the user provided path. pub enum TranslationPathType { + /// An OnScopeExpression represents + /// any expresion that evaluates to + /// a string, that expression is dynamic + /// and it's evaluated on runtime, so + /// the means to generate checks and errors + /// are limited. OnScopeExpression(TokenStream), + + /// A CompileTimePath represents a path + /// that's prefixed with the `static` + /// keyword, if the passed expression is + /// this one we read the translations + /// directly, if a translation for that + /// path does not exist, a compile time + /// error is generated. CompileTimePath(String), } @@ -95,30 +113,9 @@ impl Into for RawTranslationArgs { } } -pub fn translation_macro(args: TranslationArgs) -> TokenStream { - if let TranslationPathType::CompileTimePath(path) = args.path { - if let TranslationLanguageType::CompileTimeLiteral(lang) = args.language { - return match load_translation_static(&lang, &path) { - Ok(Some(translation)) => quote!(#translation).into(), - - Ok(None) => { - let lang_name = Iso639a::from_str(&lang) - .map(|l| l.to_string()) - .unwrap_or_else(|_| lang.clone()); - - let error_fmt = format!( - "The language '{lang}' ({lang_name}) is not available for '{path}'" - ); - - quote!(compile_error!(#error_fmt)).into() - } - - Err(err) => { - let error_fmt = err.to_string(); - quote!(compile_error!(#error_fmt)).into() - } - }; - } +pub fn translation_macro(_args: TranslationArgs) -> TokenStream { + if let TranslationPathType::CompileTimePath(path) = _args.path { + load_translation_static(None, path); } quote!("").into() diff --git a/src/translations.rs b/src/translations.rs deleted file mode 100644 index c213402..0000000 --- a/src/translations.rs +++ /dev/null @@ -1,223 +0,0 @@ -//! Translation handling module that loads and validates translation files, -//! and provides functionality to retrieve translations based on language and path. - -use crate::config::{ConfigError, SeekMode, TranslationOverlap, load_config}; -use crate::languages::Iso639a; -use crate::macros::{TranslationLanguageType, TranslationPathType}; -use proc_macro::TokenStream; -use std::fs::{read_dir, read_to_string}; -use std::io::Error as IoError; -use std::sync::OnceLock; -use strum::IntoEnumIterator; -use thiserror::Error; -use toml::{Table, Value, de::Error as TomlError}; - -/// Errors that can occur during translation processing. -#[derive(Error, Debug)] -pub enum TranslationError { - /// Configuration-related error - #[error("{0:#}")] - Config(#[from] ConfigError), - - /// IO operation error - #[error("An IO Error occurred: {0:#}")] - Io(#[from] IoError), - - /// Path contains invalid Unicode characters - #[error("The path contains invalid unicode characters.")] - InvalidUnicode, - - /// TOML parsing error with location information - #[error( - "Toml parse error '{}'{}", - .0.message(), - .0.span() - .map(|l| format!(" in {}:{}:{}", .1, l.start, l.end)) - .unwrap_or("".into()) - )] - ParseToml(TomlError, String), - - /// Invalid language code error with suggestions - #[error( - "'{0}' is not valid ISO 639-1. These are some valid languages including '{0}':\n{sorted_list}", - sorted_list = .1.join(",\n") - )] - InvalidLanguage(String, Vec), - - /// Invalid TOML structure in specific file - #[error( - "Invalid TOML structure in file {0}: Translation files must contain either nested tables or language translations, but not both at the same level." - )] - InvalidTomlFormat(String), -} - -/// Global cache for loaded translations -static TRANSLATIONS: OnceLock> = OnceLock::new(); - -/// Recursively walk a directory and collect all file paths -/// -/// # Implementation Details -/// Uses iterative depth-first search to avoid stack overflow -/// Handles filesystem errors and invalid Unicode paths -fn walk_dir(path: &str) -> Result, TranslationError> { - let mut stack = vec![path.to_string()]; - let mut result = Vec::new(); - - // Use iterative approach to avoid recursion depth limits - 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(TranslationError::InvalidUnicode)? - .to_string(), - ); - } else { - result.push(path.to_string_lossy().to_string()); - } - } - } - - Ok(result) -} - -/// Validate TOML structure for translation files -/// -/// # Validation Rules -/// 1. Nodes must be either all tables or all translations -/// 2. Translation keys must be valid ISO 639-1 codes -/// 3. Template brackets must be balanced in translation values -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 contains_translation || !translations_valid(table) { - return false; - } - contains_table = true; - } - Value::String(translation) => { - if contains_table || !Iso639a::is_valid(key) { - return false; - } - - // Check balanced template delimiters - let balance = translation.chars().fold(0i32, |acc, c| match c { - '{' => acc + 1, - '}' => acc - 1, - _ => acc, - }); - if balance != 0 { - return false; - } - - contains_translation = true; - } - _ => return false, - } - } - true -} - -/// Load translations from configured directory with thread-safe caching -/// -/// # Returns -/// Reference to loaded translations or TranslationError -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())?; - - // Sort paths case-insensitively - 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| { - let content = read_to_string(path)?; - let table = content - .parse::
() - .map_err(|err| TranslationError::ParseToml(err, path.clone()))?; - - if !translations_valid(&table) { - return Err(TranslationError::InvalidTomlFormat(path.clone())); - } - - Ok(table) - }) - .collect::, TranslationError>>()?; - - Ok(TRANSLATIONS.get_or_init(|| translations)) -} - -/// Load translation for given language and path -/// -/// # Arguments -/// * `lang` - ISO 639-1 language code -/// * `path` - Dot-separated translation path -/// -/// # Returns -/// Option with translation or TranslationError -pub fn load_translation_static(lang: &str, path: &str) -> Result, TranslationError> { - let translations = load_translations()?; - let config = load_config()?; - - if !Iso639a::is_valid(lang) { - let lang_lower = lang.to_lowercase(); - - let similarities = Iso639a::iter() - .filter(|lang| format!("{lang:?}").to_lowercase().contains(&lang_lower)) - .map(|lang| format!("{} ({lang:#})", format!("{lang:?}").to_lowercase())) - .collect::>(); - - return Err(TranslationError::InvalidLanguage( - lang.to_string(), - similarities, - )); - } - - let mut chosen_translation = None; - for translation in translations { - if let Some(value) = path - .split('.') - .try_fold(translation, |acc, key| acc.get(key)?.as_table()) - .and_then(|table| table.get(lang)) - { - chosen_translation = Some(value.to_string()); - if matches!(config.overlap(), TranslationOverlap::Ignore) { - break; - } - } - } - - Ok(chosen_translation) -} - -/// Dynamic translation loading for procedural macros -/// -/// # Arguments -/// * `lang` - Language type from macro -/// * `path` - Path type from macro -/// -/// # Returns -/// TokenStream for generated code -pub fn load_translation_dynamic( - lang: TranslationLanguageType, - path: TranslationPathType, -) -> TokenStream { - let lang = lang.dynamic(); - let path = path.dynamic(); - todo!("Implement dynamic translation loading") -} diff --git a/src/translations/errors.rs b/src/translations/errors.rs new file mode 100644 index 0000000..dca1fb7 --- /dev/null +++ b/src/translations/errors.rs @@ -0,0 +1,49 @@ +use crate::{config::ConfigError, languages::Iso639a}; +use std::io::Error as IoError; +use thiserror::Error; +use toml::de::Error as TomlError; + +/// Errors that can occur during translation processing. +#[derive(Error, Debug)] +pub enum TranslationError { + /// Configuration-related error + #[error("{0:#}")] + Config(#[from] ConfigError), + + /// IO operation error + #[error("An IO Error occurred: {0:#}")] + Io(#[from] IoError), + + /// Path contains invalid Unicode characters + #[error("The path contains invalid unicode characters.")] + InvalidUnicode, + + /// TOML parsing error with location information + #[error( + "Toml parse error '{}'{}", + .0.message(), + .0.span() + .map(|l| format!(" in {}:{}:{}", .1, l.start, l.end)) + .unwrap_or("".into()) + )] + ParseToml(TomlError, String), + + /// Invalid language code error with suggestions + #[error( + "'{0}' is not valid ISO 639-1. These are some valid languages including '{0}':\n{sorted_list}", + sorted_list = .1.join(",\n") + )] + InvalidLanguage(String, Vec), + + /// Invalid TOML structure in specific file + #[error( + "Invalid TOML structure in file {0}: Translation files must contain either nested tables or language translations, but not both at the same level." + )] + InvalidTomlFormat(String), + + #[error("The path '{0}' is not found in any of the translation files.")] + PathNotFound(String), + + #[error("The language '{0:?}' ({0:#}) is not available for the '{1}' translation.")] + LanguageNotAvailable(Iso639a, String) +} diff --git a/src/translations/generation.rs b/src/translations/generation.rs new file mode 100644 index 0000000..65ae1b2 --- /dev/null +++ b/src/translations/generation.rs @@ -0,0 +1,127 @@ +use std::collections::HashMap; +use crate::languages::Iso639a; +use proc_macro::TokenStream; +use quote::quote; +use strum::IntoEnumIterator; +use syn::{parse_macro_input, Expr}; +use super::{errors::TranslationError, utils::{load_translations, AssociatedTranslation}}; + +/// This function parses a statically obtained language +/// as an Iso639a enum instance, along this, the validation +/// is also done at parse time. +pub fn load_lang_static(lang: &str) -> Result { + Ok( + lang + .parse::() + .map_err(|_| TranslationError::InvalidLanguage( + lang.to_string(), + Iso639a::get_similarities(lang) + ))? + ) + +} + +/// This function generates a language variable, the only +/// requisite is that the expression evalutes to something +/// that implements Into. +pub fn load_lang_dynamic(lang: TokenStream) -> TokenStream { + let lang = parse_macro_input!(lang as Expr); + + let available_langs = Iso639a::iter() + .map(|language| { + let language = format!("{language:?}"); + + quote! { stringify!(#language), } + }); + + // The `String` explicit type serves as + // expression type checking, we accept `impl Into` + // for any expression that's not static. + quote! { + let language: String = (#lang).into(); + + let valid_lang = vec![#(#available_langs)*] + .iter() + .any(|lang| lang.eq_ignore_ascii_case(&language)); + } + .into() +} + + +pub fn load_translation_static(static_lang: Option, path: String) -> Result { + let mut found_association = None; + + for AssociatedTranslation { translation_table, original_path } in load_translations()? { + let translation_table = path + .split(".") + .try_fold(translation_table, |acc, curr| { + acc + .get(curr) + .map(|new| new.as_table()) + .flatten() + }); + + if let Some(translation_table) = translation_table.cloned() { + found_association = Some(AssociatedTranslation { + translation_table, + original_path: original_path.clone() + }) + } + } + + let found_association = found_association + .ok_or(TranslationError::PathNotFound(path.clone()))?; + + let translation_table = found_association + .translation_table + .into_iter() + .map(|(language, translation)| Some(( + language.to_string(), + translation.as_str()?.to_string() + ))) + .collect::>>() + .ok_or(TranslationError::InvalidTomlFormat( + found_association.original_path + ))?; + + Ok( + if let Some(static_lang) = static_lang { + let translation = translation_table + .iter() + .find(|(language, _)| static_lang.eq_insensitive(language)) + .map(|(_, translation)| translation) + .ok_or(TranslationError::LanguageNotAvailable(static_lang, path))?; + + quote! { stringify!(#translation) } + } else { + let translation_variants = translation_table + .iter() + .map(|(language, translation)| quote! { + stringify!(#language) => Ok(stringify!(#translation)), + }); + + quote! {{ + if valid_lang { + match language { + #(#translation_variants)*, + _ => Err(format!( + "A translation with the language '{}' was not found for the path '{}'", + language, + stringify!(#path) + )) + } + } else { + Err(format!("The language '{}' is not valid ISO 639-1.")) + } + }} + } + .into() + ) +} + +pub fn load_translation_dynamic(static_lang: Option, path: TokenStream) -> Result { + let translations = load_translations()?; + + Ok(quote!("").into()) +} + diff --git a/src/translations/mod.rs b/src/translations/mod.rs new file mode 100644 index 0000000..41dee42 --- /dev/null +++ b/src/translations/mod.rs @@ -0,0 +1,4 @@ + +pub mod errors; +pub mod generation; +pub mod utils; diff --git a/src/translations/utils.rs b/src/translations/utils.rs new file mode 100644 index 0000000..cd91ce0 --- /dev/null +++ b/src/translations/utils.rs @@ -0,0 +1,125 @@ +use crate::config::{SeekMode, load_config}; +use crate::languages::Iso639a; +use std::fs::{read_dir, read_to_string}; +use std::sync::OnceLock; +use toml::{Table, Value}; +use super::errors::TranslationError; + +pub struct AssociatedTranslation { + pub original_path: String, + pub translation_table: Table +} + +/// Global cache for loaded translations +static TRANSLATIONS: OnceLock> = OnceLock::new(); + +/// Recursively walk a directory and collect all file paths +/// +/// # Implementation Details +/// Uses iterative depth-first search to avoid stack overflow +/// Handles filesystem errors and invalid Unicode paths +fn walk_dir(path: &str) -> Result, TranslationError> { + let mut stack = vec![path.to_string()]; + let mut result = Vec::new(); + + // Use iterative approach to avoid recursion depth limits + 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(TranslationError::InvalidUnicode)? + .to_string(), + ); + } else { + result.push(path.to_string_lossy().to_string()); + } + } + } + + Ok(result) +} + +/// Validate TOML structure for translation files +/// +/// # Validation Rules +/// 1. Nodes must be either all tables or all translations +/// 2. Translation keys must be valid ISO 639-1 codes +/// 3. Template brackets must be balanced in translation values +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 contains_translation || !translations_valid(table) { + return false; + } + contains_table = true; + } + Value::String(translation) => { + if contains_table || !Iso639a::is_valid(key) { + return false; + } + + // Check balanced template delimiters + let balance = translation.chars().fold(0i32, |acc, c| match c { + '{' => acc + 1, + '}' => acc - 1, + _ => acc, + }); + if balance != 0 { + return false; + } + + contains_translation = true; + } + _ => return false, + } + } + true +} + +/// Load translations from configured directory with thread-safe caching +/// +/// # Returns +/// Reference to loaded translations or TranslationError +pub 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())?; + + // Sort paths case-insensitively + 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| { + let content = read_to_string(path)?; + let table = content + .parse::
() + .map_err(|err| TranslationError::ParseToml(err, path.clone()))?; + + if !translations_valid(&table) { + return Err(TranslationError::InvalidTomlFormat(path.clone())); + } + + Ok(AssociatedTranslation { + original_path: path.to_string(), + translation_table: table + }) + }) + .collect::, TranslationError>>()?; + + Ok(TRANSLATIONS.get_or_init(|| translations)) +} From 82aeb233d17d5486e0af9ebd88d67e250c501ced Mon Sep 17 00:00:00 2001 From: stifskere Date: Sun, 23 Mar 2025 22:25:49 +0100 Subject: [PATCH 011/228] feat: translation obtention refactoring && workspace creation --- Cargo.lock | 194 +++++++++++++++-- Cargo.toml | 18 +- src/translations/generation.rs | 127 ------------ src/translations/utils.rs | 125 ----------- tests/static.rs | 6 - translatable/Cargo.toml | 10 + translatable/src/lib.rs | 3 + translatable/tests/test.rs | 0 translatable_proc/Cargo.toml | 15 ++ {src => translatable_proc/src/data}/config.rs | 0 translatable_proc/src/data/mod.rs | 3 + translatable_proc/src/data/translations.rs | 195 ++++++++++++++++++ {src => translatable_proc/src}/languages.rs | 22 +- {src => translatable_proc/src}/lib.rs | 6 +- {src => translatable_proc/src}/macros.rs | 30 +-- .../src}/translations/errors.rs | 6 +- .../src/translations/generation.rs | 59 ++++++ .../src}/translations/mod.rs | 1 - 18 files changed, 485 insertions(+), 335 deletions(-) delete mode 100644 src/translations/generation.rs delete mode 100644 src/translations/utils.rs delete mode 100644 tests/static.rs create mode 100644 translatable/Cargo.toml create mode 100644 translatable/src/lib.rs create mode 100644 translatable/tests/test.rs create mode 100644 translatable_proc/Cargo.toml rename {src => translatable_proc/src/data}/config.rs (100%) create mode 100644 translatable_proc/src/data/mod.rs create mode 100644 translatable_proc/src/data/translations.rs rename {src => translatable_proc/src}/languages.rs (95%) rename {src => translatable_proc/src}/lib.rs (82%) rename {src => translatable_proc/src}/macros.rs (80%) rename {src => translatable_proc/src}/translations/errors.rs (84%) create mode 100644 translatable_proc/src/translations/generation.rs rename {src => translatable_proc/src}/translations/mod.rs (71%) diff --git a/Cargo.lock b/Cargo.lock index 017ecf1..42e6357 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + [[package]] name = "hashbrown" version = "0.15.2" @@ -22,14 +28,20 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "indexmap" -version = "2.7.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" dependencies = [ "equivalent", "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" @@ -38,48 +50,66 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "proc-macro2" -version = "1.0.93" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.38" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] [[package]] name = "rustversion" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" +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.218" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.218" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" +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" @@ -113,29 +143,44 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.98" +version = "2.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 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.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", @@ -179,6 +224,14 @@ dependencies = [ [[package]] name = "translatable" version = "0.1.0" +dependencies = [ + "translatable_proc", + "trybuild", +] + +[[package]] +name = "translatable_proc" +version = "0.1.0" dependencies = [ "quote", "serde", @@ -188,17 +241,114 @@ dependencies = [ "toml", ] +[[package]] +name = "trybuild" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ae08be68c056db96f0e6c6dd820727cca756ced9e1f4cc7fdd20e2a55e23898" +dependencies = [ + "glob", + "serde", + "serde_derive", + "serde_json", + "target-triple", + "termcolor", + "toml", +] + [[package]] name = "unicode-ident" -version = "1.0.17" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1" +checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 9444e29..d0c38c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,15 +1,3 @@ -[package] -name = "translatable" -version = "0.1.0" -edition = "2024" - -[lib] -proc-macro = true - -[dependencies] -quote = "1.0.38" -serde = { version = "1.0.218", features = ["derive"] } -strum = { version = "0.27.1", features = ["derive"] } -syn = { version = "2.0.98", features = ["full"] } -thiserror = "2.0.11" -toml = "0.8.20" +[workspace] +resolver = "2" +members = [ "translatable", "translatable_proc" ] diff --git a/src/translations/generation.rs b/src/translations/generation.rs deleted file mode 100644 index 65ae1b2..0000000 --- a/src/translations/generation.rs +++ /dev/null @@ -1,127 +0,0 @@ -use std::collections::HashMap; -use crate::languages::Iso639a; -use proc_macro::TokenStream; -use quote::quote; -use strum::IntoEnumIterator; -use syn::{parse_macro_input, Expr}; -use super::{errors::TranslationError, utils::{load_translations, AssociatedTranslation}}; - -/// This function parses a statically obtained language -/// as an Iso639a enum instance, along this, the validation -/// is also done at parse time. -pub fn load_lang_static(lang: &str) -> Result { - Ok( - lang - .parse::() - .map_err(|_| TranslationError::InvalidLanguage( - lang.to_string(), - Iso639a::get_similarities(lang) - ))? - ) - -} - -/// This function generates a language variable, the only -/// requisite is that the expression evalutes to something -/// that implements Into. -pub fn load_lang_dynamic(lang: TokenStream) -> TokenStream { - let lang = parse_macro_input!(lang as Expr); - - let available_langs = Iso639a::iter() - .map(|language| { - let language = format!("{language:?}"); - - quote! { stringify!(#language), } - }); - - // The `String` explicit type serves as - // expression type checking, we accept `impl Into` - // for any expression that's not static. - quote! { - let language: String = (#lang).into(); - - let valid_lang = vec![#(#available_langs)*] - .iter() - .any(|lang| lang.eq_ignore_ascii_case(&language)); - } - .into() -} - - -pub fn load_translation_static(static_lang: Option, path: String) -> Result { - let mut found_association = None; - - for AssociatedTranslation { translation_table, original_path } in load_translations()? { - let translation_table = path - .split(".") - .try_fold(translation_table, |acc, curr| { - acc - .get(curr) - .map(|new| new.as_table()) - .flatten() - }); - - if let Some(translation_table) = translation_table.cloned() { - found_association = Some(AssociatedTranslation { - translation_table, - original_path: original_path.clone() - }) - } - } - - let found_association = found_association - .ok_or(TranslationError::PathNotFound(path.clone()))?; - - let translation_table = found_association - .translation_table - .into_iter() - .map(|(language, translation)| Some(( - language.to_string(), - translation.as_str()?.to_string() - ))) - .collect::>>() - .ok_or(TranslationError::InvalidTomlFormat( - found_association.original_path - ))?; - - Ok( - if let Some(static_lang) = static_lang { - let translation = translation_table - .iter() - .find(|(language, _)| static_lang.eq_insensitive(language)) - .map(|(_, translation)| translation) - .ok_or(TranslationError::LanguageNotAvailable(static_lang, path))?; - - quote! { stringify!(#translation) } - } else { - let translation_variants = translation_table - .iter() - .map(|(language, translation)| quote! { - stringify!(#language) => Ok(stringify!(#translation)), - }); - - quote! {{ - if valid_lang { - match language { - #(#translation_variants)*, - _ => Err(format!( - "A translation with the language '{}' was not found for the path '{}'", - language, - stringify!(#path) - )) - } - } else { - Err(format!("The language '{}' is not valid ISO 639-1.")) - } - }} - } - .into() - ) -} - -pub fn load_translation_dynamic(static_lang: Option, path: TokenStream) -> Result { - let translations = load_translations()?; - - Ok(quote!("").into()) -} - diff --git a/src/translations/utils.rs b/src/translations/utils.rs deleted file mode 100644 index cd91ce0..0000000 --- a/src/translations/utils.rs +++ /dev/null @@ -1,125 +0,0 @@ -use crate::config::{SeekMode, load_config}; -use crate::languages::Iso639a; -use std::fs::{read_dir, read_to_string}; -use std::sync::OnceLock; -use toml::{Table, Value}; -use super::errors::TranslationError; - -pub struct AssociatedTranslation { - pub original_path: String, - pub translation_table: Table -} - -/// Global cache for loaded translations -static TRANSLATIONS: OnceLock> = OnceLock::new(); - -/// Recursively walk a directory and collect all file paths -/// -/// # Implementation Details -/// Uses iterative depth-first search to avoid stack overflow -/// Handles filesystem errors and invalid Unicode paths -fn walk_dir(path: &str) -> Result, TranslationError> { - let mut stack = vec![path.to_string()]; - let mut result = Vec::new(); - - // Use iterative approach to avoid recursion depth limits - 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(TranslationError::InvalidUnicode)? - .to_string(), - ); - } else { - result.push(path.to_string_lossy().to_string()); - } - } - } - - Ok(result) -} - -/// Validate TOML structure for translation files -/// -/// # Validation Rules -/// 1. Nodes must be either all tables or all translations -/// 2. Translation keys must be valid ISO 639-1 codes -/// 3. Template brackets must be balanced in translation values -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 contains_translation || !translations_valid(table) { - return false; - } - contains_table = true; - } - Value::String(translation) => { - if contains_table || !Iso639a::is_valid(key) { - return false; - } - - // Check balanced template delimiters - let balance = translation.chars().fold(0i32, |acc, c| match c { - '{' => acc + 1, - '}' => acc - 1, - _ => acc, - }); - if balance != 0 { - return false; - } - - contains_translation = true; - } - _ => return false, - } - } - true -} - -/// Load translations from configured directory with thread-safe caching -/// -/// # Returns -/// Reference to loaded translations or TranslationError -pub 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())?; - - // Sort paths case-insensitively - 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| { - let content = read_to_string(path)?; - let table = content - .parse::
() - .map_err(|err| TranslationError::ParseToml(err, path.clone()))?; - - if !translations_valid(&table) { - return Err(TranslationError::InvalidTomlFormat(path.clone())); - } - - Ok(AssociatedTranslation { - original_path: path.to_string(), - translation_table: table - }) - }) - .collect::, TranslationError>>()?; - - Ok(TRANSLATIONS.get_or_init(|| translations)) -} diff --git a/tests/static.rs b/tests/static.rs deleted file mode 100644 index f9e231a..0000000 --- a/tests/static.rs +++ /dev/null @@ -1,6 +0,0 @@ -use translatable::translation; - -#[test] -fn get_salutation() { - translation!("es", static salutation::test); -} diff --git a/translatable/Cargo.toml b/translatable/Cargo.toml new file mode 100644 index 0000000..66b7aa9 --- /dev/null +++ b/translatable/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "translatable" +version = "0.1.0" +edition = "2024" + +[dependencies] +translatable_proc = { path = "../translatable_proc" } + +[dev-dependencies] +trybuild = "1.0.104" diff --git a/translatable/src/lib.rs b/translatable/src/lib.rs new file mode 100644 index 0000000..584b146 --- /dev/null +++ b/translatable/src/lib.rs @@ -0,0 +1,3 @@ + +// re export the macro in the main crate. +pub use translatable_proc::translation; diff --git a/translatable/tests/test.rs b/translatable/tests/test.rs new file mode 100644 index 0000000..e69de29 diff --git a/translatable_proc/Cargo.toml b/translatable_proc/Cargo.toml new file mode 100644 index 0000000..b3c99a0 --- /dev/null +++ b/translatable_proc/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "translatable_proc" +version = "0.1.0" +edition = "2024" + +[lib] +proc-macro = true + +[dependencies] +quote = "1.0.38" +serde = { version = "1.0.218", features = ["derive"] } +strum = { version = "0.27.1", features = ["derive"] } +syn = { version = "2.0.98", features = ["full"] } +thiserror = "2.0.11" +toml = "0.8.20" diff --git a/src/config.rs b/translatable_proc/src/data/config.rs similarity index 100% rename from src/config.rs rename to translatable_proc/src/data/config.rs diff --git a/translatable_proc/src/data/mod.rs b/translatable_proc/src/data/mod.rs new file mode 100644 index 0000000..660a572 --- /dev/null +++ b/translatable_proc/src/data/mod.rs @@ -0,0 +1,3 @@ + +pub mod config; +pub mod translations; diff --git a/translatable_proc/src/data/translations.rs b/translatable_proc/src/data/translations.rs new file mode 100644 index 0000000..d268077 --- /dev/null +++ b/translatable_proc/src/data/translations.rs @@ -0,0 +1,195 @@ +use super::config::{SeekMode, load_config}; +use crate::languages::Iso639a; +use std::collections::{HashMap, VecDeque}; +use std::fs::{read_dir, read_to_string}; +use std::sync::OnceLock; +use strum::ParseError; +use thiserror::Error; +use toml::{Table, Value}; +use crate::translations::errors::TranslationError; + +#[derive(Error, Debug)] +pub enum TransformError { + #[error("A nesting can contain either strings or other nestings, but not both.")] + InvalidNesting, + + #[error("Templates in translations should match '{{' and '}}'")] + UnclosedTemplate, + + #[error("Only strings and objects are allowed for nested objects.")] + InvalidValue, + + #[error("Couldn't parse ISO 639-1 string for translation key")] + LanguageParsing(#[from] ParseError) +} + +pub enum NestingType { + Object(HashMap), + Translation(HashMap) +} + +pub struct AssociatedTranslation { + pub original_path: String, + pub translation_table: NestingType +} + +/// Global cache for loaded translations +static TRANSLATIONS: OnceLock> = OnceLock::new(); + +fn walk_dir(path: &str) -> Result, TranslationError> { + let mut stack = vec![path.to_string()]; + let mut result = Vec::new(); + + // Use iterative approach to avoid recursion depth limits + 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(TranslationError::InvalidUnicode)? + .to_string(), + ); + } else { + result.push(path.to_string_lossy().to_string()); + } + } + } + + Ok(result) +} + +fn templates_valid(translation: &str) -> bool { + let mut nestings = 0; + + for character in translation.chars() { + match character { + '{' => nestings += 1, + '}' => nestings -= 1, + _ => {} + } + } + + nestings == 0 +} + +/// Load translations from configured directory with thread-safe caching +/// +/// # Returns +/// Reference to loaded translations or TranslationError +pub 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())?; + + // Sort paths case-insensitively + 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| { + let table = read_to_string(path)? + .parse::
() + .map_err(|err| TranslationError::ParseToml(err, path.clone()))?; + + Ok(AssociatedTranslation { + original_path: path.to_string(), + translation_table: NestingType::try_from(table) + .map_err(|err| TranslationError::InvalidTomlFormat(err, path.to_string()))? + }) + }) + .collect::, TranslationError>>()?; + + Ok(TRANSLATIONS.get_or_init(|| translations)) +} + +impl NestingType { + pub fn get_path(&self, path: Vec<&str>) -> Option<&HashMap> { + match self { + Self::Object(nested) => { + let (first, rest) = path.split_first()?; + + nested + .get(*first) + .and_then(|n| n.get_path(rest.to_vec())) + }, + + Self::Translation(translation) => { + if path.is_empty() { + return Some(translation) + } + + None + } + } + } +} + +impl TryFrom
for NestingType { + type Error = TransformError; + + fn try_from(value: Table) -> Result { + let mut result = None; + + for (key, value) in value { + match value { + Value::String(translation_value) => { + if result.is_none() { + result = Some(Self::Translation(HashMap::new())); + } + + if !templates_valid(&translation_value) { + return Err(TransformError::UnclosedTemplate); + } + + match result { + Some(Self::Translation(ref mut translation)) => { + translation.insert(key.parse()?, translation_value); + }, + + Some(Self::Object(_)) => { + return Err(TransformError::InvalidNesting); + }, + + None => unreachable!() + } + }, + + Value::Table(nesting_value) => { + if result.is_none() { + result = Some(Self::Object(HashMap::new())); + } + + match result { + Some(Self::Object(ref mut nesting)) => { + nesting.insert(key, Self::try_from(nesting_value)?); + }, + + Some(Self::Translation(_)) => { + return Err(TransformError::InvalidNesting); + }, + + None => unreachable!() + } + }, + + _ => { + return Err(TransformError::InvalidValue) + } + } + } + + match result { + Some(result) => Ok(result), + None => unreachable!() + } + } +} diff --git a/src/languages.rs b/translatable_proc/src/languages.rs similarity index 95% rename from src/languages.rs rename to translatable_proc/src/languages.rs index 9bf094c..a69ef2c 100644 --- a/src/languages.rs +++ b/translatable_proc/src/languages.rs @@ -1,4 +1,3 @@ -use std::str::FromStr; use strum::{Display, EnumIter, EnumString, IntoEnumIterator}; /// ISO 639-1 language code implementation with validation @@ -7,7 +6,7 @@ use strum::{Display, EnumIter, EnumString, IntoEnumIterator}; /// - Case-insensitive parsing /// - Strict validation /// - Complete ISO 639-1 coverage -#[derive(Debug, EnumIter, Display, EnumString)] +#[derive(Debug, EnumIter, Display, EnumString, Eq, Hash, PartialEq)] #[strum(ascii_case_insensitive)] pub enum Iso639a { #[strum(serialize = "Abkhazian", serialize = "ab")] @@ -379,29 +378,16 @@ pub enum Iso639a { } impl Iso639a { - /// Validates if a string represents a valid ISO 639-1 language - /// - /// # Arguments - /// * `lang` - Input string to validate (case-insensitive) - /// - /// # Examples - /// ```ignore - /// assert!(Iso639a::is_valid("en")); - /// assert!(Iso639a::is_valid("English")); - /// assert!(!Iso639a::is_valid("xx")); - /// ``` - pub fn is_valid(lang: &str) -> bool { - Iso639a::from_str(lang).is_ok() - } - pub fn get_similarities(lang: &str) -> Vec { Self::iter() .map(|variant| variant.to_string()) .filter(|variant| variant.contains(lang)) .collect() } +} - pub fn eq_insensitive(&self, other: &str) -> bool { +impl PartialEq for Iso639a { + fn eq(&self, other: &String) -> bool { format!("{self:?}").to_lowercase() == other.to_lowercase() } } diff --git a/src/lib.rs b/translatable_proc/src/lib.rs similarity index 82% rename from src/lib.rs rename to translatable_proc/src/lib.rs index ae1d818..78bfea2 100644 --- a/src/lib.rs +++ b/translatable_proc/src/lib.rs @@ -6,11 +6,11 @@ //! - Configurable loading strategies //! - Procedural macro for compile-time checking -use macros::{RawTranslationArgs, translation_macro}; +use macros::{RawMacroArgs, translation_macro}; use proc_macro::TokenStream; use syn::parse_macro_input; -mod config; +mod data; mod languages; mod macros; mod translations; @@ -27,5 +27,5 @@ mod translations; /// - Translation path (supports static analysis) #[proc_macro] pub fn translation(input: TokenStream) -> TokenStream { - translation_macro(parse_macro_input!(input as RawTranslationArgs).into()) + translation_macro(parse_macro_input!(input as RawMacroArgs).into()) } diff --git a/src/macros.rs b/translatable_proc/src/macros.rs similarity index 80% rename from src/macros.rs rename to translatable_proc/src/macros.rs index b55d3ec..c7f8cc1 100644 --- a/src/macros.rs +++ b/translatable_proc/src/macros.rs @@ -10,7 +10,7 @@ use crate::translations::generation::load_translation_static; /// /// Parses input in the format: /// `(language_expression, static translation_path)` -pub struct RawTranslationArgs { +pub struct RawMacroArgs { /// Language specification (literal or expression) language: Expr, /// Argument seprator. @@ -24,7 +24,7 @@ pub struct RawTranslationArgs { /// A TranslationPathType is a wrapper to the path /// argument, this provides feedback on how to /// interact with the user provided path. -pub enum TranslationPathType { +pub enum PathType { /// An OnScopeExpression represents /// any expresion that evaluates to /// a string, that expression is dynamic @@ -43,19 +43,19 @@ pub enum TranslationPathType { CompileTimePath(String), } -pub enum TranslationLanguageType { +pub enum LanguageType { OnScopeExpression(TokenStream), CompileTimeLiteral(String), } pub struct TranslationArgs { - language: TranslationLanguageType, - path: TranslationPathType, + language: LanguageType, + path: PathType, } -impl Parse for RawTranslationArgs { +impl Parse for RawMacroArgs { fn parse(input: ParseStream) -> SynResult { - Ok(RawTranslationArgs { + Ok(RawMacroArgs { language: input.parse()?, _comma: input.parse()?, static_marker: input.parse()?, @@ -64,7 +64,7 @@ impl Parse for RawTranslationArgs { } } -impl TranslationPathType { +impl PathType { pub fn dynamic(self) -> TokenStream { match self { Self::OnScopeExpression(tokens) => tokens, @@ -73,7 +73,7 @@ impl TranslationPathType { } } -impl TranslationLanguageType { +impl LanguageType { pub fn dynamic(self) -> TokenStream { match self { Self::OnScopeExpression(tokens) => tokens, @@ -82,7 +82,7 @@ impl TranslationLanguageType { } } -impl Into for RawTranslationArgs { +impl Into for RawMacroArgs { fn into(self) -> TranslationArgs { let is_path_static = self.static_marker.is_some(); @@ -91,13 +91,13 @@ impl Into for RawTranslationArgs { Expr::Lit(ExprLit { lit: Lit::Str(lit_str), .. - }) => TranslationLanguageType::CompileTimeLiteral(lit_str.value()), - other => TranslationLanguageType::OnScopeExpression(quote!(#other).into()), + }) => LanguageType::CompileTimeLiteral(lit_str.value()), + other => LanguageType::OnScopeExpression(quote!(#other).into()), }, path: match self.path { Expr::Path(ExprPath { path, .. }) if is_path_static => { - TranslationPathType::CompileTimePath( + PathType::CompileTimePath( path.segments .iter() .map(|s| s.ident.to_string()) @@ -107,14 +107,14 @@ impl Into for RawTranslationArgs { ) } - path => TranslationPathType::OnScopeExpression(quote!(#path).into()), + path => PathType::OnScopeExpression(quote!(#path).into()), }, } } } pub fn translation_macro(_args: TranslationArgs) -> TokenStream { - if let TranslationPathType::CompileTimePath(path) = _args.path { + if let PathType::CompileTimePath(path) = _args.path { load_translation_static(None, path); } diff --git a/src/translations/errors.rs b/translatable_proc/src/translations/errors.rs similarity index 84% rename from src/translations/errors.rs rename to translatable_proc/src/translations/errors.rs index dca1fb7..7430132 100644 --- a/src/translations/errors.rs +++ b/translatable_proc/src/translations/errors.rs @@ -1,4 +1,4 @@ -use crate::{config::ConfigError, languages::Iso639a}; +use crate::{data::{config::ConfigError, translations::TransformError}, languages::Iso639a}; use std::io::Error as IoError; use thiserror::Error; use toml::de::Error as TomlError; @@ -37,9 +37,9 @@ pub enum TranslationError { /// Invalid TOML structure in specific file #[error( - "Invalid TOML structure in file {0}: Translation files must contain either nested tables or language translations, but not both at the same level." + "Invalid TOML structure in file {1}: {0}" )] - InvalidTomlFormat(String), + InvalidTomlFormat(TransformError, String), #[error("The path '{0}' is not found in any of the translation files.")] PathNotFound(String), diff --git a/translatable_proc/src/translations/generation.rs b/translatable_proc/src/translations/generation.rs new file mode 100644 index 0000000..3dac323 --- /dev/null +++ b/translatable_proc/src/translations/generation.rs @@ -0,0 +1,59 @@ +use std::collections::HashMap; +use crate::languages::Iso639a; +use proc_macro::TokenStream; +use quote::quote; +use strum::IntoEnumIterator; +use syn::{parse_macro_input, Expr}; +use crate::data::translations::{load_translations, AssociatedTranslation}; +use super::errors::TranslationError; + +/// This function parses a statically obtained language +/// as an Iso639a enum instance, along this, the validation +/// is also done at parse time. +pub fn load_lang_static(lang: &str) -> Result { + Ok( + lang + .parse::() + .map_err(|_| TranslationError::InvalidLanguage( + lang.to_string(), + Iso639a::get_similarities(lang) + ))? + ) + +} + +/// This function generates a language variable, the only +/// requisite is that the expression evalutes to something +/// that implements Into. +pub fn load_lang_dynamic(lang: TokenStream) -> TokenStream { + let lang = parse_macro_input!(lang as Expr); + + let available_langs = Iso639a::iter() + .map(|language| { + let language = format!("{language:?}"); + + quote! { stringify!(#language), } + }); + + // The `String` explicit type serves as + // expression type checking, we accept `impl Into` + // for any expression that's not static. + quote! { + let language: String = (#lang).into(); + + let valid_lang = vec![#(#available_langs)*] + .iter() + .any(|lang| lang.eq_ignore_ascii_case(&language)); + } + .into() +} + + +pub fn load_translation_static(static_lang: Option, path: String) -> Result { + todo!() +} + +pub fn load_translation_dynamic(static_lang: Option, path: TokenStream) -> Result { + todo!() +} + diff --git a/src/translations/mod.rs b/translatable_proc/src/translations/mod.rs similarity index 71% rename from src/translations/mod.rs rename to translatable_proc/src/translations/mod.rs index 41dee42..037e38a 100644 --- a/src/translations/mod.rs +++ b/translatable_proc/src/translations/mod.rs @@ -1,4 +1,3 @@ pub mod errors; pub mod generation; -pub mod utils; From 6c4e9e26ff2eb44ec0206c48d589f80bd973b649 Mon Sep 17 00:00:00 2001 From: stifskere Date: Mon, 24 Mar 2025 19:26:08 +0100 Subject: [PATCH 012/228] feat: add Into for NestingType and refactor what's there --- Cargo.lock | 1 + translatable_proc/Cargo.toml | 1 + translatable_proc/src/data/translations.rs | 58 ++++++++++++++- translatable_proc/src/languages.rs | 2 +- translatable_proc/src/translations/errors.rs | 6 +- .../src/translations/generation.rs | 73 +++++++++++++++---- 6 files changed, 120 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 42e6357..c850628 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -233,6 +233,7 @@ dependencies = [ name = "translatable_proc" version = "0.1.0" dependencies = [ + "proc-macro2", "quote", "serde", "strum", diff --git a/translatable_proc/Cargo.toml b/translatable_proc/Cargo.toml index b3c99a0..0b6867f 100644 --- a/translatable_proc/Cargo.toml +++ b/translatable_proc/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" proc-macro = true [dependencies] +proc-macro2 = "1.0.94" quote = "1.0.38" serde = { version = "1.0.218", features = ["derive"] } strum = { version = "0.27.1", features = ["derive"] } diff --git a/translatable_proc/src/data/translations.rs b/translatable_proc/src/data/translations.rs index d268077..cf4d237 100644 --- a/translatable_proc/src/data/translations.rs +++ b/translatable_proc/src/data/translations.rs @@ -1,9 +1,13 @@ use super::config::{SeekMode, load_config}; use crate::languages::Iso639a; -use std::collections::{HashMap, VecDeque}; +use std::clone; +use std::collections::HashMap; use std::fs::{read_dir, read_to_string}; use std::sync::OnceLock; +use proc_macro2::{Span, TokenStream}; +use quote::quote; use strum::ParseError; +use syn::LitStr; use thiserror::Error; use toml::{Table, Value}; use crate::translations::errors::TranslationError; @@ -23,14 +27,15 @@ pub enum TransformError { LanguageParsing(#[from] ParseError) } +#[derive(Clone)] pub enum NestingType { Object(HashMap), Translation(HashMap) } pub struct AssociatedTranslation { - pub original_path: String, - pub translation_table: NestingType + original_path: String, + translation_table: NestingType } /// Global cache for loaded translations @@ -133,6 +138,41 @@ impl NestingType { } } +impl Into for NestingType { + fn into(self) -> TokenStream { + match self { + Self::Object(nesting) => { + let entries = nesting + .into_iter() + .map(|(key, value)| -> TokenStream { + let key = LitStr::new(&key, Span::call_site()); + let value: TokenStream = value.into(); + quote! { (#key.to_string(), #value) } + }); + + quote! { + NestingType::Object(vec![#(#entries),*].into_iter().collect()) + } + }, + + NestingType::Translation(translation) => { + let entries = translation + .into_iter() + .map(|(lang, value)| { + let lang = LitStr::new(&lang.to_string(), Span::call_site()); + let value = LitStr::new(&value, Span::call_site()); + + quote! { (#lang.to_string(), #value.to_string()) } + }); + + quote! { + NestingType::Translation(vec![#(#entries),*].into_iter().collect()) + } + } + } + } +} + impl TryFrom
for NestingType { type Error = TransformError; @@ -193,3 +233,15 @@ impl TryFrom
for NestingType { } } } + +impl AssociatedTranslation { + #[allow(unused)] + pub fn original_path(&self) -> &str { + &self.original_path + } + + #[allow(unused)] + pub fn translation_table(&self) -> &NestingType { + &self.translation_table + } +} diff --git a/translatable_proc/src/languages.rs b/translatable_proc/src/languages.rs index a69ef2c..0a4c33c 100644 --- a/translatable_proc/src/languages.rs +++ b/translatable_proc/src/languages.rs @@ -6,7 +6,7 @@ use strum::{Display, EnumIter, EnumString, IntoEnumIterator}; /// - Case-insensitive parsing /// - Strict validation /// - Complete ISO 639-1 coverage -#[derive(Debug, EnumIter, Display, EnumString, Eq, Hash, PartialEq)] +#[derive(Debug, Clone, EnumIter, Display, EnumString, Eq, Hash, PartialEq)] #[strum(ascii_case_insensitive)] pub enum Iso639a { #[strum(serialize = "Abkhazian", serialize = "ab")] diff --git a/translatable_proc/src/translations/errors.rs b/translatable_proc/src/translations/errors.rs index 7430132..f9cb380 100644 --- a/translatable_proc/src/translations/errors.rs +++ b/translatable_proc/src/translations/errors.rs @@ -1,5 +1,6 @@ use crate::{data::{config::ConfigError, translations::TransformError}, languages::Iso639a}; use std::io::Error as IoError; +use syn::Error as SynError; use thiserror::Error; use toml::de::Error as TomlError; @@ -45,5 +46,8 @@ pub enum TranslationError { PathNotFound(String), #[error("The language '{0:?}' ({0:#}) is not available for the '{1}' translation.")] - LanguageNotAvailable(Iso639a, String) + LanguageNotAvailable(Iso639a, String), + + #[error("Error parsing macro.")] + MacroError(#[from] SynError) } diff --git a/translatable_proc/src/translations/generation.rs b/translatable_proc/src/translations/generation.rs index 3dac323..e488572 100644 --- a/translatable_proc/src/translations/generation.rs +++ b/translatable_proc/src/translations/generation.rs @@ -1,10 +1,9 @@ -use std::collections::HashMap; use crate::languages::Iso639a; -use proc_macro::TokenStream; +use proc_macro2::TokenStream; use quote::quote; use strum::IntoEnumIterator; -use syn::{parse_macro_input, Expr}; -use crate::data::translations::{load_translations, AssociatedTranslation}; +use syn::{parse2, Expr}; +use crate::data::translations::load_translations; use super::errors::TranslationError; /// This function parses a statically obtained language @@ -25,8 +24,8 @@ pub fn load_lang_static(lang: &str) -> Result { /// This function generates a language variable, the only /// requisite is that the expression evalutes to something /// that implements Into. -pub fn load_lang_dynamic(lang: TokenStream) -> TokenStream { - let lang = parse_macro_input!(lang as Expr); +pub fn load_lang_dynamic(lang: TokenStream) -> Result { + let lang: Expr = parse2(lang)?; let available_langs = Iso639a::iter() .map(|language| { @@ -38,22 +37,64 @@ pub fn load_lang_dynamic(lang: TokenStream) -> TokenStream { // The `String` explicit type serves as // expression type checking, we accept `impl Into` // for any expression that's not static. - quote! { - let language: String = (#lang).into(); - - let valid_lang = vec![#(#available_langs)*] - .iter() - .any(|lang| lang.eq_ignore_ascii_case(&language)); - } - .into() + Ok( + quote! { + let language: String = (#lang).into(); + + let valid_lang = vec![#(#available_langs)*] + .iter() + .any(|lang| lang.eq_ignore_ascii_case(&language)); + } + ) } pub fn load_translation_static(static_lang: Option, path: String) -> Result { - todo!() + let translation_object = load_translations()? + .iter() + .find_map(|association| association + .translation_table() + .get_path( + path + .split(".") + .collect() + ) + ) + .ok_or(TranslationError::PathNotFound(path.to_string()))?; + + Ok( + match static_lang { + Some(language) => { + let translation = translation_object + .get(&language) + .ok_or(TranslationError::LanguageNotAvailable(language, path))?; + + quote! { stringify!(#translation) } + }, + + None => { + quote! { + if valid_lang { + + } else { + + } + } + } + } + ) } pub fn load_translation_dynamic(static_lang: Option, path: TokenStream) -> Result { - todo!() + let translation = load_translations()? + .into_iter() + .map(|association| association + .translation_table() + .clone() + .into() + ) + .collect::>(); + + Ok(quote! {}) } From 20ce1c5427b225a6a5a966cef933edc10b2af12f Mon Sep 17 00:00:00 2001 From: stifskere Date: Mon, 24 Mar 2025 20:56:27 +0100 Subject: [PATCH 013/228] feat: simplified load_translation_static --- translatable_proc/src/translations/generation.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/translatable_proc/src/translations/generation.rs b/translatable_proc/src/translations/generation.rs index e488572..1c0265e 100644 --- a/translatable_proc/src/translations/generation.rs +++ b/translatable_proc/src/translations/generation.rs @@ -40,6 +40,7 @@ pub fn load_lang_dynamic(lang: TokenStream) -> Result, path: String) -> Re }, None => { + let translation_object = translation_object + .iter() + .map(|(key, value)| { + let key = format!("{key:?}").to_lowercase(); + quote! { (stringify!(#key), stringify!(#value)) } + }); + quote! { if valid_lang { - + Ok(vec![#(#translation_object),*] + .iter() + .collect::>() + .get(&language)) } else { - + Err(translatable::Error::InvalidLanguage(language)) } } } From 455cd5d59d09f7dc1eeaf9ac2d82d3143c40d065 Mon Sep 17 00:00:00 2001 From: stifskere Date: Mon, 24 Mar 2025 22:19:26 +0100 Subject: [PATCH 014/228] feat: exported enums for runtime handling --- Cargo.lock | 1 + translatable/Cargo.toml | 1 + translatable/src/lib.rs | 36 +++++++++++++++++++ translatable_proc/src/data/translations.rs | 4 +-- .../src/translations/generation.rs | 5 +-- 5 files changed, 43 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c850628..bc813ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -225,6 +225,7 @@ dependencies = [ name = "translatable" version = "0.1.0" dependencies = [ + "thiserror", "translatable_proc", "trybuild", ] diff --git a/translatable/Cargo.toml b/translatable/Cargo.toml index 66b7aa9..f003659 100644 --- a/translatable/Cargo.toml +++ b/translatable/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +thiserror = "2.0.12" translatable_proc = { path = "../translatable_proc" } [dev-dependencies] diff --git a/translatable/src/lib.rs b/translatable/src/lib.rs index 584b146..4ee134b 100644 --- a/translatable/src/lib.rs +++ b/translatable/src/lib.rs @@ -1,3 +1,39 @@ +use thiserror::Error; // re export the macro in the main crate. pub use translatable_proc::translation; + +/// This error is used on results for the +/// translation procedural macro, the macro +/// will return a Result, Error>, +/// when there is a dynamic expression to resolve. +/// +/// For example, if the language is a dynamic expression +/// meaning it's not a literal &'static str, and it evaluates +/// on runtime, if the runtime evaluation is invalid because +/// the language does not match the ISO 639-1 specification +/// or something else, the translation macro will return an +/// Error::InvalidLanguage. +/// +/// For more information on the possible errors read each +/// enum branch documentation. +#[derive(Error, Debug)] +pub enum Error { + #[error("The language '{0}' is invalid.")] + InvalidLanguage(String), + + #[error("The langauge '{0}' is not available for the path '{1}'")] + LanguageNotAvailable(String, String) +} + +/// This module is for internal usage, it's members +/// are not documented, and there is no support on +/// using it. +pub mod internal { + use std::collections::HashMap; + + pub enum NestingType { + Object(HashMap), + Translation(HashMap) + } +} diff --git a/translatable_proc/src/data/translations.rs b/translatable_proc/src/data/translations.rs index cf4d237..774e7c2 100644 --- a/translatable_proc/src/data/translations.rs +++ b/translatable_proc/src/data/translations.rs @@ -151,7 +151,7 @@ impl Into for NestingType { }); quote! { - NestingType::Object(vec![#(#entries),*].into_iter().collect()) + translatable::internal::NestingType::Object(vec![#(#entries),*].into_iter().collect()) } }, @@ -166,7 +166,7 @@ impl Into for NestingType { }); quote! { - NestingType::Translation(vec![#(#entries),*].into_iter().collect()) + translatable::internal::NestingType::Translation(vec![#(#entries),*].into_iter().collect()) } } } diff --git a/translatable_proc/src/translations/generation.rs b/translatable_proc/src/translations/generation.rs index 1c0265e..3a44fd2 100644 --- a/translatable_proc/src/translations/generation.rs +++ b/translatable_proc/src/translations/generation.rs @@ -83,10 +83,11 @@ pub fn load_translation_static(static_lang: Option, path: String) -> Re quote! { if valid_lang { - Ok(vec![#(#translation_object),*] + vec![#(#translation_object),*] .iter() .collect::>() - .get(&language)) + .get(&language) + .ok_or(translatable::Error::LanguageNotAvailable(language, stringify!(#path).to_string())) } else { Err(translatable::Error::InvalidLanguage(language)) } From 603eac570ee88344c1a239973e7869e18e2e0d3a Mon Sep 17 00:00:00 2001 From: stifskere Date: Tue, 25 Mar 2025 15:34:28 +0100 Subject: [PATCH 015/228] feat: finished generation functions --- translatable/src/lib.rs | 22 ++++++++++ translatable_proc/src/data/translations.rs | 1 - .../src/translations/generation.rs | 44 +++++++++++++++++-- 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/translatable/src/lib.rs b/translatable/src/lib.rs index 4ee134b..5133ab5 100644 --- a/translatable/src/lib.rs +++ b/translatable/src/lib.rs @@ -36,4 +36,26 @@ pub mod internal { Object(HashMap), Translation(HashMap) } + + impl NestingType { + pub fn get_path(&self, path: Vec<&str>) -> Option<&HashMap> { + match self { + Self::Object(nested) => { + let (first, rest) = path.split_first()?; + + nested + .get(*first) + .and_then(|n| n.get_path(rest.to_vec())) + }, + + Self::Translation(translation) => { + if path.is_empty() { + return Some(translation) + } + + None + } + } + } + } } diff --git a/translatable_proc/src/data/translations.rs b/translatable_proc/src/data/translations.rs index 774e7c2..b36e907 100644 --- a/translatable_proc/src/data/translations.rs +++ b/translatable_proc/src/data/translations.rs @@ -1,6 +1,5 @@ use super::config::{SeekMode, load_config}; use crate::languages::Iso639a; -use std::clone; use std::collections::HashMap; use std::fs::{read_dir, read_to_string}; use std::sync::OnceLock; diff --git a/translatable_proc/src/translations/generation.rs b/translatable_proc/src/translations/generation.rs index 3a44fd2..5efa892 100644 --- a/translatable_proc/src/translations/generation.rs +++ b/translatable_proc/src/translations/generation.rs @@ -18,7 +18,6 @@ pub fn load_lang_static(lang: &str) -> Result { Iso639a::get_similarities(lang) ))? ) - } /// This function generates a language variable, the only @@ -98,7 +97,7 @@ pub fn load_translation_static(static_lang: Option, path: String) -> Re } pub fn load_translation_dynamic(static_lang: Option, path: TokenStream) -> Result { - let translation = load_translations()? + let nestings = load_translations()? .into_iter() .map(|association| association .translation_table() @@ -107,6 +106,45 @@ pub fn load_translation_dynamic(static_lang: Option, path: TokenStream) ) .collect::>(); - Ok(quote! {}) + let translation_quote = quote! { + let path: String = #path.into(); + + let translation = vec![#(#nestings),*] + .find_map(|nesting| nesting + .get_path( + stringify!(path) + .split(".") + .collect() + ) + ); + }; + + Ok(match static_lang { + Some(language) => { + let language = format!("{language:?}"); + + quote! { + #translation_quote + + translation + .and_then(|translation| translation.get(stringify!(#language))) + .ok_or(translatable::Error::LanguageNotAvailable(stringify!(#language).to_string(), path)) + } + }, + + None => { + quote! { + #translation_quote + + if valid_lang { + translation + .and_then(|translation| translation.get(&language)) + .ok_or(translatable::Error::LanguageNotAvailable(language, path)) + } else { + Err(translatable::Error::InvalidLanguage(language)) + } + } + } + }) } From aae5961c8ad1a899abf4f9ad0661fbc4f219528d Mon Sep 17 00:00:00 2001 From: stifskere Date: Tue, 25 Mar 2025 16:05:30 +0100 Subject: [PATCH 016/228] feat: first version of translation macros --- translatable_proc/src/lib.rs | 2 +- translatable_proc/src/macros.rs | 83 ++++++++++++++++----------------- 2 files changed, 41 insertions(+), 44 deletions(-) diff --git a/translatable_proc/src/lib.rs b/translatable_proc/src/lib.rs index 78bfea2..a6c2b93 100644 --- a/translatable_proc/src/lib.rs +++ b/translatable_proc/src/lib.rs @@ -27,5 +27,5 @@ mod translations; /// - Translation path (supports static analysis) #[proc_macro] pub fn translation(input: TokenStream) -> TokenStream { - translation_macro(parse_macro_input!(input as RawMacroArgs).into()) + translation_macro(parse_macro_input!(input as RawMacroArgs).into()).into() } diff --git a/translatable_proc/src/macros.rs b/translatable_proc/src/macros.rs index c7f8cc1..99c6e10 100644 --- a/translatable_proc/src/macros.rs +++ b/translatable_proc/src/macros.rs @@ -1,10 +1,9 @@ -use proc_macro::TokenStream; +use proc_macro2::TokenStream; use quote::quote; use syn::parse::{Parse, ParseStream}; use syn::token::Static; use syn::{Expr, ExprLit, ExprPath, Lit, Result as SynResult, Token}; - -use crate::translations::generation::load_translation_static; +use crate::translations::generation::{load_lang_dynamic, load_lang_static, load_translation_dynamic, load_translation_static}; /// Internal representation of macro arguments before processing /// @@ -21,25 +20,8 @@ pub struct RawMacroArgs { path: Expr, } -/// A TranslationPathType is a wrapper to the path -/// argument, this provides feedback on how to -/// interact with the user provided path. pub enum PathType { - /// An OnScopeExpression represents - /// any expresion that evaluates to - /// a string, that expression is dynamic - /// and it's evaluated on runtime, so - /// the means to generate checks and errors - /// are limited. OnScopeExpression(TokenStream), - - /// A CompileTimePath represents a path - /// that's prefixed with the `static` - /// keyword, if the passed expression is - /// this one we read the translations - /// directly, if a translation for that - /// path does not exist, a compile time - /// error is generated. CompileTimePath(String), } @@ -64,24 +46,6 @@ impl Parse for RawMacroArgs { } } -impl PathType { - pub fn dynamic(self) -> TokenStream { - match self { - Self::OnScopeExpression(tokens) => tokens, - Self::CompileTimePath(cmp_val) => quote!(#cmp_val).into(), - } - } -} - -impl LanguageType { - pub fn dynamic(self) -> TokenStream { - match self { - Self::OnScopeExpression(tokens) => tokens, - Self::CompileTimeLiteral(cmp_val) => quote!(#cmp_val).into(), - } - } -} - impl Into for RawMacroArgs { fn into(self) -> TranslationArgs { let is_path_static = self.static_marker.is_some(); @@ -113,10 +77,43 @@ impl Into for RawMacroArgs { } } -pub fn translation_macro(_args: TranslationArgs) -> TokenStream { - if let PathType::CompileTimePath(path) = _args.path { - load_translation_static(None, path); - } +pub fn translation_macro(args: TranslationArgs) -> TokenStream { + let TranslationArgs { language, path } = args; + + let (lang_expr, static_lang) = match language { + LanguageType::CompileTimeLiteral(lang) => ( + None, + Some(load_lang_static(&lang).ok()).flatten(), + ), + LanguageType::OnScopeExpression(lang) => (Some(load_lang_dynamic(lang)), None), + }; - quote!("").into() + let translation_expr = match path { + PathType::CompileTimePath(p) => load_translation_static(static_lang, p), + PathType::OnScopeExpression(p) => load_translation_dynamic(static_lang, p), + }; + + match (lang_expr, translation_expr) { + (Some(lang), Ok(trans)) => { + match lang { + Ok(lang) => { + quote! { + #lang + #trans + } + }, + + Err(e) => { + let e = format!("{e:#}"); + quote! { compile_error!{#e} } + } + } + }, + (None, Ok(trans)) => trans, + (_, Err(e)) => { + let e = format!("{e:#}"); + quote! { compile_error!(#e) } + }, + } + .into() } From fbdb9f82fdc5c06f6f8c24f1f7ff0b6fd106d524 Mon Sep 17 00:00:00 2001 From: stifskere Date: Tue, 25 Mar 2025 18:27:44 +0100 Subject: [PATCH 017/228] chore: tests pass --- translatable/tests/test.rs | 31 +++++++++++++ translatable_proc/src/data/translations.rs | 2 +- translatable_proc/src/macros.rs | 4 +- .../src/translations/generation.rs | 43 +++++++++++-------- 4 files changed, 60 insertions(+), 20 deletions(-) diff --git a/translatable/tests/test.rs b/translatable/tests/test.rs index e69de29..2585cff 100644 --- a/translatable/tests/test.rs +++ b/translatable/tests/test.rs @@ -0,0 +1,31 @@ +use translatable::translation; + +#[test] +fn both_static() { + let result = translation!("es", static salutation::test); + + assert!(result == "Hola") +} + +#[test] +fn language_static_path_dynamic() { + let result = translation!("es", "salutation.test"); + + assert!(result.unwrap() == "Hola".to_string()) +} + +#[test] +fn language_dynamic_path_static() { + let language = "es"; + let result = translation!(language, static salutation::test); + + assert!(result.unwrap() == "Hola".to_string()) +} + +#[test] +fn both_dynamic() { + let language = "es"; + let result = translation!(language, "salutation.test"); + + assert!(result.unwrap() == "Hola".to_string()) +} diff --git a/translatable_proc/src/data/translations.rs b/translatable_proc/src/data/translations.rs index b36e907..07c20f0 100644 --- a/translatable_proc/src/data/translations.rs +++ b/translatable_proc/src/data/translations.rs @@ -158,7 +158,7 @@ impl Into for NestingType { let entries = translation .into_iter() .map(|(lang, value)| { - let lang = LitStr::new(&lang.to_string(), Span::call_site()); + let lang = LitStr::new(&format!("{lang:?}").to_lowercase(), Span::call_site()); let value = LitStr::new(&value, Span::call_site()); quote! { (#lang.to_string(), #value.to_string()) } diff --git a/translatable_proc/src/macros.rs b/translatable_proc/src/macros.rs index 99c6e10..d2337bb 100644 --- a/translatable_proc/src/macros.rs +++ b/translatable_proc/src/macros.rs @@ -97,10 +97,10 @@ pub fn translation_macro(args: TranslationArgs) -> TokenStream { (Some(lang), Ok(trans)) => { match lang { Ok(lang) => { - quote! { + quote! {{ #lang #trans - } + }} }, Err(e) => { diff --git a/translatable_proc/src/translations/generation.rs b/translatable_proc/src/translations/generation.rs index 5efa892..47a93dd 100644 --- a/translatable_proc/src/translations/generation.rs +++ b/translatable_proc/src/translations/generation.rs @@ -30,7 +30,7 @@ pub fn load_lang_dynamic(lang: TokenStream) -> Result Result, path: String) -> Re .get(&language) .ok_or(TranslationError::LanguageNotAvailable(language, path))?; - quote! { stringify!(#translation) } + quote! { #translation } }, None => { @@ -77,20 +80,22 @@ pub fn load_translation_static(static_lang: Option, path: String) -> Re .iter() .map(|(key, value)| { let key = format!("{key:?}").to_lowercase(); - quote! { (stringify!(#key), stringify!(#value)) } + quote! { (#key, #value) } }); - quote! { + quote! {{ if valid_lang { vec![#(#translation_object),*] - .iter() + .into_iter() .collect::>() - .get(&language) - .ok_or(translatable::Error::LanguageNotAvailable(language, stringify!(#path).to_string())) + .get(language.as_str()) + .ok_or(translatable::Error::LanguageNotAvailable(language, #path.to_string())) + .cloned() + .map(|translation| translation.to_string()) } else { Err(translatable::Error::InvalidLanguage(language)) } - } + }} } } ) @@ -109,10 +114,12 @@ pub fn load_translation_dynamic(static_lang: Option, path: TokenStream) let translation_quote = quote! { let path: String = #path.into(); - let translation = vec![#(#nestings),*] + let nested_translations = vec![#(#nestings),*]; + let translation = nested_translations + .iter() .find_map(|nesting| nesting .get_path( - stringify!(path) + path .split(".") .collect() ) @@ -121,29 +128,31 @@ pub fn load_translation_dynamic(static_lang: Option, path: TokenStream) Ok(match static_lang { Some(language) => { - let language = format!("{language:?}"); + let language = format!("{language:?}").to_lowercase(); - quote! { + quote! {{ #translation_quote translation - .and_then(|translation| translation.get(stringify!(#language))) - .ok_or(translatable::Error::LanguageNotAvailable(stringify!(#language).to_string(), path)) - } + .and_then(|translation| translation.get(#language)) + .ok_or(translatable::Error::LanguageNotAvailable(#language.to_string(), path)) + .cloned() + }} }, None => { - quote! { + quote! {{ #translation_quote if valid_lang { translation .and_then(|translation| translation.get(&language)) .ok_or(translatable::Error::LanguageNotAvailable(language, path)) + .cloned() } else { Err(translatable::Error::InvalidLanguage(language)) } - } + }} } }) } From e3d89c04937c968b82b23d11b5088c39a5eac1b9 Mon Sep 17 00:00:00 2001 From: stifskere Date: Tue, 25 Mar 2025 19:13:26 +0100 Subject: [PATCH 018/228] fix: error messages --- translatable_proc/src/languages.rs | 2 +- translatable_proc/src/macros.rs | 8 +++++++- translatable_proc/src/translations/errors.rs | 15 ++++++++++++--- translatable_proc/src/translations/generation.rs | 5 +---- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/translatable_proc/src/languages.rs b/translatable_proc/src/languages.rs index 0a4c33c..2d80ede 100644 --- a/translatable_proc/src/languages.rs +++ b/translatable_proc/src/languages.rs @@ -380,7 +380,7 @@ pub enum Iso639a { impl Iso639a { pub fn get_similarities(lang: &str) -> Vec { Self::iter() - .map(|variant| variant.to_string()) + .map(|variant| format!("{variant:#} ({variant:?})")) .filter(|variant| variant.contains(lang)) .collect() } diff --git a/translatable_proc/src/macros.rs b/translatable_proc/src/macros.rs index d2337bb..b2ee948 100644 --- a/translatable_proc/src/macros.rs +++ b/translatable_proc/src/macros.rs @@ -83,7 +83,13 @@ pub fn translation_macro(args: TranslationArgs) -> TokenStream { let (lang_expr, static_lang) = match language { LanguageType::CompileTimeLiteral(lang) => ( None, - Some(load_lang_static(&lang).ok()).flatten(), + match load_lang_static(&lang) { + Ok(lang) => Some(lang), + Err(e) => { + let e = format!("{e:#}"); + return quote! { compile_error!(#e) } + } + }, ), LanguageType::OnScopeExpression(lang) => (Some(load_lang_dynamic(lang)), None), }; diff --git a/translatable_proc/src/translations/errors.rs b/translatable_proc/src/translations/errors.rs index f9cb380..269104e 100644 --- a/translatable_proc/src/translations/errors.rs +++ b/translatable_proc/src/translations/errors.rs @@ -31,10 +31,19 @@ pub enum TranslationError { /// Invalid language code error with suggestions #[error( - "'{0}' is not valid ISO 639-1. These are some valid languages including '{0}':\n{sorted_list}", - sorted_list = .1.join(",\n") + "'{0}' is not valid ISO 639-1. {similarities}", + similarities = { + let similarities = Iso639a::get_similarities(.0) + .join("\n"); + + if similarities.is_empty() { + "".into() + } else { + format!("These are some valid languages including '{}':\n{similarities}", .0) + } + } )] - InvalidLanguage(String, Vec), + InvalidLanguage(String), /// Invalid TOML structure in specific file #[error( diff --git a/translatable_proc/src/translations/generation.rs b/translatable_proc/src/translations/generation.rs index 47a93dd..3d8aedd 100644 --- a/translatable_proc/src/translations/generation.rs +++ b/translatable_proc/src/translations/generation.rs @@ -13,10 +13,7 @@ pub fn load_lang_static(lang: &str) -> Result { Ok( lang .parse::() - .map_err(|_| TranslationError::InvalidLanguage( - lang.to_string(), - Iso639a::get_similarities(lang) - ))? + .map_err(|_| TranslationError::InvalidLanguage(lang.to_string()))? ) } From 45544d5aae664733d7ef31932f50d22276ef78b0 Mon Sep 17 00:00:00 2001 From: stifskere Date: Tue, 25 Mar 2025 19:34:55 +0100 Subject: [PATCH 019/228] feat: add similarities limit --- translatable_proc/src/languages.rs | 38 ++++++++++++++++++-- translatable_proc/src/translations/errors.rs | 14 ++++++-- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/translatable_proc/src/languages.rs b/translatable_proc/src/languages.rs index 2d80ede..f6d575f 100644 --- a/translatable_proc/src/languages.rs +++ b/translatable_proc/src/languages.rs @@ -377,12 +377,44 @@ pub enum Iso639a { ZU, } +pub struct Similarities { + overflow_by: usize, + similarities: Vec +} + +impl Similarities { + pub fn overflow_by(&self) -> usize { + self.overflow_by + } + + pub fn similarities(&self) -> &[T] { + &self.similarities + } +} + impl Iso639a { - pub fn get_similarities(lang: &str) -> Vec { - Self::iter() + pub fn get_similarities(lang: &str, max_amount: usize) -> Similarities { + let all_similarities = Self::iter() .map(|variant| format!("{variant:#} ({variant:?})")) .filter(|variant| variant.contains(lang)) - .collect() + .collect::>(); + + let overflow_by = all_similarities.len() as i32 - max_amount as i32; + + if overflow_by > 0 { + Similarities { + similarities: all_similarities + .into_iter() + .take(max_amount) + .collect(), + overflow_by: overflow_by as usize + } + } else { + Similarities { + similarities: all_similarities, + overflow_by: 0 + } + } } } diff --git a/translatable_proc/src/translations/errors.rs b/translatable_proc/src/translations/errors.rs index 269104e..33f6e98 100644 --- a/translatable_proc/src/translations/errors.rs +++ b/translatable_proc/src/translations/errors.rs @@ -33,13 +33,21 @@ pub enum TranslationError { #[error( "'{0}' is not valid ISO 639-1. {similarities}", similarities = { - let similarities = Iso639a::get_similarities(.0) + let similarities = Iso639a::get_similarities(.0, 10); + let similarities_format = similarities + .similarities() .join("\n"); - if similarities.is_empty() { + if similarities_format.is_empty() { "".into() } else { - format!("These are some valid languages including '{}':\n{similarities}", .0) + let including_format = format!("These are some valid languages including '{}':\n{similarities_format}", .0); + + if similarities.overflow_by() > 0 { + format!("{including_format}\n... and {} more.", similarities.overflow_by()) + } else { + including_format + } } } )] From be45b348092ed14b49ef75842ce1b1361860593f Mon Sep 17 00:00:00 2001 From: Chiko Date: Tue, 25 Mar 2025 18:36:38 +0000 Subject: [PATCH 020/228] chore: clippy suggestions and code formatting --- translatable_proc/src/data/mod.rs | 1 - translatable_proc/src/data/translations.rs | 8 +- translatable_proc/src/macros.rs | 61 ++++---- translatable_proc/src/translations/errors.rs | 11 +- .../src/translations/generation.rs | 145 ++++++++---------- translatable_proc/src/translations/mod.rs | 1 - 6 files changed, 105 insertions(+), 122 deletions(-) diff --git a/translatable_proc/src/data/mod.rs b/translatable_proc/src/data/mod.rs index 660a572..d40b299 100644 --- a/translatable_proc/src/data/mod.rs +++ b/translatable_proc/src/data/mod.rs @@ -1,3 +1,2 @@ - pub mod config; pub mod translations; diff --git a/translatable_proc/src/data/translations.rs b/translatable_proc/src/data/translations.rs index 07c20f0..5b07d49 100644 --- a/translatable_proc/src/data/translations.rs +++ b/translatable_proc/src/data/translations.rs @@ -137,10 +137,10 @@ impl NestingType { } } -impl Into for NestingType { - fn into(self) -> TokenStream { - match self { - Self::Object(nesting) => { +impl From for TokenStream { + fn from(val: NestingType) -> Self { + match val { + NestingType::Object(nesting) => { let entries = nesting .into_iter() .map(|(key, value)| -> TokenStream { diff --git a/translatable_proc/src/macros.rs b/translatable_proc/src/macros.rs index b2ee948..b5d19f1 100644 --- a/translatable_proc/src/macros.rs +++ b/translatable_proc/src/macros.rs @@ -1,9 +1,11 @@ +use crate::translations::generation::{ + load_lang_dynamic, load_lang_static, load_translation_dynamic, load_translation_static, +}; use proc_macro2::TokenStream; use quote::quote; use syn::parse::{Parse, ParseStream}; use syn::token::Static; use syn::{Expr, ExprLit, ExprPath, Lit, Result as SynResult, Token}; -use crate::translations::generation::{load_lang_dynamic, load_lang_static, load_translation_dynamic, load_translation_static}; /// Internal representation of macro arguments before processing /// @@ -12,7 +14,7 @@ use crate::translations::generation::{load_lang_dynamic, load_lang_static, load_ pub struct RawMacroArgs { /// Language specification (literal or expression) language: Expr, - /// Argument seprator. + /// Argument separator. _comma: Token![,], /// Static marker for path analysis static_marker: Option, @@ -46,12 +48,12 @@ impl Parse for RawMacroArgs { } } -impl Into for RawMacroArgs { - fn into(self) -> TranslationArgs { - let is_path_static = self.static_marker.is_some(); +impl From for TranslationArgs { + fn from(val: RawMacroArgs) -> Self { + let is_path_static = val.static_marker.is_some(); TranslationArgs { - language: match self.language { + language: match val.language { Expr::Lit(ExprLit { lit: Lit::Str(lit_str), .. @@ -59,17 +61,15 @@ impl Into for RawMacroArgs { other => LanguageType::OnScopeExpression(quote!(#other).into()), }, - path: match self.path { - Expr::Path(ExprPath { path, .. }) if is_path_static => { - PathType::CompileTimePath( - path.segments - .iter() - .map(|s| s.ident.to_string()) - .collect::>() - .join(".") - .to_string(), - ) - } + path: match val.path { + Expr::Path(ExprPath { path, .. }) if is_path_static => PathType::CompileTimePath( + path.segments + .iter() + .map(|s| s.ident.to_string()) + .collect::>() + .join(".") + .to_string(), + ), path => PathType::OnScopeExpression(quote!(#path).into()), }, @@ -87,7 +87,7 @@ pub fn translation_macro(args: TranslationArgs) -> TokenStream { Ok(lang) => Some(lang), Err(e) => { let e = format!("{e:#}"); - return quote! { compile_error!(#e) } + return quote! { compile_error!(#e) }; } }, ), @@ -100,26 +100,23 @@ pub fn translation_macro(args: TranslationArgs) -> TokenStream { }; match (lang_expr, translation_expr) { - (Some(lang), Ok(trans)) => { - match lang { - Ok(lang) => { - quote! {{ - #lang - #trans - }} - }, + (Some(lang), Ok(trans)) => match lang { + Ok(lang) => { + quote! {{ + #lang + #trans + }} + } - Err(e) => { - let e = format!("{e:#}"); - quote! { compile_error!{#e} } - } + Err(e) => { + let e = format!("{e:#}"); + quote! { compile_error!{#e} } } }, (None, Ok(trans)) => trans, (_, Err(e)) => { let e = format!("{e:#}"); quote! { compile_error!(#e) } - }, + } } - .into() } diff --git a/translatable_proc/src/translations/errors.rs b/translatable_proc/src/translations/errors.rs index 33f6e98..dab6186 100644 --- a/translatable_proc/src/translations/errors.rs +++ b/translatable_proc/src/translations/errors.rs @@ -1,4 +1,7 @@ -use crate::{data::{config::ConfigError, translations::TransformError}, languages::Iso639a}; +use crate::{ + data::{config::ConfigError, translations::TransformError}, + languages::Iso639a, +}; use std::io::Error as IoError; use syn::Error as SynError; use thiserror::Error; @@ -54,9 +57,7 @@ pub enum TranslationError { InvalidLanguage(String), /// Invalid TOML structure in specific file - #[error( - "Invalid TOML structure in file {1}: {0}" - )] + #[error("Invalid TOML structure in file {1}: {0}")] InvalidTomlFormat(TransformError, String), #[error("The path '{0}' is not found in any of the translation files.")] @@ -66,5 +67,5 @@ pub enum TranslationError { LanguageNotAvailable(Iso639a, String), #[error("Error parsing macro.")] - MacroError(#[from] SynError) + MacroError(#[from] SynError), } diff --git a/translatable_proc/src/translations/generation.rs b/translatable_proc/src/translations/generation.rs index 3d8aedd..90837dd 100644 --- a/translatable_proc/src/translations/generation.rs +++ b/translatable_proc/src/translations/generation.rs @@ -1,111 +1,99 @@ +use super::errors::TranslationError; +use crate::data::translations::load_translations; use crate::languages::Iso639a; use proc_macro2::TokenStream; use quote::quote; use strum::IntoEnumIterator; -use syn::{parse2, Expr}; -use crate::data::translations::load_translations; -use super::errors::TranslationError; +use syn::{Expr, parse2}; /// This function parses a statically obtained language /// as an Iso639a enum instance, along this, the validation /// is also done at parse time. pub fn load_lang_static(lang: &str) -> Result { - Ok( - lang - .parse::() - .map_err(|_| TranslationError::InvalidLanguage(lang.to_string()))? - ) + lang.parse::() + .map_err(|_| TranslationError::InvalidLanguage(lang.to_string())) } /// This function generates a language variable, the only -/// requisite is that the expression evalutes to something +/// requisite is that the expression evaluates to something /// that implements Into. pub fn load_lang_dynamic(lang: TokenStream) -> Result { let lang: Expr = parse2(lang)?; - let available_langs = Iso639a::iter() - .map(|language| { - let language = format!("{language:?}"); + let available_langs = Iso639a::iter().map(|language| { + let language = format!("{language:?}"); - quote! { #language, } - }); + quote! { #language, } + }); // The `String` explicit type serves as // expression type checking, we accept `impl Into` // for any expression that's not static. - Ok( - quote! { - #[doc(hidden)] - let language: String = (#lang).into(); - #[doc(hidden)] - let language = language.to_lowercase(); - - #[doc(hidden)] - let valid_lang = vec![#(#available_langs)*] - .iter() - .any(|lang| lang.eq_ignore_ascii_case(&language)); - } - ) + Ok(quote! { + #[doc(hidden)] + let language: String = (#lang).into(); + #[doc(hidden)] + let language = language.to_lowercase(); + + #[doc(hidden)] + let valid_lang = vec![#(#available_langs)*] + .iter() + .any(|lang| lang.eq_ignore_ascii_case(&language)); + }) } - -pub fn load_translation_static(static_lang: Option, path: String) -> Result { +pub fn load_translation_static( + static_lang: Option, + path: String, +) -> Result { let translation_object = load_translations()? .iter() - .find_map(|association| association - .translation_table() - .get_path( - path - .split(".") - .collect() - ) - ) + .find_map(|association| { + association + .translation_table() + .get_path(path.split(".").collect()) + }) .ok_or(TranslationError::PathNotFound(path.to_string()))?; - Ok( - match static_lang { - Some(language) => { - let translation = translation_object - .get(&language) - .ok_or(TranslationError::LanguageNotAvailable(language, path))?; - - quote! { #translation } - }, - - None => { - let translation_object = translation_object - .iter() - .map(|(key, value)| { - let key = format!("{key:?}").to_lowercase(); - quote! { (#key, #value) } - }); - - quote! {{ - if valid_lang { - vec![#(#translation_object),*] - .into_iter() - .collect::>() - .get(language.as_str()) - .ok_or(translatable::Error::LanguageNotAvailable(language, #path.to_string())) - .cloned() - .map(|translation| translation.to_string()) - } else { - Err(translatable::Error::InvalidLanguage(language)) - } - }} - } + Ok(match static_lang { + Some(language) => { + let translation = translation_object + .get(&language) + .ok_or(TranslationError::LanguageNotAvailable(language, path))?; + + quote! { #translation } + } + + None => { + let translation_object = translation_object.iter().map(|(key, value)| { + let key = format!("{key:?}").to_lowercase(); + quote! { (#key, #value) } + }); + + quote! {{ + if valid_lang { + vec![#(#translation_object),*] + .into_iter() + .collect::>() + .get(language.as_str()) + .ok_or(translatable::Error::LanguageNotAvailable(language, #path.to_string())) + .cloned() + .map(|translation| translation.to_string()) + } else { + Err(translatable::Error::InvalidLanguage(language)) + } + }} } - ) + }) } -pub fn load_translation_dynamic(static_lang: Option, path: TokenStream) -> Result { +pub fn load_translation_dynamic( + static_lang: Option, + path: TokenStream, +) -> Result { let nestings = load_translations()? - .into_iter() - .map(|association| association - .translation_table() - .clone() - .into() - ) + .iter() + .map(|association| association.translation_table().clone().into()) .collect::>(); let translation_quote = quote! { @@ -135,7 +123,7 @@ pub fn load_translation_dynamic(static_lang: Option, path: TokenStream) .ok_or(translatable::Error::LanguageNotAvailable(#language.to_string(), path)) .cloned() }} - }, + } None => { quote! {{ @@ -153,4 +141,3 @@ pub fn load_translation_dynamic(static_lang: Option, path: TokenStream) } }) } - diff --git a/translatable_proc/src/translations/mod.rs b/translatable_proc/src/translations/mod.rs index 037e38a..d80bff5 100644 --- a/translatable_proc/src/translations/mod.rs +++ b/translatable_proc/src/translations/mod.rs @@ -1,3 +1,2 @@ - pub mod errors; pub mod generation; From 071568dd1198e67123a21be0d92aca8848d9ae45 Mon Sep 17 00:00:00 2001 From: Chiko Date: Tue, 25 Mar 2025 18:49:52 +0000 Subject: [PATCH 021/228] chore: remove unnecessary `.into()` --- translatable_proc/src/macros.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/translatable_proc/src/macros.rs b/translatable_proc/src/macros.rs index b5d19f1..ea15439 100644 --- a/translatable_proc/src/macros.rs +++ b/translatable_proc/src/macros.rs @@ -58,7 +58,7 @@ impl From for TranslationArgs { lit: Lit::Str(lit_str), .. }) => LanguageType::CompileTimeLiteral(lit_str.value()), - other => LanguageType::OnScopeExpression(quote!(#other).into()), + other => LanguageType::OnScopeExpression(quote!(#other)), }, path: match val.path { @@ -71,7 +71,7 @@ impl From for TranslationArgs { .to_string(), ), - path => PathType::OnScopeExpression(quote!(#path).into()), + path => PathType::OnScopeExpression(quote!(#path)), }, } } From 33965a067aaeeb40daa4bc44422b6c4d5802e412 Mon Sep 17 00:00:00 2001 From: stifskere Date: Wed, 26 Mar 2025 13:43:33 +0100 Subject: [PATCH 022/228] fix: hide all the variables that shouldn't be used by the user --- translatable/src/lib.rs | 3 +++ translatable_proc/src/translations/errors.rs | 9 ++++----- translatable_proc/src/translations/generation.rs | 3 +++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/translatable/src/lib.rs b/translatable/src/lib.rs index 5133ab5..2948e17 100644 --- a/translatable/src/lib.rs +++ b/translatable/src/lib.rs @@ -29,14 +29,17 @@ pub enum Error { /// This module is for internal usage, it's members /// are not documented, and there is no support on /// using it. +#[doc(hidden)] pub mod internal { use std::collections::HashMap; + #[doc(hidden)] pub enum NestingType { Object(HashMap), Translation(HashMap) } + #[doc(hidden)] impl NestingType { pub fn get_path(&self, path: Vec<&str>) -> Option<&HashMap> { match self { diff --git a/translatable_proc/src/translations/errors.rs b/translatable_proc/src/translations/errors.rs index dab6186..b64bde0 100644 --- a/translatable_proc/src/translations/errors.rs +++ b/translatable_proc/src/translations/errors.rs @@ -1,7 +1,6 @@ -use crate::{ - data::{config::ConfigError, translations::TransformError}, - languages::Iso639a, -}; +use crate::languages::Iso639a; +use crate::data::translations::TransformError; +use crate::data::config::ConfigError; use std::io::Error as IoError; use syn::Error as SynError; use thiserror::Error; @@ -60,7 +59,7 @@ pub enum TranslationError { #[error("Invalid TOML structure in file {1}: {0}")] InvalidTomlFormat(TransformError, String), - #[error("The path '{0}' is not found in any of the translation files.")] + #[error("The path '{0}' is not found in any of the translation files as a translation object.")] PathNotFound(String), #[error("The language '{0:?}' ({0:#}) is not available for the '{1}' translation.")] diff --git a/translatable_proc/src/translations/generation.rs b/translatable_proc/src/translations/generation.rs index 90837dd..a771a9a 100644 --- a/translatable_proc/src/translations/generation.rs +++ b/translatable_proc/src/translations/generation.rs @@ -97,9 +97,12 @@ pub fn load_translation_dynamic( .collect::>(); let translation_quote = quote! { + #[doc(hidden)] let path: String = #path.into(); + #[doc(hidden)] let nested_translations = vec![#(#nestings),*]; + #[doc(hidden)] let translation = nested_translations .iter() .find_map(|nesting| nesting From d574d5b6e386c385bf28bb9abb6c375415fb0979 Mon Sep 17 00:00:00 2001 From: stifskere Date: Wed, 26 Mar 2025 13:47:10 +0100 Subject: [PATCH 023/228] feat: add cause method to error for convenience --- translatable/src/lib.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/translatable/src/lib.rs b/translatable/src/lib.rs index 2948e17..12ac6c8 100644 --- a/translatable/src/lib.rs +++ b/translatable/src/lib.rs @@ -26,6 +26,16 @@ pub enum Error { LanguageNotAvailable(String, String) } +impl Error { + /// This method is a convenience implementation + /// to obtain the `Display` from each error. + #[inline] + #[cold] + pub fn cause(&self) -> String { + format!("{self:#}") + } +} + /// This module is for internal usage, it's members /// are not documented, and there is no support on /// using it. From c3206bb917740c1f7474f084df39e74cf632b9ca Mon Sep 17 00:00:00 2001 From: stifskere Date: Wed, 26 Mar 2025 15:32:00 +0100 Subject: [PATCH 024/228] chore: remove docs from docs hidden --- translatable/src/lib.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/translatable/src/lib.rs b/translatable/src/lib.rs index 12ac6c8..d4a951b 100644 --- a/translatable/src/lib.rs +++ b/translatable/src/lib.rs @@ -36,9 +36,6 @@ impl Error { } } -/// This module is for internal usage, it's members -/// are not documented, and there is no support on -/// using it. #[doc(hidden)] pub mod internal { use std::collections::HashMap; From 09206d783afd5dd31535e57105c6dd01b5c2e528 Mon Sep 17 00:00:00 2001 From: stifskere Date: Wed, 26 Mar 2025 16:35:00 +0100 Subject: [PATCH 025/228] fix: use overlap --- translatable_proc/src/data/translations.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/translatable_proc/src/data/translations.rs b/translatable_proc/src/data/translations.rs index 5b07d49..ffebd89 100644 --- a/translatable_proc/src/data/translations.rs +++ b/translatable_proc/src/data/translations.rs @@ -1,4 +1,4 @@ -use super::config::{SeekMode, load_config}; +use super::config::{load_config, SeekMode, TranslationOverlap}; use crate::languages::Iso639a; use std::collections::HashMap; use std::fs::{read_dir, read_to_string}; @@ -97,7 +97,7 @@ pub fn load_translations() -> Result<&'static Vec, Transl translation_paths.reverse(); } - let translations = translation_paths + let mut translations = translation_paths .iter() .map(|path| { let table = read_to_string(path)? @@ -112,6 +112,10 @@ pub fn load_translations() -> Result<&'static Vec, Transl }) .collect::, TranslationError>>()?; + if let TranslationOverlap::Overwrite = config.overlap() { + translations.reverse(); + } + Ok(TRANSLATIONS.get_or_init(|| translations)) } From d3b03bc71c578b5b0a76dfef3fcd3a8dfdfb003e Mon Sep 17 00:00:00 2001 From: stifskere Date: Wed, 26 Mar 2025 17:33:44 +0100 Subject: [PATCH 026/228] docs: improved readme with real data --- README.md | 223 ++++++++++++++++++++---------------------------------- 1 file changed, 82 insertions(+), 141 deletions(-) diff --git a/README.md b/README.md index fcefe6c..73856da 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,17 @@ -# Translatable +# Translatable πŸŒπŸ—£οΈπŸ’¬πŸŒ [![Crates.io](https://img.shields.io/crates/v/translatable)](https://crates.io/crates/translatable) [![Docs.rs](https://docs.rs/translatable/badge.svg)](https://docs.rs/translatable) A robust internationalization solution for Rust featuring compile-time validation, ISO 639-1 compliance, and TOML-based translation management. -## Table of Contents +## Table of Contents πŸ“– -- [Features](#features-πŸš€) -- [Installation](#installation-πŸ“¦) -- [Usage](#usage-πŸ› οΈ) -- [Configuration](#configuration-βš™οΈ) -- [Error Handling](#error-handling-🚨) -- [Example Structure](#example-structure-πŸ“‚) -- [Integration Guide](#integration-guide-πŸ”—) +- [Features](#features-) +- [Installation](#installation-) +- [Usage](#usage-) +- [Configuration](#configuration-) +- [Example implementation](#example-implementation-) ## Features πŸš€ @@ -35,76 +33,90 @@ cargo add translatable ## Usage πŸ› οΈ -### Macro Behavior Matrix +### Configuration -| Parameters | Compile-Time Checks | Return Type | -| --------------------------------- | ---------------------------------------------------------------- | ---------------------------------------- | -| `static path` + `static language` | - Path existence
- Language validity
- Template validation | `&'static str` | -| `static path` + dynamic language | - Path existence
- Template structure | `Result<&'static str, TranslationError>` | -| dynamic path + `static language` | - Language validity | `Result<&'static str, TranslationError>` | -| dynamic path + dynamic language | None (runtime checks only) | `Result<&'static str, TranslationError>` | +There are things you can configure on how translations are loaded from the folder, for this +you should make a `translatable.toml` in the root of the project, and abide by the following +configuration values. -### Key Implications +| Key | Value type | Description | +|-----------|------------------------------------|--------------------------------------------------------------------------------------------------------------------------------| +| `path` | String | Where the translation files will be stored, non translation files in that folder will cause errors. | +| `seek\_mode` | "alphabetical" \| "unalphabetical" | The found translations are ordered by file name, based on this field. | +| `overlap` | "overwrite" \| "ignore" | Orderly if a translation is found "overwrite" will keep searching for translations and "ignore" will preserve the current one. | -- **Static Path** - βœ… Verifies translation path exists in TOML files - ❌ Requires path literal (e.g., `static common::greeting`) +`seek_mode` and `overlap` only reverse the translations as convenient, this way the process +doesn't get repeated every time a translation is loaded. -- **Static Language** - βœ… Validates ISO 639-1 compliance - ❌ Requires language literal (e.g., `"en"`) +### Translation file format -- **Mixed Modes** +All the translation files are going to be loaded from the path specified in the configuration, +all the files inside the path must be TOML files and sub folders, a `walk_dir` algorithm is used +to load all the translations inside that folder. - ```rust - // Compile-time path + runtime language - translation!(user_lang, static user::profile::title) +The translation files have three rules +- Objects (including top level) can only contain objects and strings +- If an object contains another object, it can only contain other objects (known as nested object) +- If an object contains a string, it can only contain other strings (known as translation object) - // Compile-time language + runtime path - translation!("fr", dynamic_path) - ``` +### Loading translations -- **Full Dynamic** +The load configuration such as `seek_mode` and `overlap` is not relevant here, as previously +specified, these configuration values only get applied once by reversing the translations conveniently. - ```rust - // Runtime checks only - translation!(lang_var, path_var) // Returns Result - ``` +To load translations you make use of the `translatable::translation` macro, that macro requires two +parameters to be passed. -- **Full Static** +The first parameter consists of the language which can be passed dynamically as a variable or an expression +that resolves to an `impl Into`, or statically as a `&'static str` literal. Not mattering the way +it's passed, the translation must comply with the `ISO 639-1` standard. - ```rust - // Compile-time checks only - translation!("en", static common::greeting) // Returns &'static str - ``` +The second parameter consists of the path, which can be passed dynamically as a variable or an expression +that resolves to an `impl Into` with the format `path.to.translation`, or statically with the following +syntax `static path::to::translation`. -Optimization Guide +Depending on whether the parameters are static or dynamic the macro will act different, differing whether +the checks are compile-time or run-time, the following table is a macro behavior matrix. -```rust -// Maximum safety - fails compile if any issues -let text = translation!("es", static home::welcome_message); +| Parameters | Compile-Time checks | Return type | +|----------------------------------------------------|--------------------------------------------------------|-----------------------------------------| +| `static language` + `static path` (most optimized) | Path existence, Language validity, \*Template validation | &'static str (stack) | +| `dynamic language` + `dynamic path` | None | Result (heap) | +| `static language` + `dynamic path` | Language validity | Result (heap) | +| `dynamic language` + `static path` (commonly used) | Path existence, \*Template validation | Result (heap) | -// Balanced approach - compile-time path validation -let result = translation!(user_lang, static user::profile::title); +- For the error handling, if you want to integrate this with `thiserror` you can use a `#[from] translatable::TranslationError`, +as a nested error, all the errors implement display, for optimization purposes there are not the same amount of errors with +dynamic parameters than there are with static parameters. -// Flexible runtime - handles dynamic inputs -let result = translation!(lang_var, path_var)?; -``` +- The runtime errors implement a `cause()` method that returns a heap allocated `String` with the error reason, essentially +the error display. + +- Template validation in the static parameter handling means variable existence, since templates are generated as a `format!` +call which processes expressions found in scope. It's always recommended to use full paths in translation templates +to avoid needing to make variables in scope, unless the calls are contextual, in that case there is nothing that can +be done to avoid making variables. + +## Example implementation πŸ“‚ + +The following examples are an example application structure for a possible +real project. -## Example Structure πŸ“‚ +### Example application tree -```txt +```plain project-root/ β”œβ”€β”€ Cargo.toml β”œβ”€β”€ translatable.toml -└── translations/ - β”œβ”€β”€ app.toml - β”œβ”€β”€ errors.toml - └── user/ - β”œβ”€β”€ profile.toml +β”œβ”€β”€ translations/ +β”‚ └── app.toml +└── src/ + └── main.rs ``` -### Example Translation File (translations/app.toml) +### Example translation file (translations/app.toml) + +Notice how `common.greeting` has a template named `name`. ```toml [home] @@ -120,95 +132,24 @@ greeting = { } ``` -### Translation File Organization - -The `translations/` folder can be structured flexibly. You can organize translations based on features, modules, or locales. -Here are some best practices: - -- Keep related translations in subdirectories (`user/profile.toml`, `errors.toml`) -- Use consistent naming conventions (`common.toml`, `app.toml`) -- Keep files small and manageable to avoid conflicts - -## Configuration βš™οΈ - -Create `translatable.toml` in your project root: - -```toml -path = "./translations" -seek_mode = "alphabetical" -overlap = "overwrite" -``` - -| Option | Default | Description | -| --------- | -------------- | ------------------------------------------- | -| path | ./translations | Translation directory location | -| seek_mode | alphabetical | Order in which translation files are loaded | -| overlap | overwrite | Defines conflict resolution strategy | - -### Configuration Options Explained - -- **`seek_mode`**: Controls the order of file processing (e.g., `alphabetical`, `manual`). -- **`overlap`**: Determines priority when duplicate keys exist (`overwrite` replaces existing keys, `first` keeps the first occurrence). - -## Error Handling 🚨 +### Example application usage -### Invalid Language Code - -```sh -Error: 'e' is not valid ISO 639-1. These are some valid languages including 'e': - ae (Avestan), - eu (Basque), - be (Belarusian), - ce (Chechen), - en (English), - ... (12 more) - --> tests/static.rs:5:5 - | - 5 | translation!("e", static salutation::test); - | - = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) -``` - -### Structural Validation - -```sh -Error: Invalid TOML structure in file ./translations/test.toml: Translation files must contain either nested tables or language translations, but not both at the same level. -``` - -### Template Validation - -```sh -Error: Toml parse error 'invalid inline table - expected `}`' in ./translations/test.toml:49:50 - --> tests/static.rs:5:5 - | - 5 | translation!("es", static salutation::test); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) -``` - -## Integration Guide πŸ”— - -If you're using `translatable` in a web application, here’s how to integrate it: - -### Actix-Web Example +Notice how that template is in scope, whole expressions can be used +in the templates such as `path::to::function()`, or other constants. ```rust -use actix_web::{get, web, App, HttpServer, Responder}; +extern crate translatable; use translatable::translation; -#[get("/")] -async fn home() -> impl Responder { - let text = translation!("en", static home::welcome_message); - text.to_string() -} +fn main() { + let dynamic_lang = "es"; + let dynamic_path = "common.greeting" + let name = "john"; -#[actix_web::main] -async fn main() -> std::io::Result<()> { - HttpServer::new(|| App::new().service(home)) - .bind("127.0.0.1:8080")? - .run() - .await + assert!(translation!("es", static common::greeting) == "Β‘Hola john!"); + assert!(translation!("es", dynamic_path).unwrap() == "Β‘Hola john!".into()); + assert!(translation!(dynamic_lang, static common::greeting).unwrap() == "Β‘Hola john!".into()); + assert!(translation!(dynamic_lang, dynamic_path).unwrap() == "Β‘Hola john!".into()); } ``` + From a6d0f1e642148d6152dd7c328665626c6a6036b3 Mon Sep 17 00:00:00 2001 From: stifskere Date: Wed, 26 Mar 2025 17:34:59 +0100 Subject: [PATCH 027/228] docs: remove configuration as it is sub category of usage --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 73856da..0216fc7 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,6 @@ A robust internationalization solution for Rust featuring compile-time validatio - [Features](#features-) - [Installation](#installation-) - [Usage](#usage-) -- [Configuration](#configuration-) - [Example implementation](#example-implementation-) ## Features πŸš€ From b623fbc91239f07402690638667e81fb69056abc Mon Sep 17 00:00:00 2001 From: stifskere Date: Wed, 26 Mar 2025 17:37:07 +0100 Subject: [PATCH 028/228] docs: fix readme escapes --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0216fc7..0260932 100644 --- a/README.md +++ b/README.md @@ -40,9 +40,9 @@ configuration values. | Key | Value type | Description | |-----------|------------------------------------|--------------------------------------------------------------------------------------------------------------------------------| -| `path` | String | Where the translation files will be stored, non translation files in that folder will cause errors. | -| `seek\_mode` | "alphabetical" \| "unalphabetical" | The found translations are ordered by file name, based on this field. | -| `overlap` | "overwrite" \| "ignore" | Orderly if a translation is found "overwrite" will keep searching for translations and "ignore" will preserve the current one. | +| `path` | `String` | Where the translation files will be stored, non translation files in that folder will cause errors. | +| `seek_mode` | `"alphabetical"` \| `"unalphabetical"` | The found translations are ordered by file name, based on this field. | +| `overlap` | `"overwrite"` \| `"ignore"` | Orderly if a translation is found "overwrite" will keep searching for translations and "ignore" will preserve the current one. | `seek_mode` and `overlap` only reverse the translations as convenient, this way the process doesn't get repeated every time a translation is loaded. From ca8daa16f629153fcb4e23f50e4e8a67840cee1a Mon Sep 17 00:00:00 2001 From: stifskere Date: Wed, 26 Mar 2025 17:39:26 +0100 Subject: [PATCH 029/228] docs: fix indent for app tree and add miniscript for config values --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0260932..fc6a492 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ configuration values. |-----------|------------------------------------|--------------------------------------------------------------------------------------------------------------------------------| | `path` | `String` | Where the translation files will be stored, non translation files in that folder will cause errors. | | `seek_mode` | `"alphabetical"` \| `"unalphabetical"` | The found translations are ordered by file name, based on this field. | -| `overlap` | `"overwrite"` \| `"ignore"` | Orderly if a translation is found "overwrite" will keep searching for translations and "ignore" will preserve the current one. | +| `overlap` | `"overwrite"` \| `"ignore"` | Orderly if a translation is found `"overwrite"` will keep searching for translations and `"ignore"` will preserve the current one. | `seek_mode` and `overlap` only reverse the translations as convenient, this way the process doesn't get repeated every time a translation is loaded. @@ -110,7 +110,7 @@ project-root/ β”œβ”€β”€ translations/ β”‚ └── app.toml └── src/ - └── main.rs + └── main.rs ``` ### Example translation file (translations/app.toml) From 494a9aaed1af79548413b662aaa39dcd2798243d Mon Sep 17 00:00:00 2001 From: stifskere Date: Wed, 26 Mar 2025 17:46:23 +0100 Subject: [PATCH 030/228] docs: clarify return types on behavior matrix --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index fc6a492..844c225 100644 --- a/README.md +++ b/README.md @@ -77,12 +77,12 @@ syntax `static path::to::translation`. Depending on whether the parameters are static or dynamic the macro will act different, differing whether the checks are compile-time or run-time, the following table is a macro behavior matrix. -| Parameters | Compile-Time checks | Return type | -|----------------------------------------------------|--------------------------------------------------------|-----------------------------------------| -| `static language` + `static path` (most optimized) | Path existence, Language validity, \*Template validation | &'static str (stack) | -| `dynamic language` + `dynamic path` | None | Result (heap) | -| `static language` + `dynamic path` | Language validity | Result (heap) | -| `dynamic language` + `static path` (commonly used) | Path existence, \*Template validation | Result (heap) | +| Parameters | Compile-Time checks | Return type | +|----------------------------------------------------|----------------------------------------------------------|-----------------------------------------------------------------------------------| +| `static language` + `static path` (most optimized) | Path existence, Language validity, \*Template validation | `&'static str` (stack) if there are no templates or `String` (heap) if there are. | +| `dynamic language` + `dynamic path` | None | `Result` (heap) | +| `static language` + `dynamic path` | Language validity | `Result` (heap) | +| `dynamic language` + `static path` (commonly used) | Path existence, \*Template validation | `Result` (heap) | - For the error handling, if you want to integrate this with `thiserror` you can use a `#[from] translatable::TranslationError`, as a nested error, all the errors implement display, for optimization purposes there are not the same amount of errors with From d24260bbf2a05bd629dc0d0c5f9586e7e15851bc Mon Sep 17 00:00:00 2001 From: stifskere Date: Wed, 26 Mar 2025 18:01:10 +0100 Subject: [PATCH 031/228] feat: add PathNotFound in runtime errors --- translatable/src/lib.rs | 5 +++- .../src/translations/generation.rs | 24 ++++++++++++------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/translatable/src/lib.rs b/translatable/src/lib.rs index d4a951b..1998721 100644 --- a/translatable/src/lib.rs +++ b/translatable/src/lib.rs @@ -23,7 +23,10 @@ pub enum Error { InvalidLanguage(String), #[error("The langauge '{0}' is not available for the path '{1}'")] - LanguageNotAvailable(String, String) + LanguageNotAvailable(String, String), + + #[error("The path '{0}' was not found in any of the translations files.")] + PathNotFound(String) } impl Error { diff --git a/translatable_proc/src/translations/generation.rs b/translatable_proc/src/translations/generation.rs index a771a9a..edc62d1 100644 --- a/translatable_proc/src/translations/generation.rs +++ b/translatable_proc/src/translations/generation.rs @@ -121,10 +121,14 @@ pub fn load_translation_dynamic( quote! {{ #translation_quote - translation - .and_then(|translation| translation.get(#language)) - .ok_or(translatable::Error::LanguageNotAvailable(#language.to_string(), path)) - .cloned() + if let Some(translation) = translation { + translation + .get(#language) + .ok_or(translatable::Error::LanguageNotAvailable(#language.to_string(), path)) + .cloned() + } else { + Err(translatable::Error::PathNotFound(path)) + } }} } @@ -133,10 +137,14 @@ pub fn load_translation_dynamic( #translation_quote if valid_lang { - translation - .and_then(|translation| translation.get(&language)) - .ok_or(translatable::Error::LanguageNotAvailable(language, path)) - .cloned() + if let Some(translation) = translation { + translation + .get(&language) + .ok_or(translatable::Error::LanguageNotAvailable(language, path)) + .cloned() + } else { + Err(translatable::Error::PathNotFound(path)) + } } else { Err(translatable::Error::InvalidLanguage(language)) } From 3bcc1ec47e6ffb5dd4f379437c648a6e249a5dc2 Mon Sep 17 00:00:00 2001 From: stifskere Date: Wed, 26 Mar 2025 18:11:20 +0100 Subject: [PATCH 032/228] fix: fix translation format and sync with tests (tests won't pass until templating implemented) --- README.md | 16 ++++++---------- translatable/tests/test.rs | 20 ++++++++++++-------- translations/test.toml | 9 +++++++-- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 844c225..f966d49 100644 --- a/README.md +++ b/README.md @@ -118,17 +118,13 @@ project-root/ Notice how `common.greeting` has a template named `name`. ```toml -[home] -welcome_message = { - en = "Welcome to our app!", - es = "Β‘Bienvenido a nuestra aplicaciΓ³n!" -} +[welcome_message] +en = "Welcome to our app!" +es = "Β‘Bienvenido a nuestra aplicaciΓ³n!" -[common] -greeting = { - en = "Hello {name}!", - es = "Β‘Hola {name}!" -} +[common.greeting] +en = "Hello {name}!" +es = "Β‘Hola {name}!" ``` ### Example application usage diff --git a/translatable/tests/test.rs b/translatable/tests/test.rs index 2585cff..8a3783f 100644 --- a/translatable/tests/test.rs +++ b/translatable/tests/test.rs @@ -2,30 +2,34 @@ use translatable::translation; #[test] fn both_static() { - let result = translation!("es", static salutation::test); + let name = "john"; + let result = translation!("es", static common::greeting); - assert!(result == "Hola") + assert!(result == "Β‘Hola john!") } #[test] fn language_static_path_dynamic() { - let result = translation!("es", "salutation.test"); + let name = "john"; + let result = translation!("es", "common.greeting"); - assert!(result.unwrap() == "Hola".to_string()) + assert!(result.unwrap() == "Β‘Hola john!".to_string()) } #[test] fn language_dynamic_path_static() { + let name = "john"; let language = "es"; - let result = translation!(language, static salutation::test); + let result = translation!(language, static common::greeting); - assert!(result.unwrap() == "Hola".to_string()) + assert!(result.unwrap() == "Β‘Hola john!".to_string()) } #[test] fn both_dynamic() { + let name = "john"; let language = "es"; - let result = translation!(language, "salutation.test"); + let result = translation!(language, "common.greeting"); - assert!(result.unwrap() == "Hola".to_string()) + assert!(result.unwrap() == "Β‘Hola john!".to_string()) } diff --git a/translations/test.toml b/translations/test.toml index e5613df..6aecb64 100644 --- a/translations/test.toml +++ b/translations/test.toml @@ -1,3 +1,8 @@ -[salutation] -test = { es = "Hola", en = "Hello" } +[welcome_message] +en = "Welcome to our app!" +es = "Β‘Bienvenido a nuestra aplicaciΓ³n!" + +[common.greeting] +en = "Hello {name}!" +es = "Β‘Hola {name}!" From df71e1022fe23fd495d3371a7ab9e86d43a07ceb Mon Sep 17 00:00:00 2001 From: installer Date: Thu, 27 Mar 2025 03:38:34 +0000 Subject: [PATCH 033/228] docs: code documentation and rustfmt file --- flake.lock | 128 ++++++++++++--- flake.nix | 112 +++++-------- rustfmt.toml | 36 +++++ translatable/src/lib.rs | 58 ++++--- translatable_proc/src/data/config.rs | 16 +- translatable_proc/src/data/translations.rs | 149 +++++++++--------- translatable_proc/src/languages.rs | 15 +- translatable_proc/src/lib.rs | 3 +- translatable_proc/src/macros.rs | 114 ++++++++------ translatable_proc/src/translations/errors.rs | 11 +- .../src/translations/generation.rs | 77 +++++---- 11 files changed, 430 insertions(+), 289 deletions(-) create mode 100644 rustfmt.toml diff --git a/flake.lock b/flake.lock index a9e7b0a..603a706 100644 --- a/flake.lock +++ b/flake.lock @@ -1,42 +1,126 @@ { "nodes": { - "nixpkgs": { + "crane": { "locked": { - "lastModified": 1740695751, - "narHash": "sha256-D+R+kFxy1KsheiIzkkx/6L63wEHBYX21OIwlFV8JvDs=", - "rev": "6313551cd05425cd5b3e63fe47dbc324eabb15e4", - "revCount": 760502, - "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.760502%2Brev-6313551cd05425cd5b3e63fe47dbc324eabb15e4/01954f5d-9aa0-742c-829e-dfa0c472c2db/source.tar.gz" + "lastModified": 1742394900, + "narHash": "sha256-vVOAp9ahvnU+fQoKd4SEXB2JG2wbENkpqcwlkIXgUC0=", + "owner": "ipetkov", + "repo": "crane", + "rev": "70947c1908108c0c551ddfd73d4f750ff2ea67cd", + "type": "github" }, "original": { - "type": "tarball", - "url": "https://flakehub.com/f/NixOS/nixpkgs/0.1.%2A.tar.gz" + "owner": "ipetkov", + "repo": "crane", + "type": "github" } }, - "root": { + "fenix": { "inputs": { "nixpkgs": "nixpkgs", - "rust-overlay": "rust-overlay" + "rust-analyzer-src": "rust-analyzer-src" + }, + "locked": { + "lastModified": 1742452566, + "narHash": "sha256-sVuLDQ2UIWfXUBbctzrZrXM2X05YjX08K7XHMztt36E=", + "owner": "nix-community", + "repo": "fenix", + "rev": "7d9ba794daf5e8cc7ee728859bc688d8e26d5f06", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "fenix", + "type": "github" } }, - "rust-overlay": { + "flake-utils": { "inputs": { - "nixpkgs": [ - "nixpkgs" - ] + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1742288794, + "narHash": "sha256-Txwa5uO+qpQXrNG4eumPSD+hHzzYi/CdaM80M9XRLCo=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "b6eaf97c6960d97350c584de1b6dcff03c9daf42", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1742923925, + "narHash": "sha256-biPjLws6FiBVUUDHEMFq5pUQL84Wf7PntPYdo3oKkFw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "25d1b84f5c90632a623c48d83a2faf156451e6b1", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "crane": "crane", + "fenix": "fenix", + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs_2" + } + }, + "rust-analyzer-src": { + "flake": false, + "locked": { + "lastModified": 1742296961, + "narHash": "sha256-gCpvEQOrugHWLimD1wTFOJHagnSEP6VYBDspq96Idu0=", + "owner": "rust-lang", + "repo": "rust-analyzer", + "rev": "15d87419f1a123d8f888d608129c3ce3ff8f13d4", + "type": "github" + }, + "original": { + "owner": "rust-lang", + "ref": "nightly", + "repo": "rust-analyzer", + "type": "github" + } + }, + "systems": { "locked": { - "lastModified": 1740796337, - "narHash": "sha256-FuoXrXZPoJEZQ3PF7t85tEpfBVID9JQIOnVKMNfTAb0=", - "owner": "oxalica", - "repo": "rust-overlay", - "rev": "bbac9527bc6b28b6330b13043d0e76eac11720dc", + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", "type": "github" }, "original": { - "owner": "oxalica", - "repo": "rust-overlay", + "owner": "nix-systems", + "repo": "default", "type": "github" } } diff --git a/flake.nix b/flake.nix index dab6dbb..48b81cb 100644 --- a/flake.nix +++ b/flake.nix @@ -1,86 +1,50 @@ { - description = "A Nix-flake-based Rust development environment"; - + description = "Flake configuration file for translatable.rs development."; inputs = { - nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.1.*.tar.gz"; - rust-overlay = { - url = "github:oxalica/rust-overlay"; - inputs.nixpkgs.follows = "nixpkgs"; - }; + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + crane.url = "github:ipetkov/crane"; + fenix.url = "github:nix-community/fenix"; + flake-utils.url = "github:numtide/flake-utils"; }; outputs = { - self, nixpkgs, - rust-overlay, - }: - let - supportedSystems = [ - "x86_64-linux" - "aarch64-linux" - "x86_64-darwin" - "aarch64-darwin" - ]; - forEachSupportedSystem = - f: - nixpkgs.lib.genAttrs supportedSystems ( - system: - f { - pkgs = import nixpkgs { - inherit system; - overlays = [ - rust-overlay.overlays.default - self.overlays.default - ]; - }; - } - ); - in - { - overlays.default = final: prev: { - rustToolchain = - let - rust = prev.rust-bin; - in - if builtins.pathExists ./rust-toolchain.toml then - rust.fromRustupToolchainFile ./rust-toolchain.toml - else if builtins.pathExists ./rust-toolchain then - rust.fromRustupToolchainFile ./rust-toolchain - else - rust.stable.latest.default.override { - extensions = [ - "rust-src" - "rustfmt" - ]; - }; - }; - - devShells = forEachSupportedSystem ( - { pkgs }: - { - default = pkgs.mkShell { - packages = with pkgs; [ - # General - openssl - pkg-config + flake-utils, + fenix, + ... + }@inputs: + flake-utils.lib.eachDefaultSystem ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + crane = inputs.crane.mkLib pkgs; - # Rust - cargo-deny - cargo-edit - cargo-watch - rust-analyzer - rustToolchain - ]; + toolchain = + with fenix.packages.${system}; + combine [ + minimal.rustc + minimal.cargo + complete.rust-src + complete.rustfmt + complete.clippy + ]; - env = { - # Required by rust-analyzer - RUST_SRC_PATH = "${pkgs.rustToolchain}/lib/rustlib/src/rust/library"; + craneLib = crane.overrideToolchain toolchain; + in + { + devShells.default = craneLib.devShell { + packages = with pkgs; [ + toolchain + rustfmt + clippy + qemu-user + ]; - LAZYVIM_RUST_DIAGNOSTICS = "bacon-ls"; - }; + env = { + LAZYVIM_RUST_DIAGNOSTICS = "bacon-ls"; }; - } - ); - }; + }; + } + ); } diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..4d191db --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,36 @@ +# Formatting width settings +max_width = 100 +use_small_heuristics = "Max" + +# Indentation & spacing +tab_spaces = 4 +hard_tabs = false +newline_style = "Unix" + +# Function & struct formatting +fn_single_line = false +struct_lit_width = 40 +struct_variant_width = 40 + +# Imports & ordering +imports_granularity = "Module" +group_imports = "StdExternalCrate" +reorder_imports = true +reorder_modules = true + +# Wrapping & line breaking +wrap_comments = true +format_strings = true +match_arm_leading_pipes = "Preserve" +match_block_trailing_comma = true +trailing_comma = "Vertical" + +# Miscellaneous +condense_wildcard_suffixes = true +force_explicit_abi = true +merge_derives = true +normalize_comments = true +normalize_doc_attributes = true +use_field_init_shorthand = true +use_try_shorthand = true +edition = "2024" diff --git a/translatable/src/lib.rs b/translatable/src/lib.rs index 1998721..b8a03ad 100644 --- a/translatable/src/lib.rs +++ b/translatable/src/lib.rs @@ -1,37 +1,32 @@ use thiserror::Error; - -// re export the macro in the main crate. +/// Re-export the procedural macro for crate users pub use translatable_proc::translation; -/// This error is used on results for the -/// translation procedural macro, the macro -/// will return a Result, Error>, -/// when there is a dynamic expression to resolve. -/// -/// For example, if the language is a dynamic expression -/// meaning it's not a literal &'static str, and it evaluates -/// on runtime, if the runtime evaluation is invalid because -/// the language does not match the ISO 639-1 specification -/// or something else, the translation macro will return an -/// Error::InvalidLanguage. +/// Error type for translation resolution failures /// -/// For more information on the possible errors read each -/// enum branch documentation. +/// Returned by the translation macro when dynamic resolution fails. +/// For static resolution failures, errors are reported at compile time. #[derive(Error, Debug)] pub enum Error { + /// Invalid ISO 639-1 language code provided #[error("The language '{0}' is invalid.")] InvalidLanguage(String), + /// Translation exists but not available for specified language #[error("The langauge '{0}' is not available for the path '{1}'")] LanguageNotAvailable(String, String), + /// Requested translation path doesn't exist in any translation files #[error("The path '{0}' was not found in any of the translations files.")] - PathNotFound(String) + PathNotFound(String), } impl Error { - /// This method is a convenience implementation - /// to obtain the `Display` from each error. + /// Returns formatted error message as a String + /// + /// Useful for error reporting and logging. Marked `#[cold]` to hint to the + /// compiler that this path is unlikely to be taken (optimization for error + /// paths). #[inline] #[cold] pub fn cause(&self) -> String { @@ -39,35 +34,38 @@ impl Error { } } +/// Internal implementation details for translation resolution #[doc(hidden)] pub mod internal { use std::collections::HashMap; + /// Represents nested translation structures #[doc(hidden)] pub enum NestingType { + /// Intermediate node containing nested translation objects Object(HashMap), - Translation(HashMap) + /// Leaf node containing actual translations for different languages + Translation(HashMap), } #[doc(hidden)] impl NestingType { + /// Resolves a translation path through nested structures + /// + /// # Arguments + /// * `path` - Slice of path segments to resolve + /// + /// # Returns + /// - `Some(&HashMap)` if path resolves to translations + /// - `None` if path is invalid pub fn get_path(&self, path: Vec<&str>) -> Option<&HashMap> { match self { Self::Object(nested) => { let (first, rest) = path.split_first()?; - - nested - .get(*first) - .and_then(|n| n.get_path(rest.to_vec())) + nested.get(*first)?.get_path(rest.to_vec()) }, - Self::Translation(translation) => { - if path.is_empty() { - return Some(translation) - } - - None - } + Self::Translation(translation) => path.is_empty().then_some(translation), } } } diff --git a/translatable_proc/src/data/config.rs b/translatable_proc/src/data/config.rs index cb919c0..3d1fc8c 100644 --- a/translatable_proc/src/data/config.rs +++ b/translatable_proc/src/data/config.rs @@ -3,12 +3,14 @@ //! This module provides functionality to load and manage configuration //! settings for localization/translation workflows from a TOML file. -use serde::Deserialize; use std::fs::read_to_string; use std::io::Error as IoError; use std::sync::OnceLock; + +use serde::Deserialize; use thiserror::Error; -use toml::{de::Error as TomlError, from_str as toml_from_str}; +use toml::de::Error as TomlError; +use toml::from_str as toml_from_str; /// Errors that can occur during configuration loading #[derive(Error, Debug)] @@ -82,7 +84,8 @@ pub struct TranslatableConfig { /// Translation conflict resolution strategy /// - /// Determines behavior when multiple files contain the same translation path + /// Determines behavior when multiple files contain the same translation + /// path #[serde(default)] overlap: TranslationOverlap, } @@ -123,11 +126,10 @@ pub fn load_config() -> Result<&'static TranslatableConfig, ConfigError> { return Ok(config); } - let config: TranslatableConfig = toml_from_str( - read_to_string("./translatable.toml") + let config: TranslatableConfig = + toml_from_str(read_to_string("./translatable.toml") .unwrap_or("".into()) // if no config file is found use defaults. - .as_str(), - )?; + .as_str())?; Ok(TRANSLATABLE_CONFIG.get_or_init(|| config)) } diff --git a/translatable_proc/src/data/translations.rs b/translatable_proc/src/data/translations.rs index ffebd89..4adf4a9 100644 --- a/translatable_proc/src/data/translations.rs +++ b/translatable_proc/src/data/translations.rs @@ -1,45 +1,65 @@ -use super::config::{load_config, SeekMode, TranslationOverlap}; -use crate::languages::Iso639a; use std::collections::HashMap; use std::fs::{read_dir, read_to_string}; use std::sync::OnceLock; + use proc_macro2::{Span, TokenStream}; use quote::quote; use strum::ParseError; use syn::LitStr; use thiserror::Error; use toml::{Table, Value}; + +use super::config::{SeekMode, TranslationOverlap, load_config}; +use crate::languages::Iso639a; use crate::translations::errors::TranslationError; +/// Errors occurring during TOML-to-translation structure transformation #[derive(Error, Debug)] pub enum TransformError { + /// Mixed content found in nesting node (strings and objects cannot coexist) #[error("A nesting can contain either strings or other nestings, but not both.")] InvalidNesting, + /// Template syntax error with unbalanced braces #[error("Templates in translations should match '{{' and '}}'")] UnclosedTemplate, + /// Invalid value type encountered in translation structure #[error("Only strings and objects are allowed for nested objects.")] InvalidValue, + /// Failed to parse language code from translation key #[error("Couldn't parse ISO 639-1 string for translation key")] - LanguageParsing(#[from] ParseError) + LanguageParsing(#[from] ParseError), } +/// Represents hierarchical translation structure #[derive(Clone)] pub enum NestingType { + /// Nested namespace containing other translation objects Object(HashMap), - Translation(HashMap) + /// Leaf node containing actual translations per language + Translation(HashMap), } +/// Translation association with its source file pub struct AssociatedTranslation { + /// Original file path of the translation original_path: String, - translation_table: NestingType + /// Hierarchical translation data + translation_table: NestingType, } -/// Global cache for loaded translations +/// Global thread-safe cache for loaded translations static TRANSLATIONS: OnceLock> = OnceLock::new(); +/// Recursively walks directory to find all translation files +/// +/// # Arguments +/// * `path` - Root directory to scan +/// +/// # Returns +/// Vec of file paths or TranslationError fn walk_dir(path: &str) -> Result, TranslationError> { let mut stack = vec![path.to_string()]; let mut result = Vec::new(); @@ -51,11 +71,7 @@ fn walk_dir(path: &str) -> Result, TranslationError> { for entry in directory { let path = entry.path(); if path.is_dir() { - stack.push( - path.to_str() - .ok_or(TranslationError::InvalidUnicode)? - .to_string(), - ); + stack.push(path.to_str().ok_or(TranslationError::InvalidUnicode)?.to_string()); } else { result.push(path.to_string_lossy().to_string()); } @@ -65,6 +81,7 @@ fn walk_dir(path: &str) -> Result, TranslationError> { Ok(result) } +/// Validates template brace balancing in translation strings fn templates_valid(translation: &str) -> bool { let mut nestings = 0; @@ -72,17 +89,22 @@ fn templates_valid(translation: &str) -> bool { match character { '{' => nestings += 1, '}' => nestings -= 1, - _ => {} + _ => {}, } } nestings == 0 } -/// Load translations from configured directory with thread-safe caching +/// Loads and caches translations from configured directory /// /// # Returns -/// Reference to loaded translations or TranslationError +/// Reference to cached translations or TranslationError +/// +/// # Implementation Details +/// - Uses OnceLock for thread-safe initialization +/// - Applies sorting based on configuration +/// - Handles file parsing and validation pub fn load_translations() -> Result<&'static Vec, TranslationError> { if let Some(translations) = TRANSLATIONS.get() { return Ok(translations); @@ -91,7 +113,7 @@ pub fn load_translations() -> Result<&'static Vec, Transl let config = load_config()?; let mut translation_paths = walk_dir(config.path())?; - // Sort paths case-insensitively + // Apply sorting based on configuration translation_paths.sort_by_key(|path| path.to_lowercase()); if let SeekMode::Unalphabetical = config.seek_mode() { translation_paths.reverse(); @@ -107,11 +129,12 @@ pub fn load_translations() -> Result<&'static Vec, Transl Ok(AssociatedTranslation { original_path: path.to_string(), translation_table: NestingType::try_from(table) - .map_err(|err| TranslationError::InvalidTomlFormat(err, path.to_string()))? + .map_err(|err| TranslationError::InvalidTomlFormat(err, path.to_string()))?, }) }) .collect::, TranslationError>>()?; + // Handle translation overlap configuration if let TranslationOverlap::Overwrite = config.overlap() { translations.reverse(); } @@ -120,38 +143,34 @@ pub fn load_translations() -> Result<&'static Vec, Transl } impl NestingType { + /// Resolves a translation path through the nesting hierarchy + /// + /// # Arguments + /// * `path` - Slice of path segments to resolve + /// + /// # Returns + /// Reference to translations if path exists and points to leaf node pub fn get_path(&self, path: Vec<&str>) -> Option<&HashMap> { match self { Self::Object(nested) => { let (first, rest) = path.split_first()?; - - nested - .get(*first) - .and_then(|n| n.get_path(rest.to_vec())) + nested.get(*first)?.get_path(rest.to_vec()) }, - - Self::Translation(translation) => { - if path.is_empty() { - return Some(translation) - } - - None - } + Self::Translation(translation) => path.is_empty().then_some(translation), } } } impl From for TokenStream { + /// Converts NestingType to procedural macro output tokens fn from(val: NestingType) -> Self { match val { NestingType::Object(nesting) => { - let entries = nesting - .into_iter() - .map(|(key, value)| -> TokenStream { - let key = LitStr::new(&key, Span::call_site()); - let value: TokenStream = value.into(); - quote! { (#key.to_string(), #value) } - }); + let entries = nesting.into_iter().map(|(key, value)| -> TokenStream { + let key = LitStr::new(&key, Span::call_site()); + let value: TokenStream = value.into(); + quote! { (#key.to_string(), #value) } + }); quote! { translatable::internal::NestingType::Object(vec![#(#entries),*].into_iter().collect()) @@ -159,19 +178,17 @@ impl From for TokenStream { }, NestingType::Translation(translation) => { - let entries = translation - .into_iter() - .map(|(lang, value)| { - let lang = LitStr::new(&format!("{lang:?}").to_lowercase(), Span::call_site()); - let value = LitStr::new(&value, Span::call_site()); + let entries = translation.into_iter().map(|(lang, value)| { + let lang = LitStr::new(&format!("{lang:?}").to_lowercase(), Span::call_site()); + let value = LitStr::new(&value, Span::call_site()); - quote! { (#lang.to_string(), #value.to_string()) } - }); + quote! { (#lang.to_string(), #value.to_string()) } + }); quote! { translatable::internal::NestingType::Translation(vec![#(#entries),*].into_iter().collect()) } - } + }, } } } @@ -179,70 +196,54 @@ impl From for TokenStream { impl TryFrom
for NestingType { type Error = TransformError; + /// Converts TOML table to validated translation structure fn try_from(value: Table) -> Result { let mut result = None; for (key, value) in value { match value { Value::String(translation_value) => { - if result.is_none() { - result = Some(Self::Translation(HashMap::new())); - } - - if !templates_valid(&translation_value) { - return Err(TransformError::UnclosedTemplate); - } + // Initialize result if first entry + let result = result.get_or_insert_with(|| Self::Translation(HashMap::new())); match result { - Some(Self::Translation(ref mut translation)) => { + Self::Translation(translation) => { + if !templates_valid(&translation_value) { + return Err(TransformError::UnclosedTemplate); + } translation.insert(key.parse()?, translation_value); }, - - Some(Self::Object(_)) => { - return Err(TransformError::InvalidNesting); - }, - - None => unreachable!() + Self::Object(_) => return Err(TransformError::InvalidNesting), } }, Value::Table(nesting_value) => { - if result.is_none() { - result = Some(Self::Object(HashMap::new())); - } + let result = result.get_or_insert_with(|| Self::Object(HashMap::new())); match result { - Some(Self::Object(ref mut nesting)) => { + Self::Object(nesting) => { nesting.insert(key, Self::try_from(nesting_value)?); }, - - Some(Self::Translation(_)) => { - return Err(TransformError::InvalidNesting); - }, - - None => unreachable!() + Self::Translation(_) => return Err(TransformError::InvalidNesting), } }, - _ => { - return Err(TransformError::InvalidValue) - } + _ => return Err(TransformError::InvalidValue), } } - match result { - Some(result) => Ok(result), - None => unreachable!() - } + result.ok_or(TransformError::InvalidValue) } } impl AssociatedTranslation { + /// Gets the original file path of the translation #[allow(unused)] pub fn original_path(&self) -> &str { &self.original_path } + /// Gets reference to the translation data structure #[allow(unused)] pub fn translation_table(&self) -> &NestingType { &self.translation_table diff --git a/translatable_proc/src/languages.rs b/translatable_proc/src/languages.rs index f6d575f..1deef53 100644 --- a/translatable_proc/src/languages.rs +++ b/translatable_proc/src/languages.rs @@ -377,9 +377,12 @@ pub enum Iso639a { ZU, } +/// This struct represents a list of similar languages to the provided one. pub struct Similarities { + /// Indicates how many languages are not included in the list. overflow_by: usize, - similarities: Vec + /// List of similar languages. + similarities: Vec, } impl Similarities { @@ -393,6 +396,7 @@ impl Similarities { } impl Iso639a { + /// This method returns a list of similar languages to the provided one. pub fn get_similarities(lang: &str, max_amount: usize) -> Similarities { let all_similarities = Self::iter() .map(|variant| format!("{variant:#} ({variant:?})")) @@ -403,16 +407,13 @@ impl Iso639a { if overflow_by > 0 { Similarities { - similarities: all_similarities - .into_iter() - .take(max_amount) - .collect(), - overflow_by: overflow_by as usize + similarities: all_similarities.into_iter().take(max_amount).collect(), + overflow_by: overflow_by as usize, } } else { Similarities { similarities: all_similarities, - overflow_by: 0 + overflow_by: 0, } } } diff --git a/translatable_proc/src/lib.rs b/translatable_proc/src/lib.rs index a6c2b93..1989353 100644 --- a/translatable_proc/src/lib.rs +++ b/translatable_proc/src/lib.rs @@ -1,4 +1,5 @@ -//! Internationalization library providing compile-time and runtime translation facilities +//! Internationalization library providing compile-time and runtime translation +//! facilities //! //! # Features //! - TOML-based translation files diff --git a/translatable_proc/src/macros.rs b/translatable_proc/src/macros.rs index ea15439..cab7b92 100644 --- a/translatable_proc/src/macros.rs +++ b/translatable_proc/src/macros.rs @@ -1,39 +1,52 @@ -use crate::translations::generation::{ - load_lang_dynamic, load_lang_static, load_translation_dynamic, load_translation_static, -}; use proc_macro2::TokenStream; use quote::quote; use syn::parse::{Parse, ParseStream}; use syn::token::Static; use syn::{Expr, ExprLit, ExprPath, Lit, Result as SynResult, Token}; -/// Internal representation of macro arguments before processing +use crate::translations::generation::{ + load_lang_dynamic, load_lang_static, load_translation_dynamic, load_translation_static, +}; + +/// Represents raw input arguments for the translation macro +/// +/// Parses input in the format: `(language_spec, static translation_path)` /// -/// Parses input in the format: -/// `(language_expression, static translation_path)` +/// # Syntax +/// - `language_spec`: String literal or expression implementing `Into` +/// - `translation_path`: Path expression (either static or dynamic) pub struct RawMacroArgs { - /// Language specification (literal or expression) + /// Language specification (either literal string or expression) language: Expr, - /// Argument separator. + /// Comma separator between arguments _comma: Token![,], - /// Static marker for path analysis + /// Optional `static` keyword marker for path resolution static_marker: Option, - /// Translation path specification + /// Translation path (either static path or dynamic expression) path: Expr, } +/// Represents the type of translation path resolution pub enum PathType { + /// Runtime-resolved path expression OnScopeExpression(TokenStream), + /// Compile-time resolved path string CompileTimePath(String), } +/// Represents the type of language specification pub enum LanguageType { + /// Runtime-resolved language expression OnScopeExpression(TokenStream), + /// Compile-time validated language literal CompileTimeLiteral(String), } +/// Processed translation arguments ready for code generation pub struct TranslationArgs { + /// Language resolution type language: LanguageType, + /// Path resolution type path: PathType, } @@ -53,70 +66,83 @@ impl From for TranslationArgs { let is_path_static = val.static_marker.is_some(); TranslationArgs { + // Extract language specification language: match val.language { - Expr::Lit(ExprLit { - lit: Lit::Str(lit_str), - .. - }) => LanguageType::CompileTimeLiteral(lit_str.value()), + Expr::Lit(ExprLit { lit: Lit::Str(lit_str), .. }) => { + LanguageType::CompileTimeLiteral(lit_str.value()) + }, + // Preserve other expressions for runtime resolution other => LanguageType::OnScopeExpression(quote!(#other)), }, + // Extract path specification path: match val.path { - Expr::Path(ExprPath { path, .. }) if is_path_static => PathType::CompileTimePath( - path.segments - .iter() - .map(|s| s.ident.to_string()) - .collect::>() - .join(".") - .to_string(), - ), + // Convert path expressions to strings when static marker present + Expr::Path(ExprPath { path, .. }) if is_path_static => { + // Convert path segments to a dot-separated string + let path_str = path.segments.iter().map(|s| s.ident.to_string()).fold( + String::new(), + |mut acc, s| { + if !acc.is_empty() { + acc.push('.'); + } + acc.push_str(&s); + acc + }, + ); + PathType::CompileTimePath(path_str) + }, + // Preserve dynamic path expressions path => PathType::OnScopeExpression(quote!(#path)), }, } } } +/// Generates translation code based on processed arguments +/// +/// # Arguments +/// - `args`: Processed translation arguments +/// +/// # Returns +/// TokenStream with either: +/// - Compiled translation string +/// - Runtime translation resolution logic +/// - Compile errors for invalid inputs pub fn translation_macro(args: TranslationArgs) -> TokenStream { let TranslationArgs { language, path } = args; + // Process language specification let (lang_expr, static_lang) = match language { LanguageType::CompileTimeLiteral(lang) => ( None, match load_lang_static(&lang) { Ok(lang) => Some(lang), - Err(e) => { - let e = format!("{e:#}"); - return quote! { compile_error!(#e) }; - } + Err(e) => return error_token(&e), }, ), - LanguageType::OnScopeExpression(lang) => (Some(load_lang_dynamic(lang)), None), + LanguageType::OnScopeExpression(lang) => { + (Some(load_lang_dynamic(lang).map_err(|e| error_token(&e))), None) + }, }; + // Process translation path let translation_expr = match path { PathType::CompileTimePath(p) => load_translation_static(static_lang, p), PathType::OnScopeExpression(p) => load_translation_dynamic(static_lang, p), }; match (lang_expr, translation_expr) { - (Some(lang), Ok(trans)) => match lang { - Ok(lang) => { - quote! {{ - #lang - #trans - }} - } - - Err(e) => { - let e = format!("{e:#}"); - quote! { compile_error!{#e} } - } - }, + (Some(Ok(lang)), Ok(trans)) => quote! {{ #lang #trans }}, + (Some(Err(e)), _) => e, (None, Ok(trans)) => trans, - (_, Err(e)) => { - let e = format!("{e:#}"); - quote! { compile_error!(#e) } - } + (_, Err(e)) => error_token(&e), } } + +/// Helper function to create compile error tokens +fn error_token(e: &impl std::fmt::Display) -> TokenStream { + let msg = format!("{e:#}"); + quote! { compile_error!(#msg) } +} diff --git a/translatable_proc/src/translations/errors.rs b/translatable_proc/src/translations/errors.rs index b64bde0..ccb49b8 100644 --- a/translatable_proc/src/translations/errors.rs +++ b/translatable_proc/src/translations/errors.rs @@ -1,11 +1,13 @@ -use crate::languages::Iso639a; -use crate::data::translations::TransformError; -use crate::data::config::ConfigError; use std::io::Error as IoError; + use syn::Error as SynError; use thiserror::Error; use toml::de::Error as TomlError; +use crate::data::config::ConfigError; +use crate::data::translations::TransformError; +use crate::languages::Iso639a; + /// Errors that can occur during translation processing. #[derive(Error, Debug)] pub enum TranslationError { @@ -59,12 +61,15 @@ pub enum TranslationError { #[error("Invalid TOML structure in file {1}: {0}")] InvalidTomlFormat(TransformError, String), + /// Path not found in any translation file #[error("The path '{0}' is not found in any of the translation files as a translation object.")] PathNotFound(String), + /// Language not available for the specified path #[error("The language '{0:?}' ({0:#}) is not available for the '{1}' translation.")] LanguageNotAvailable(Iso639a, String), + /// Error parsing macro. #[error("Error parsing macro.")] MacroError(#[from] SynError), } diff --git a/translatable_proc/src/translations/generation.rs b/translatable_proc/src/translations/generation.rs index edc62d1..0b6f361 100644 --- a/translatable_proc/src/translations/generation.rs +++ b/translatable_proc/src/translations/generation.rs @@ -1,25 +1,37 @@ -use super::errors::TranslationError; -use crate::data::translations::load_translations; -use crate::languages::Iso639a; use proc_macro2::TokenStream; use quote::quote; use strum::IntoEnumIterator; use syn::{Expr, parse2}; -/// This function parses a statically obtained language -/// as an Iso639a enum instance, along this, the validation -/// is also done at parse time. +use super::errors::TranslationError; +use crate::data::translations::load_translations; +use crate::languages::Iso639a; + +/// Parses a static language string into an Iso639a enum instance with +/// compile-time validation. +/// +/// # Arguments +/// * `lang` - A string slice representing the language code to parse +/// +/// # Returns +/// - `Ok(Iso639a)` if valid language code +/// - `Err(TranslationError)` if parsing fails pub fn load_lang_static(lang: &str) -> Result { - lang.parse::() - .map_err(|_| TranslationError::InvalidLanguage(lang.to_string())) + lang.parse::().map_err(|_| TranslationError::InvalidLanguage(lang.to_string())) } -/// This function generates a language variable, the only -/// requisite is that the expression evaluates to something -/// that implements Into. +/// Generates runtime validation for a dynamic language expression. +/// +/// # Arguments +/// * `lang` - TokenStream representing an expression that implements +/// `Into` +/// +/// # Returns +/// TokenStream with code to validate language at runtime pub fn load_lang_dynamic(lang: TokenStream) -> Result { let lang: Expr = parse2(lang)?; + // Generate list of available language codes let available_langs = Iso639a::iter().map(|language| { let language = format!("{language:?}"); @@ -42,17 +54,21 @@ pub fn load_lang_dynamic(lang: TokenStream) -> Result, path: String, ) -> Result { let translation_object = load_translations()? .iter() - .find_map(|association| { - association - .translation_table() - .get_path(path.split(".").collect()) - }) + .find_map(|association| association.translation_table().get_path(path.split('.').collect())) .ok_or(TranslationError::PathNotFound(path.to_string()))?; Ok(match static_lang { @@ -62,7 +78,7 @@ pub fn load_translation_static( .ok_or(TranslationError::LanguageNotAvailable(language, path))?; quote! { #translation } - } + }, None => { let translation_object = translation_object.iter().map(|(key, value)| { @@ -83,10 +99,18 @@ pub fn load_translation_static( Err(translatable::Error::InvalidLanguage(language)) } }} - } + }, }) } +/// Loads translations for dynamic language and path resolution +/// +/// # Arguments +/// * `static_lang` - Optional predefined language +/// * `path` - TokenStream representing dynamic path expression +/// +/// # Returns +/// TokenStream with runtime translation resolution logic pub fn load_translation_dynamic( static_lang: Option, path: TokenStream, @@ -102,16 +126,15 @@ pub fn load_translation_dynamic( #[doc(hidden)] let nested_translations = vec![#(#nestings),*]; + #[doc(hidden)] let translation = nested_translations .iter() - .find_map(|nesting| nesting - .get_path( - path - .split(".") - .collect() - ) - ); + .find_map(|nesting| nesting.get_path( + path + .split('.') + .collect() + )); }; Ok(match static_lang { @@ -130,7 +153,7 @@ pub fn load_translation_dynamic( Err(translatable::Error::PathNotFound(path)) } }} - } + }, None => { quote! {{ @@ -149,6 +172,6 @@ pub fn load_translation_dynamic( Err(translatable::Error::InvalidLanguage(language)) } }} - } + }, }) } From 01d38db2e7cd0ec4f36d99a89c6fe30d5900746f Mon Sep 17 00:00:00 2001 From: stifskere Date: Thu, 27 Mar 2025 11:17:07 +0100 Subject: [PATCH 034/228] fix: change #[doc(hidden)] place for NestingType::get_path --- translatable/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/translatable/src/lib.rs b/translatable/src/lib.rs index b8a03ad..1f93e2e 100644 --- a/translatable/src/lib.rs +++ b/translatable/src/lib.rs @@ -48,7 +48,6 @@ pub mod internal { Translation(HashMap), } - #[doc(hidden)] impl NestingType { /// Resolves a translation path through nested structures /// @@ -58,6 +57,7 @@ pub mod internal { /// # Returns /// - `Some(&HashMap)` if path resolves to translations /// - `None` if path is invalid + #[doc(hidden)] pub fn get_path(&self, path: Vec<&str>) -> Option<&HashMap> { match self { Self::Object(nested) => { From 68bf3035b715dba7e73912cab6f6284f182cf963 Mon Sep 17 00:00:00 2001 From: stifskere Date: Thu, 27 Mar 2025 13:51:56 +0100 Subject: [PATCH 035/228] feat: kwarg parsing for format --- translatable/tests/test.rs | 2 +- translatable_proc/src/macros.rs | 87 +++++++++++++++++-- .../src/translations/generation.rs | 4 + 3 files changed, 83 insertions(+), 10 deletions(-) diff --git a/translatable/tests/test.rs b/translatable/tests/test.rs index 8a3783f..8f1965d 100644 --- a/translatable/tests/test.rs +++ b/translatable/tests/test.rs @@ -29,7 +29,7 @@ fn language_dynamic_path_static() { fn both_dynamic() { let name = "john"; let language = "es"; - let result = translation!(language, "common.greeting"); + let result = translation!(language, "common.greeting", lol = 10, name); assert!(result.unwrap() == "Β‘Hola john!".to_string()) } diff --git a/translatable_proc/src/macros.rs b/translatable_proc/src/macros.rs index cab7b92..a4c8b7e 100644 --- a/translatable_proc/src/macros.rs +++ b/translatable_proc/src/macros.rs @@ -1,8 +1,11 @@ +use std::collections::HashMap; + use proc_macro2::TokenStream; -use quote::quote; +use quote::{quote, ToTokens}; use syn::parse::{Parse, ParseStream}; +use syn::punctuated::Punctuated; use syn::token::Static; -use syn::{Expr, ExprLit, ExprPath, Lit, Result as SynResult, Token}; +use syn::{parse_quote, Expr, ExprLit, ExprPath, Lit, MetaNameValue, Path, Result as SynResult, Token, Ident}; use crate::translations::generation::{ load_lang_dynamic, load_lang_static, load_translation_dynamic, load_translation_static, @@ -24,6 +27,10 @@ pub struct RawMacroArgs { static_marker: Option, /// Translation path (either static path or dynamic expression) path: Expr, + + _comma2: Option, + + format_kwargs: Punctuated, } /// Represents the type of translation path resolution @@ -48,15 +55,58 @@ pub struct TranslationArgs { language: LanguageType, /// Path resolution type path: PathType, + + format_kwargs: HashMap } impl Parse for RawMacroArgs { fn parse(input: ParseStream) -> SynResult { + let language = input.parse()?; + let _comma = input.parse()?; + let static_marker = input.parse()?; + let path = input.parse()?; + + let _comma2 = if input.peek(Token![,]) { + Some(input.parse()?) + } else { + None + }; + + let mut format_kwargs = Punctuated::new(); + + if _comma2.is_some() { + while !input.is_empty() { + let lookahead = input.lookahead1(); + + if lookahead.peek(Ident) { + let key: Ident = input.parse()?; + let eq_token: Token![=] = input.parse().unwrap_or(Token![=](key.span())); + let value: Expr = input.parse().unwrap_or(parse_quote!(#key)); + + format_kwargs.push(MetaNameValue { + path: Path::from(key), + eq_token, + value + }); + } else { + format_kwargs.push(input.parse()?); + } + + if input.peek(Token![,]) { + input.parse::()?; + } else { + break; + } + } + }; + Ok(RawMacroArgs { - language: input.parse()?, - _comma: input.parse()?, - static_marker: input.parse()?, - path: input.parse()?, + language, + _comma, + static_marker, + path, + _comma2, + format_kwargs, }) } } @@ -96,6 +146,25 @@ impl From for TranslationArgs { // Preserve dynamic path expressions path => PathType::OnScopeExpression(quote!(#path)), }, + + format_kwargs: val + .format_kwargs + .iter() + .map(|pair| ( + pair + .path + .get_ident() + .map(|i| i.to_string()) + .unwrap_or_else(|| pair + .path + .to_token_stream() + .to_string() + ), + pair + .value + .to_token_stream() + )) + .collect() } } } @@ -111,7 +180,7 @@ impl From for TranslationArgs { /// - Runtime translation resolution logic /// - Compile errors for invalid inputs pub fn translation_macro(args: TranslationArgs) -> TokenStream { - let TranslationArgs { language, path } = args; + let TranslationArgs { language, path, format_kwargs } = args; // Process language specification let (lang_expr, static_lang) = match language { @@ -129,8 +198,8 @@ pub fn translation_macro(args: TranslationArgs) -> TokenStream { // Process translation path let translation_expr = match path { - PathType::CompileTimePath(p) => load_translation_static(static_lang, p), - PathType::OnScopeExpression(p) => load_translation_dynamic(static_lang, p), + PathType::CompileTimePath(p) => load_translation_static(static_lang, p, format_kwargs), + PathType::OnScopeExpression(p) => load_translation_dynamic(static_lang, p, format_kwargs), }; match (lang_expr, translation_expr) { diff --git a/translatable_proc/src/translations/generation.rs b/translatable_proc/src/translations/generation.rs index 0b6f361..3d3f900 100644 --- a/translatable_proc/src/translations/generation.rs +++ b/translatable_proc/src/translations/generation.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use proc_macro2::TokenStream; use quote::quote; use strum::IntoEnumIterator; @@ -65,6 +67,7 @@ pub fn load_lang_dynamic(lang: TokenStream) -> Result, path: String, + format_kwargs: HashMap ) -> Result { let translation_object = load_translations()? .iter() @@ -114,6 +117,7 @@ pub fn load_translation_static( pub fn load_translation_dynamic( static_lang: Option, path: TokenStream, + format_kwargs: HashMap ) -> Result { let nestings = load_translations()? .iter() From 9573dadc271c5dfc69a4911c4a6c347b9ab35ec7 Mon Sep 17 00:00:00 2001 From: stifskere Date: Thu, 27 Mar 2025 16:01:48 +0100 Subject: [PATCH 036/228] feat: implement templating on the macros --- translatable/tests/test.rs | 12 ++---- translatable_proc/src/macros.rs | 3 +- .../src/translations/generation.rs | 43 ++++++++++++++++++- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/translatable/tests/test.rs b/translatable/tests/test.rs index 8f1965d..fca37d3 100644 --- a/translatable/tests/test.rs +++ b/translatable/tests/test.rs @@ -2,34 +2,30 @@ use translatable::translation; #[test] fn both_static() { - let name = "john"; - let result = translation!("es", static common::greeting); + let result = translation!("es", static common::greeting, name = "john"); assert!(result == "Β‘Hola john!") } #[test] fn language_static_path_dynamic() { - let name = "john"; - let result = translation!("es", "common.greeting"); + let result = translation!("es", "common.greeting", name = "john"); assert!(result.unwrap() == "Β‘Hola john!".to_string()) } #[test] fn language_dynamic_path_static() { - let name = "john"; let language = "es"; - let result = translation!(language, static common::greeting); + let result = translation!(language, static common::greeting, name = "john"); assert!(result.unwrap() == "Β‘Hola john!".to_string()) } #[test] fn both_dynamic() { - let name = "john"; let language = "es"; - let result = translation!(language, "common.greeting", lol = 10, name); + let result = translation!(language, "common.greeting", lol = 10, name = "john"); assert!(result.unwrap() == "Β‘Hola john!".to_string()) } diff --git a/translatable_proc/src/macros.rs b/translatable_proc/src/macros.rs index a4c8b7e..1a4267d 100644 --- a/translatable_proc/src/macros.rs +++ b/translatable_proc/src/macros.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::fmt::Display; use proc_macro2::TokenStream; use quote::{quote, ToTokens}; @@ -211,7 +212,7 @@ pub fn translation_macro(args: TranslationArgs) -> TokenStream { } /// Helper function to create compile error tokens -fn error_token(e: &impl std::fmt::Display) -> TokenStream { +fn error_token(e: &impl Display) -> TokenStream { let msg = format!("{e:#}"); quote! { compile_error!(#msg) } } diff --git a/translatable_proc/src/translations/generation.rs b/translatable_proc/src/translations/generation.rs index 3d3f900..b65a347 100644 --- a/translatable_proc/src/translations/generation.rs +++ b/translatable_proc/src/translations/generation.rs @@ -3,12 +3,26 @@ use std::collections::HashMap; use proc_macro2::TokenStream; use quote::quote; use strum::IntoEnumIterator; -use syn::{Expr, parse2}; +use syn::{parse2, parse_str, Error as SynError, Expr, Ident}; use super::errors::TranslationError; use crate::data::translations::load_translations; use crate::languages::Iso639a; +fn kwarg_dynamic_replaces(format_kwargs: HashMap) -> Vec { + format_kwargs + .iter() + .map(|(key, value)| + quote! { + .map(|translation| translation.replace( + format!("{{{}}}", #key).as_str(), + format!("{:#}", #value).as_str() + )) + } + ) + .collect::>() +} + /// Parses a static language string into an Iso639a enum instance with /// compile-time validation. /// @@ -80,7 +94,26 @@ pub fn load_translation_static( .get(&language) .ok_or(TranslationError::LanguageNotAvailable(language, path))?; - quote! { #translation } + if format_kwargs.is_empty() { + quote! { #translation } + } else { + let format_kwargs = format_kwargs + .iter() + .map(|(key, value)| { + let key: Ident = parse_str(&key)?; + Ok::(quote! { + #[doc(hidden)] + let #key = #value; + }) + }) + .collect::, _>>()?; + + quote! {{ + #(#format_kwargs)* + + format!(#translation) + }} + } }, None => { @@ -88,6 +121,7 @@ pub fn load_translation_static( let key = format!("{key:?}").to_lowercase(); quote! { (#key, #value) } }); + let replaces = kwarg_dynamic_replaces(format_kwargs); quote! {{ if valid_lang { @@ -98,6 +132,7 @@ pub fn load_translation_static( .ok_or(translatable::Error::LanguageNotAvailable(language, #path.to_string())) .cloned() .map(|translation| translation.to_string()) + #(#replaces)* } else { Err(translatable::Error::InvalidLanguage(language)) } @@ -141,6 +176,8 @@ pub fn load_translation_dynamic( )); }; + let replaces = kwarg_dynamic_replaces(format_kwargs); + Ok(match static_lang { Some(language) => { let language = format!("{language:?}").to_lowercase(); @@ -153,6 +190,7 @@ pub fn load_translation_dynamic( .get(#language) .ok_or(translatable::Error::LanguageNotAvailable(#language.to_string(), path)) .cloned() + #(#replaces)* } else { Err(translatable::Error::PathNotFound(path)) } @@ -169,6 +207,7 @@ pub fn load_translation_dynamic( .get(&language) .ok_or(translatable::Error::LanguageNotAvailable(language, path)) .cloned() + #(#replaces)* } else { Err(translatable::Error::PathNotFound(path)) } From 18764dc12d45a0452f7fb500ea27fb88b5b55606 Mon Sep 17 00:00:00 2001 From: stifskere Date: Thu, 27 Mar 2025 16:31:42 +0100 Subject: [PATCH 037/228] feat: possibly inefficient {} escaping --- .../src/translations/generation.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/translatable_proc/src/translations/generation.rs b/translatable_proc/src/translations/generation.rs index b65a347..b1e4ee0 100644 --- a/translatable_proc/src/translations/generation.rs +++ b/translatable_proc/src/translations/generation.rs @@ -14,10 +14,20 @@ fn kwarg_dynamic_replaces(format_kwargs: HashMap) -> Vec a temporary placeholder + format!("\x01{{{}}}\x01", #key).as_str() + ) + .replace( + format!("{{{}}}", #key).as_str(), // Replace {key} -> value + format!("{:#}", #value).as_str() + ) + .replace( + format!("\x01{{{}}}\x01", #key).as_str(), // Restore {key} from the placeholder + format!("{{{}}}", #key).as_str() + ) + ) } ) .collect::>() From 471200955a9a757819885ab83d000683daa0253e Mon Sep 17 00:00:00 2001 From: stifskere Date: Thu, 27 Mar 2025 17:35:48 +0100 Subject: [PATCH 038/228] docs: readme --- README.md | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index f966d49..c856ddd 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,9 @@ A robust internationalization solution for Rust featuring compile-time validation, ISO 639-1 compliance, and TOML-based translation management. +**This library prioritizes ergonomics over raw performance.** +Our goal is not to be *blazingly fast* but to provide the most user-friendly experience for implementing translationsβ€”whether you're a first-time user or an experienced developer. If you require maximum performance, consider alternative libraries, a custom implementation, or even hard-coded values on the stack. + ## Table of Contents πŸ“– - [Features](#features-) @@ -63,7 +66,7 @@ The translation files have three rules The load configuration such as `seek_mode` and `overlap` is not relevant here, as previously specified, these configuration values only get applied once by reversing the translations conveniently. -To load translations you make use of the `translatable::translation` macro, that macro requires two +To load translations you make use of the `translatable::translation` macro, that macro requires at least two parameters to be passed. The first parameter consists of the language which can be passed dynamically as a variable or an expression @@ -74,6 +77,12 @@ The second parameter consists of the path, which can be passed dynamically as a that resolves to an `impl Into` with the format `path.to.translation`, or statically with the following syntax `static path::to::translation`. +The rest of parameters are `meta-variable patterns` also known as `key = value` parameters or key-value pairs, +these are processed as replaces, *or format if the call is all-static*. When a template (`{}`) is found with +the name of a key inside it gets replaced for whatever is the `Display` implementation of the value. This meaning +that the value must always implement `Display`. Otherwise, if you want to have a `{}` inside your translation, +you can escape it the same way `format!` does, by using `{{}}`. + Depending on whether the parameters are static or dynamic the macro will act different, differing whether the checks are compile-time or run-time, the following table is a macro behavior matrix. @@ -82,7 +91,7 @@ the checks are compile-time or run-time, the following table is a macro behavior | `static language` + `static path` (most optimized) | Path existence, Language validity, \*Template validation | `&'static str` (stack) if there are no templates or `String` (heap) if there are. | | `dynamic language` + `dynamic path` | None | `Result` (heap) | | `static language` + `dynamic path` | Language validity | `Result` (heap) | -| `dynamic language` + `static path` (commonly used) | Path existence, \*Template validation | `Result` (heap) | +| `dynamic language` + `static path` (commonly used) | Path existence | `Result` (heap) | - For the error handling, if you want to integrate this with `thiserror` you can use a `#[from] translatable::TranslationError`, as a nested error, all the errors implement display, for optimization purposes there are not the same amount of errors with @@ -91,10 +100,11 @@ dynamic parameters than there are with static parameters. - The runtime errors implement a `cause()` method that returns a heap allocated `String` with the error reason, essentially the error display. -- Template validation in the static parameter handling means variable existence, since templates are generated as a `format!` -call which processes expressions found in scope. It's always recommended to use full paths in translation templates -to avoid needing to make variables in scope, unless the calls are contextual, in that case there is nothing that can -be done to avoid making variables. +- Template validation in static parameter handling means purely variable existence, an all-static invocation +generates a quoted translation (`""`), essentially the same value you can find in your translation file, so if the +invocation is all-static the macro will generate a `format!` call, which implicitly validates the variable +existence, if the variable is found outer scope the macro may use that. In the case where any of the +parameters is dynamic, the macro will return an error if some replacement couldn't be found. ## Example implementation πŸ“‚ @@ -129,22 +139,21 @@ es = "Β‘Hola {name}!" ### Example application usage -Notice how that template is in scope, whole expressions can be used -in the templates such as `path::to::function()`, or other constants. +Notice how there is a template, this template is being replaced by the +`name = "john"` key value pair passed as third parameter. ```rust extern crate translatable; use translatable::translation; fn main() { - let dynamic_lang = "es"; - let dynamic_path = "common.greeting" - let name = "john"; - - assert!(translation!("es", static common::greeting) == "Β‘Hola john!"); - assert!(translation!("es", dynamic_path).unwrap() == "Β‘Hola john!".into()); - assert!(translation!(dynamic_lang, static common::greeting).unwrap() == "Β‘Hola john!".into()); - assert!(translation!(dynamic_lang, dynamic_path).unwrap() == "Β‘Hola john!".into()); + let dynamic_lang = "es"; + let dynamic_path = "common.greeting" + + assert!(translation!("es", static common::greeting) == "Β‘Hola john!", name = "john"); + assert!(translation!("es", dynamic_path).unwrap() == "Β‘Hola john!".into(), name = "john"); + assert!(translation!(dynamic_lang, static common::greeting).unwrap() == "Β‘Hola john!".into(), name = "john"); + assert!(translation!(dynamic_lang, dynamic_path).unwrap() == "Β‘Hola john!".into(), name = "john"); } ``` From 15034253474096c7da4440bf488e28eeaca08566 Mon Sep 17 00:00:00 2001 From: stifskere Date: Thu, 27 Mar 2025 19:20:17 +0100 Subject: [PATCH 039/228] feat: completly remove template validation, as it's kinda pointless. --- README.md | 11 +-- .../src/translations/generation.rs | 72 +++++++++---------- 2 files changed, 37 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index c856ddd..190a105 100644 --- a/README.md +++ b/README.md @@ -81,14 +81,15 @@ The rest of parameters are `meta-variable patterns` also known as `key = value` these are processed as replaces, *or format if the call is all-static*. When a template (`{}`) is found with the name of a key inside it gets replaced for whatever is the `Display` implementation of the value. This meaning that the value must always implement `Display`. Otherwise, if you want to have a `{}` inside your translation, -you can escape it the same way `format!` does, by using `{{}}`. +you can escape it the same way `format!` does, by using `{{}}`. Just like object construction works in rust, if +you have a parameter like `x = x`, you can shorten it to `x`. Depending on whether the parameters are static or dynamic the macro will act different, differing whether the checks are compile-time or run-time, the following table is a macro behavior matrix. | Parameters | Compile-Time checks | Return type | |----------------------------------------------------|----------------------------------------------------------|-----------------------------------------------------------------------------------| -| `static language` + `static path` (most optimized) | Path existence, Language validity, \*Template validation | `&'static str` (stack) if there are no templates or `String` (heap) if there are. | +| `static language` + `static path` (most optimized) | Path existence, Language validity | `&'static str` (stack) if there are no templates or `String` (heap) if there are. | | `dynamic language` + `dynamic path` | None | `Result` (heap) | | `static language` + `dynamic path` | Language validity | `Result` (heap) | | `dynamic language` + `static path` (commonly used) | Path existence | `Result` (heap) | @@ -100,12 +101,6 @@ dynamic parameters than there are with static parameters. - The runtime errors implement a `cause()` method that returns a heap allocated `String` with the error reason, essentially the error display. -- Template validation in static parameter handling means purely variable existence, an all-static invocation -generates a quoted translation (`""`), essentially the same value you can find in your translation file, so if the -invocation is all-static the macro will generate a `format!` call, which implicitly validates the variable -existence, if the variable is found outer scope the macro may use that. In the case where any of the -parameters is dynamic, the macro will return an error if some replacement couldn't be found. - ## Example implementation πŸ“‚ The following examples are an example application structure for a possible diff --git a/translatable_proc/src/translations/generation.rs b/translatable_proc/src/translations/generation.rs index b1e4ee0..c7c1a14 100644 --- a/translatable_proc/src/translations/generation.rs +++ b/translatable_proc/src/translations/generation.rs @@ -3,33 +3,40 @@ use std::collections::HashMap; use proc_macro2::TokenStream; use quote::quote; use strum::IntoEnumIterator; -use syn::{parse2, parse_str, Error as SynError, Expr, Ident}; +use syn::{parse2, Expr}; use super::errors::TranslationError; use crate::data::translations::load_translations; use crate::languages::Iso639a; -fn kwarg_dynamic_replaces(format_kwargs: HashMap) -> Vec { +fn kwarg_static_replaces(key: &str, value: &TokenStream) -> TokenStream { + quote! { + .replace( + format!("{{{{{}}}}}", #key).as_str(), // Replace {{key}} -> a temporary placeholder + format!("\x01{{{}}}\x01", #key).as_str() + ) + .replace( + format!("{{{}}}", #key).as_str(), // Replace {key} -> value + format!("{:#}", #value).as_str() + ) + .replace( + format!("\x01{{{}}}\x01", #key).as_str(), // Restore {key} from the placeholder + format!("{{{}}}", #key).as_str() + ) + } +} + +fn kwarg_dynamic_replaces(format_kwargs: &HashMap) -> Vec { format_kwargs .iter() - .map(|(key, value)| + .map(|(key, value)| { + let static_replaces = kwarg_static_replaces(key, value); quote! { .map(|translation| translation - .replace( - format!("{{{{{}}}}}", #key).as_str(), // Replace {{key}} -> a temporary placeholder - format!("\x01{{{}}}\x01", #key).as_str() - ) - .replace( - format!("{{{}}}", #key).as_str(), // Replace {key} -> value - format!("{:#}", #value).as_str() - ) - .replace( - format!("\x01{{{}}}\x01", #key).as_str(), // Restore {key} from the placeholder - format!("{{{}}}", #key).as_str() - ) + #static_replaces ) } - ) + }) .collect::>() } @@ -97,6 +104,7 @@ pub fn load_translation_static( .iter() .find_map(|association| association.translation_table().get_path(path.split('.').collect())) .ok_or(TranslationError::PathNotFound(path.to_string()))?; + let replaces = kwarg_dynamic_replaces(&format_kwargs); Ok(match static_lang { Some(language) => { @@ -104,26 +112,15 @@ pub fn load_translation_static( .get(&language) .ok_or(TranslationError::LanguageNotAvailable(language, path))?; - if format_kwargs.is_empty() { - quote! { #translation } - } else { - let format_kwargs = format_kwargs - .iter() - .map(|(key, value)| { - let key: Ident = parse_str(&key)?; - Ok::(quote! { - #[doc(hidden)] - let #key = #value; - }) - }) - .collect::, _>>()?; - - quote! {{ - #(#format_kwargs)* - - format!(#translation) - }} - } + let static_replaces = format_kwargs + .iter() + .map(|(key, value)| kwarg_static_replaces(key, value)) + .collect::>(); + + quote! {{ + #translation + #(#static_replaces)* + }} }, None => { @@ -131,7 +128,6 @@ pub fn load_translation_static( let key = format!("{key:?}").to_lowercase(); quote! { (#key, #value) } }); - let replaces = kwarg_dynamic_replaces(format_kwargs); quote! {{ if valid_lang { @@ -186,7 +182,7 @@ pub fn load_translation_dynamic( )); }; - let replaces = kwarg_dynamic_replaces(format_kwargs); + let replaces = kwarg_dynamic_replaces(&format_kwargs); Ok(match static_lang { Some(language) => { From 953d5f939a9fa1391bd0100b2df376f1655b6696 Mon Sep 17 00:00:00 2001 From: stifskere Date: Thu, 27 Mar 2025 19:57:10 +0100 Subject: [PATCH 040/228] feat: pre process compile warning, wait for addition in the std. --- translatable/tests/test.rs | 3 ++- translatable_proc/src/macros.rs | 19 ++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/translatable/tests/test.rs b/translatable/tests/test.rs index fca37d3..e8a3856 100644 --- a/translatable/tests/test.rs +++ b/translatable/tests/test.rs @@ -17,7 +17,8 @@ fn language_static_path_dynamic() { #[test] fn language_dynamic_path_static() { let language = "es"; - let result = translation!(language, static common::greeting, name = "john"); + let name = "john"; + let result = translation!(language, static common::greeting, name = name); assert!(result.unwrap() == "Β‘Hola john!".to_string()) } diff --git a/translatable_proc/src/macros.rs b/translatable_proc/src/macros.rs index 1a4267d..8e0ed62 100644 --- a/translatable_proc/src/macros.rs +++ b/translatable_proc/src/macros.rs @@ -82,7 +82,24 @@ impl Parse for RawMacroArgs { if lookahead.peek(Ident) { let key: Ident = input.parse()?; let eq_token: Token![=] = input.parse().unwrap_or(Token![=](key.span())); - let value: Expr = input.parse().unwrap_or(parse_quote!(#key)); + let mut value = input.parse::(); + + if let Ok(value) = &mut value { + let key_string = key.to_string(); + if key_string == value.to_token_stream().to_string() { +// let warning = format!( +// "redundant field initialier, use `{key_string}` instead of `{key_string} = {key_string}`" +// ); + + *value = parse_quote! {{ + // compile_warn!(#warning); + // !!! https://internals.rust-lang.org/t/pre-rfc-add-compile-warning-macro/9370 !!! + #value + }} + } + } + + let value = value.unwrap_or(parse_quote!(#key)); format_kwargs.push(MetaNameValue { path: Path::from(key), From 500edbc3b0854bde6afeed8b9771dee50d7879d9 Mon Sep 17 00:00:00 2001 From: stifskere Date: Thu, 27 Mar 2025 22:42:05 +0100 Subject: [PATCH 041/228] feat: licenses and contributing --- CONTRIBUTING.md | 35 ++++++++++ LICENSE-APACHE | 176 ++++++++++++++++++++++++++++++++++++++++++++++++ LICENSE-MIT | 23 +++++++ README.md | 10 +++ 4 files changed, 244 insertions(+) create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3058e2a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,35 @@ +# Contributing to translatable + +In translatable we welcome any contribution from anyone, bug reports, pull requests, and feedback. +This document serves as guidance if you are thinking of submitting any of the above. + +## Submitting bug reports and feature requests + +To submit a bug report or feature request you can open an issue in this repository `FlakySL/translatable`. + +When reporting a bug or asking for help, please include enough details so that the people helping you +can reproduce the behavior you are seeking. For some tips on how to approach this, read about how to +produce a (Minimal, Complete, and Verifiable example)[https://stackoverflow.com/help/minimal-reproducible-example]. + +When making a feature request, please make it clear what problem you intend to solve with the feature, +any ideas for how translatable could support solving that problem, any possible alternatives, and any +disadvantages. + +Before submitting anything please, check that another issue with your same problem/request does not +already exist, if you want to extend on a problem or have an actual conversation about it, you can +use our discord channel [at Flaky](https://discord.gg/AJWFyps23a). + +It is recommended that you use the issue templates provided in this repository. + +## Running tests and compiling the project + +This project uses [`make`](https://www.gnu.org/software/make/) for everything you may want to run. + +- To run tests you can use the `make test` command, that runs both integration and unit tests. +- To compile the project alone you may use `make compile`, that simply compiles the project in each +target directory. + +## Code of conduct + +In translatable and community we abide by the [Rust code of conduct](https://www.rust-lang.org/policies/code-of-conduct). +For escalation or moderation issues please contact Esteve ([esteve@memw.es](esteve@memw.es)) instead of the Rust moderation team. diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..1b5ec8b --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..31aa793 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index f966d49..f1005fe 100644 --- a/README.md +++ b/README.md @@ -148,3 +148,13 @@ fn main() { } ``` +## License + + +This repository is licensed under either of Apache License, Version 2.0 +or MIT license at your option. + + +Unless you explicitly state any contribution intentionally submitted +for inclusion in translatable by you, as defined in the Apache-2.0 license, shall be +dual licensed as above, without any additional terms or conditions. From cd3f30cb351fb204c653ec5f41aedb0a74af06cc Mon Sep 17 00:00:00 2001 From: stifskere Date: Thu, 27 Mar 2025 22:57:59 +0100 Subject: [PATCH 042/228] feat: prepare Cargo.toml for release --- README-MACROS.md | 10 ++++++++++ README.md | 3 ++- translatable/Cargo.toml | 6 ++++++ translatable_proc/Cargo.toml | 5 +++++ 4 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 README-MACROS.md diff --git a/README-MACROS.md b/README-MACROS.md new file mode 100644 index 0000000..ff4433a --- /dev/null +++ b/README-MACROS.md @@ -0,0 +1,10 @@ +# Translatable macros + +The sole purpose of this crate is exporting macros for the [translatable](https://crates.io/crates/translatable) +package, using this crate without the other one is not supported, and support requests or bug reports +for the use of this will redirect you to use [translatable](https://crates.io/crates/translatable). + +## Licensing + +Licensing for this crate follows the [translatable](https://crates.io/crates/translatable) licensing, +as they are essentially the same package. diff --git a/README.md b/README.md index f1005fe..ff35277 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ A robust internationalization solution for Rust featuring compile-time validatio - [Installation](#installation-) - [Usage](#usage-) - [Example implementation](#example-implementation-) +- [Licensing](#license-) ## Features πŸš€ @@ -148,7 +149,7 @@ fn main() { } ``` -## License +## License πŸ“œ This repository is licensed under either of Apache License, Version 2.0 diff --git a/translatable/Cargo.toml b/translatable/Cargo.toml index f003659..ee35591 100644 --- a/translatable/Cargo.toml +++ b/translatable/Cargo.toml @@ -1,7 +1,13 @@ [package] name = "translatable" +description = "A robust internationalization solution for Rust featuring compile-time validation, ISO 639-1 compliance, and TOML-based translation management. " +repository = "https://github.com/FlakySL/translatable.rs" +license = "MIT OR Apache-2.0" +readme = "../README.md" version = "0.1.0" edition = "2024" +authors = ["Esteve Autet ", "chikof"] +tags = ["i18n", "translations", "idioms", "languages", "internazionalization"] [dependencies] thiserror = "2.0.12" diff --git a/translatable_proc/Cargo.toml b/translatable_proc/Cargo.toml index 0b6867f..d082dd6 100644 --- a/translatable_proc/Cargo.toml +++ b/translatable_proc/Cargo.toml @@ -1,7 +1,12 @@ [package] name = "translatable_proc" +description = "Proc macro crate for the translatable library." +repository = "https://github.com/FlakySL/translatable.rs" +license = "MIT OR Apache-2.0" +readme = "../README-MACROS.md" version = "0.1.0" edition = "2024" +authors = ["Esteve Autet ", "chikof"] [lib] proc-macro = true From 7963c379f243ca9fe0fe40da126e2164ac3f7cf7 Mon Sep 17 00:00:00 2001 From: stifskere Date: Thu, 27 Mar 2025 22:59:18 +0100 Subject: [PATCH 043/228] chore: update chiko email --- translatable/Cargo.toml | 2 +- translatable_proc/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/translatable/Cargo.toml b/translatable/Cargo.toml index ee35591..3224447 100644 --- a/translatable/Cargo.toml +++ b/translatable/Cargo.toml @@ -6,7 +6,7 @@ license = "MIT OR Apache-2.0" readme = "../README.md" version = "0.1.0" edition = "2024" -authors = ["Esteve Autet ", "chikof"] +authors = ["Esteve Autet ", "Chiko "] tags = ["i18n", "translations", "idioms", "languages", "internazionalization"] [dependencies] diff --git a/translatable_proc/Cargo.toml b/translatable_proc/Cargo.toml index d082dd6..6ef2923 100644 --- a/translatable_proc/Cargo.toml +++ b/translatable_proc/Cargo.toml @@ -6,7 +6,7 @@ license = "MIT OR Apache-2.0" readme = "../README-MACROS.md" version = "0.1.0" edition = "2024" -authors = ["Esteve Autet ", "chikof"] +authors = ["Esteve Autet ", "Chiko "] [lib] proc-macro = true From 69c19becf4013e637896b8ce3e2e7351a44a465e Mon Sep 17 00:00:00 2001 From: stifskere Date: Thu, 27 Mar 2025 23:00:35 +0100 Subject: [PATCH 044/228] fix: readme sub --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index ff35277..683602f 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,8 @@ This repository is licensed under either of Apache Lice or MIT license at your option. + Unless you explicitly state any contribution intentionally submitted for inclusion in translatable by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. + From b8516d5f32365a20e3df6b5890b0c4f613e51b56 Mon Sep 17 00:00:00 2001 From: stifskere Date: Thu, 27 Mar 2025 23:01:02 +0100 Subject: [PATCH 045/228] fix: add br in between licenses in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 683602f..692f74c 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ fn main() { This repository is licensed under either of Apache License, Version 2.0 or MIT license at your option. - +
Unless you explicitly state any contribution intentionally submitted for inclusion in translatable by you, as defined in the Apache-2.0 license, shall be From 44f851be2bce73c3a1d2b9d6917fe7ee682d143f Mon Sep 17 00:00:00 2001 From: Chiko Date: Fri, 28 Mar 2025 01:39:11 +0000 Subject: [PATCH 046/228] docs: improve documentation grammar, formatting, and clarity - Fix markdown formatting and grammar in CONTRIBUTING.md - Clarify macros crate dependency requirements - Standardize project name capitalization - Improve readability of contribution guidelines --- CONTRIBUTING.md | 41 ++++++++++++++++++----------------------- Cargo.toml | 2 +- README-MACROS.md | 9 +++------ translatable/Cargo.toml | 8 +++++++- 4 files changed, 29 insertions(+), 31 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3058e2a..d1b2dbd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,35 +1,30 @@ -# Contributing to translatable +# Contributing to Translatable -In translatable we welcome any contribution from anyone, bug reports, pull requests, and feedback. -This document serves as guidance if you are thinking of submitting any of the above. +In Translatable, we welcome contributions from everyone, including bug reports, pull requests, and feedback. This document serves as guidance if you are considering submitting any of the above. -## Submitting bug reports and feature requests +## Submitting Bug Reports and Feature Requests -To submit a bug report or feature request you can open an issue in this repository `FlakySL/translatable`. +To submit a bug report or feature request, you can open an issue in this repository: [`FlakySL/translatable.rs`](https://github.com/FlakySL/translatable.rs). -When reporting a bug or asking for help, please include enough details so that the people helping you -can reproduce the behavior you are seeking. For some tips on how to approach this, read about how to -produce a (Minimal, Complete, and Verifiable example)[https://stackoverflow.com/help/minimal-reproducible-example]. +When reporting a bug or requesting help, please include sufficient details to allow others to reproduce the behavior you're encountering. For guidance on how to approach this, read about [How to Create a Minimal, Reproducible Example](https://stackoverflow.com/help/minimal-reproducible-example). -When making a feature request, please make it clear what problem you intend to solve with the feature, -any ideas for how translatable could support solving that problem, any possible alternatives, and any -disadvantages. +When making a feature request, please clearly explain: +1. The problem you want to solve +2. How Translatable could help address this problem +3. Any potential alternatives +4. Possible disadvantages of your proposal -Before submitting anything please, check that another issue with your same problem/request does not -already exist, if you want to extend on a problem or have an actual conversation about it, you can -use our discord channel [at Flaky](https://discord.gg/AJWFyps23a). +Before submitting, please verify that no existing issue addresses your specific problem/request. If you want to elaborate on a problem or discuss it further, you can use our [Discord channel](https://discord.gg/AJWFyps23a) at Flaky. -It is recommended that you use the issue templates provided in this repository. +We recommend using the issue templates provided in this repository. -## Running tests and compiling the project +## Running Tests and Compiling the Project -This project uses [`make`](https://www.gnu.org/software/make/) for everything you may want to run. +This project uses [`make`](https://www.gnu.org/software/make/) for all common tasks: -- To run tests you can use the `make test` command, that runs both integration and unit tests. -- To compile the project alone you may use `make compile`, that simply compiles the project in each -target directory. +- Run `make test` to execute both integration and unit tests +- Use `make compile` to compile the project in each target directory -## Code of conduct +## Code of Conduct -In translatable and community we abide by the [Rust code of conduct](https://www.rust-lang.org/policies/code-of-conduct). -For escalation or moderation issues please contact Esteve ([esteve@memw.es](esteve@memw.es)) instead of the Rust moderation team. +The Translatable community follows the [Rust Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct). For moderation issues or escalation, please contact Esteve at [esteve@memw.es](mailto:esteve@memw.es) rather than the Rust moderation team. diff --git a/Cargo.toml b/Cargo.toml index d0c38c5..5c69739 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] resolver = "2" -members = [ "translatable", "translatable_proc" ] +members = ["translatable", "translatable_proc"] diff --git a/README-MACROS.md b/README-MACROS.md index ff4433a..dd9aa82 100644 --- a/README-MACROS.md +++ b/README-MACROS.md @@ -1,10 +1,7 @@ -# Translatable macros +# Translatable Macros -The sole purpose of this crate is exporting macros for the [translatable](https://crates.io/crates/translatable) -package, using this crate without the other one is not supported, and support requests or bug reports -for the use of this will redirect you to use [translatable](https://crates.io/crates/translatable). +This crate exists solely to provide macros for the [Translatable](https://crates.io/crates/translatable) crate. Using this crate without the main Translatable crate is **not supported**, and any support requests or bug reports regarding standalone usage will be redirected to the [Translatable](https://crates.io/crates/translatable) crate. ## Licensing -Licensing for this crate follows the [translatable](https://crates.io/crates/translatable) licensing, -as they are essentially the same package. +This crate shares the same licensing terms as [Translatable](https://crates.io/crates/translatable), as these crates are essentially part of the same ecosystem. diff --git a/translatable/Cargo.toml b/translatable/Cargo.toml index 3224447..dd8202a 100644 --- a/translatable/Cargo.toml +++ b/translatable/Cargo.toml @@ -7,7 +7,13 @@ readme = "../README.md" version = "0.1.0" edition = "2024" authors = ["Esteve Autet ", "Chiko "] -tags = ["i18n", "translations", "idioms", "languages", "internazionalization"] +keywords = [ + "i18n", + "translations", + "idioms", + "languages", + "internazionalization", +] [dependencies] thiserror = "2.0.12" From f215a61837f8bb2762ee7c16ce4d68397f247f05 Mon Sep 17 00:00:00 2001 From: Chiko Date: Fri, 28 Mar 2025 02:00:24 +0000 Subject: [PATCH 047/228] feat: add issue templates for feature requests and bug reports --- .github/ISSUE_TEMPLATE/BUG_REPORT.yml | 55 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml | 35 ++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 28 +++++++++++ 3 files changed, 118 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/BUG_REPORT.yml create mode 100644 .github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml new file mode 100644 index 0000000..b11a503 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml @@ -0,0 +1,55 @@ +name: Bug Report +description: Report unexpected behavior or crashes +title: "[BUG] " +labels: ["bug", "triage"] +body: + - type: checkboxes + attributes: + label: Pre-submission Checklist + options: + - label: I've checked existing issues and pull requests + required: true + - label: I've read the [Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct) + required: true + + - type: dropdown + attributes: + label: Component + options: + - Core library + - Macros crate + - Documentation + - CI/CD + validations: + required: true + + - type: input + attributes: + label: Translatable Version + placeholder: 0.x.x + validations: + required: true + + - type: input + attributes: + label: Rust Version + placeholder: Output of `rustc -V` + validations: + required: true + + - type: textarea + attributes: + label: Reproduction Steps + description: Step-by-step instructions to reproduce the issue + validations: + required: true + + - type: textarea + attributes: + label: Expected vs Actual Behavior + description: What you expected to happen vs what actually happened + + - type: textarea + attributes: + label: Additional Context + description: Logs, screenshots, or code samples diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml new file mode 100644 index 0000000..785f5e6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml @@ -0,0 +1,35 @@ +name: Feature Request +description: Suggest an idea for Translatable +title: "[FEATURE] " +labels: ["enhancement", "triage"] +body: + - type: checkboxes + attributes: + label: Pre-submission Checklist + options: + - label: I've checked existing issues and pull requests + required: true + - label: I've read the [Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct) + required: true + + - type: textarea + attributes: + label: Problem Description + description: What problem are you trying to solve? + validations: + required: true + + - type: textarea + attributes: + label: Proposed Solution + description: How should Translatable address this problem? + + - type: textarea + attributes: + label: Alternatives Considered + description: Other ways this could potentially be solved + + - type: textarea + attributes: + label: Additional Context + description: Potential disadvantages, edge cases, or examples diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..8588987 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,28 @@ +blank_issues_enabled: false + +contact_links: + - name: "πŸ’¬ Community Help (Discord)" + url: https://discord.gg/AJWFyps23a + about: | + For general questions, discussion, or brainstorming: + - Get real-time help from maintainers + - Discuss potential features + - Chat with other contributors + *Please check existing issues first!* + + - name: "βš–οΈ Code of Conduct" + url: https://www.rust-lang.org/policies/code-of-conduct + about: | + All community interactions must follow: + - Rust's Code of Conduct + - Project-specific guidelines + *Required reading before participating* + + - name: "🚨 Moderation Contact" + url: mailto:esteve@memw.es + about: | + For urgent moderation issues: + - Code of Conduct violations + - Community safety concerns + - Escalation requests + *Do NOT contact Rust moderation team* From 385b4a43bdd7fdfac7862895a988161b7c2b59ed Mon Sep 17 00:00:00 2001 From: Chiko Date: Fri, 28 Mar 2025 16:20:30 +0000 Subject: [PATCH 048/228] docs: kwargs documentation --- translatable_proc/src/macros.rs | 70 +++++++++---------- .../src/translations/generation.rs | 45 +++++++++++- 2 files changed, 77 insertions(+), 38 deletions(-) diff --git a/translatable_proc/src/macros.rs b/translatable_proc/src/macros.rs index 8e0ed62..2ac902b 100644 --- a/translatable_proc/src/macros.rs +++ b/translatable_proc/src/macros.rs @@ -2,11 +2,14 @@ use std::collections::HashMap; use std::fmt::Display; use proc_macro2::TokenStream; -use quote::{quote, ToTokens}; +use quote::{ToTokens, quote}; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; use syn::token::Static; -use syn::{parse_quote, Expr, ExprLit, ExprPath, Lit, MetaNameValue, Path, Result as SynResult, Token, Ident}; +use syn::{ + Expr, ExprLit, ExprPath, Ident, Lit, MetaNameValue, Path, Result as SynResult, Token, + parse_quote, +}; use crate::translations::generation::{ load_lang_dynamic, load_lang_static, load_translation_dynamic, load_translation_static, @@ -28,9 +31,9 @@ pub struct RawMacroArgs { static_marker: Option, /// Translation path (either static path or dynamic expression) path: Expr, - + /// Optional comma separator for additional arguments _comma2: Option, - + /// Format arguments for string interpolation format_kwargs: Punctuated, } @@ -56,8 +59,8 @@ pub struct TranslationArgs { language: LanguageType, /// Path resolution type path: PathType, - - format_kwargs: HashMap + /// Format arguments for string interpolation + format_kwargs: HashMap, } impl Parse for RawMacroArgs { @@ -67,18 +70,17 @@ impl Parse for RawMacroArgs { let static_marker = input.parse()?; let path = input.parse()?; - let _comma2 = if input.peek(Token![,]) { - Some(input.parse()?) - } else { - None - }; + // Parse optional comma before format arguments + let _comma2 = if input.peek(Token![,]) { Some(input.parse()?) } else { None }; let mut format_kwargs = Punctuated::new(); - if _comma2.is_some() { + // Parse format arguments if comma was present + if _comma2.is_some() { while !input.is_empty() { let lookahead = input.lookahead1(); + // Handle both identifier-based and arbitrary key-value pairs if lookahead.peek(Ident) { let key: Ident = input.parse()?; let eq_token: Token![=] = input.parse().unwrap_or(Token![=](key.span())); @@ -87,10 +89,12 @@ impl Parse for RawMacroArgs { if let Ok(value) = &mut value { let key_string = key.to_string(); if key_string == value.to_token_stream().to_string() { -// let warning = format!( -// "redundant field initialier, use `{key_string}` instead of `{key_string} = {key_string}`" -// ); + // let warning = format!( + // "redundant field initialier, use + // `{key_string}` instead of `{key_string} = {key_string}`" + // ); + // Generate warning for redundant initializer *value = parse_quote! {{ // compile_warn!(#warning); // !!! https://internals.rust-lang.org/t/pre-rfc-add-compile-warning-macro/9370 !!! @@ -101,15 +105,12 @@ impl Parse for RawMacroArgs { let value = value.unwrap_or(parse_quote!(#key)); - format_kwargs.push(MetaNameValue { - path: Path::from(key), - eq_token, - value - }); + format_kwargs.push(MetaNameValue { path: Path::from(key), eq_token, value }); } else { format_kwargs.push(input.parse()?); } + // Continue parsing while commas are present if input.peek(Token![,]) { input.parse::()?; } else { @@ -136,6 +137,7 @@ impl From for TranslationArgs { TranslationArgs { // Extract language specification language: match val.language { + // Handle string literals for compile-time validation Expr::Lit(ExprLit { lit: Lit::Str(lit_str), .. }) => { LanguageType::CompileTimeLiteral(lit_str.value()) }, @@ -165,24 +167,22 @@ impl From for TranslationArgs { path => PathType::OnScopeExpression(quote!(#path)), }, + // Convert format arguments to HashMap with string keys format_kwargs: val .format_kwargs .iter() - .map(|pair| ( - pair - .path - .get_ident() - .map(|i| i.to_string()) - .unwrap_or_else(|| pair - .path - .to_token_stream() - .to_string() - ), - pair - .value - .to_token_stream() - )) - .collect() + .map(|pair| { + ( + // Extract key as identifier or stringified path + pair.path + .get_ident() + .map(|i| i.to_string()) + .unwrap_or_else(|| pair.path.to_token_stream().to_string()), + // Store value as token stream + pair.value.to_token_stream(), + ) + }) + .collect(), } } } diff --git a/translatable_proc/src/translations/generation.rs b/translatable_proc/src/translations/generation.rs index c7c1a14..aaddabb 100644 --- a/translatable_proc/src/translations/generation.rs +++ b/translatable_proc/src/translations/generation.rs @@ -3,12 +3,36 @@ use std::collections::HashMap; use proc_macro2::TokenStream; use quote::quote; use strum::IntoEnumIterator; -use syn::{parse2, Expr}; +use syn::{Expr, parse2}; use super::errors::TranslationError; use crate::data::translations::load_translations; use crate::languages::Iso639a; +/// Generates compile-time string replacement logic for a single format +/// argument. +/// +/// Implements a three-step replacement strategy to safely handle nested +/// templates: +/// 1. Temporarily replace `{{key}}` with `\x01{key}\x01` to protect wrapper +/// braces +/// 2. Replace `{key}` with the provided value +/// 3. Restore original `{key}` syntax from temporary markers +/// +/// # Arguments +/// * `key` - Template placeholder name (without braces) +/// * `value` - Expression to substitute, must implement `std::fmt::Display` +/// +/// # Example +/// For key = "name" and value = `user.first_name`: +/// ```rust +/// let template = "{{name}} is a user"; +/// +/// template +/// .replace("{{name}}", "\x01{name}\x01") +/// .replace("{name}", &format!("{:#}", "Juan")) +/// .replace("\x01{name}\x01", "{name}"); +/// ``` fn kwarg_static_replaces(key: &str, value: &TokenStream) -> TokenStream { quote! { .replace( @@ -26,6 +50,21 @@ fn kwarg_static_replaces(key: &str, value: &TokenStream) -> TokenStream { } } +/// Generates runtime-safe template substitution chain for multiple format +/// arguments. +/// +/// Creates an iterator of chained replacement operations that will be applied +/// sequentially at runtime while preserving nested template syntax. +/// +/// # Arguments +/// * `format_kwargs` - Key/value pairs where: +/// - Key: Template placeholder name +/// - Value: Runtime expression implementing `Display` +/// +/// # Note +/// The replacement order is important to prevent accidental substitution in +/// nested templates. All replacements are wrapped in `Option::map` to handle +/// potential `None` values from translation lookup. fn kwarg_dynamic_replaces(format_kwargs: &HashMap) -> Vec { format_kwargs .iter() @@ -98,7 +137,7 @@ pub fn load_lang_dynamic(lang: TokenStream) -> Result, path: String, - format_kwargs: HashMap + format_kwargs: HashMap, ) -> Result { let translation_object = load_translations()? .iter() @@ -158,7 +197,7 @@ pub fn load_translation_static( pub fn load_translation_dynamic( static_lang: Option, path: TokenStream, - format_kwargs: HashMap + format_kwargs: HashMap, ) -> Result { let nestings = load_translations()? .iter() From d092d75e97bb5728a3270c7555534eddecd05398 Mon Sep 17 00:00:00 2001 From: stifskere Date: Fri, 28 Mar 2025 20:15:11 +0100 Subject: [PATCH 049/228] feat: change templates --- .github/ISSUE_TEMPLATE/BUG_REPORT.yml | 14 ++++---------- .github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml | 4 +++- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml index b11a503..8cdcd53 100644 --- a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml @@ -1,7 +1,7 @@ name: Bug Report description: Report unexpected behavior or crashes title: "[BUG] " -labels: ["bug", "triage"] +labels: ["bug-report", "triage"] body: - type: checkboxes attributes: @@ -11,6 +11,8 @@ body: required: true - label: I've read the [Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct) required: true + - label: Are you using the latest translatable version? + required: true - type: dropdown attributes: @@ -19,21 +21,13 @@ body: - Core library - Macros crate - Documentation - - CI/CD - validations: - required: true - - - type: input - attributes: - label: Translatable Version - placeholder: 0.x.x validations: required: true - type: input attributes: label: Rust Version - placeholder: Output of `rustc -V` + placeholder: Output of `rustc --version` validations: required: true diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml index 785f5e6..687f118 100644 --- a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml +++ b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml @@ -1,7 +1,7 @@ name: Feature Request description: Suggest an idea for Translatable title: "[FEATURE] " -labels: ["enhancement", "triage"] +labels: ["feature-request", "triage"] body: - type: checkboxes attributes: @@ -23,6 +23,8 @@ body: attributes: label: Proposed Solution description: How should Translatable address this problem? + validations: + required: true - type: textarea attributes: From d65b558bf965172bc44a33b032cded453777747b Mon Sep 17 00:00:00 2001 From: stifskere Date: Fri, 28 Mar 2025 22:57:27 +0100 Subject: [PATCH 050/228] feat: add SUPPORT.md --- .github/SUPPORT.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .github/SUPPORT.md diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md new file mode 100644 index 0000000..d195d31 --- /dev/null +++ b/.github/SUPPORT.md @@ -0,0 +1,3 @@ +# Useful resources + +- [Discord server](https://discord.gg/AJWFyps23a) From 5979f7677b83ecb420821555483297b2dd37bf4a Mon Sep 17 00:00:00 2001 From: stifskere Date: Fri, 28 Mar 2025 23:03:47 +0100 Subject: [PATCH 051/228] feat: SECURITY.md --- SECURITY.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..7b1087e --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,9 @@ + +# Security Vulnerabilities + +This library does not directly interact with networking, or anything another person +might be able to do to access the service using this library. + +To update the dependencies and solve vulnerability issues within we use dependabot, +which we believe a safe enough alternative to update all the dependencies +in the project. From 824f2a485fe415b45ab8c9675e0f5c5d48b578a3 Mon Sep 17 00:00:00 2001 From: stifskere Date: Fri, 28 Mar 2025 23:10:34 +0100 Subject: [PATCH 052/228] feat: governance --- GOVERNANCE.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 GOVERNANCE.md diff --git a/GOVERNANCE.md b/GOVERNANCE.md new file mode 100644 index 0000000..b0a97e6 --- /dev/null +++ b/GOVERNANCE.md @@ -0,0 +1,10 @@ + +# Governance and moderation + +This project is mainly maintained by the authors listed in both `translatable/Cargo.toml` and `translatable_proc/Cargo.toml`. + +- Esteve Autet +- Chiko + +There is no hierarchy established (yet) but this might be subject to change soon. For any inquiries you can +contact any of the emails listed above. From c6fa52c660a1ea24ac5c81a786d0f7aeafbe86c4 Mon Sep 17 00:00:00 2001 From: stifskere Date: Fri, 28 Mar 2025 23:13:34 +0100 Subject: [PATCH 053/228] feat: code of conduct --- CODE_OF_CONDUCT.md | 75 ++++++++++++++++++++++++++++++++++++++++++++++ GOVERNANCE.md | 4 +-- 2 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..18920f3 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,75 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +education, socio-economic status, nationality, personal appearance, race, +religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at `esteve@memw.es` or `chiko@envs.net`. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + + diff --git a/GOVERNANCE.md b/GOVERNANCE.md index b0a97e6..e17f65b 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -3,8 +3,8 @@ This project is mainly maintained by the authors listed in both `translatable/Cargo.toml` and `translatable_proc/Cargo.toml`. -- Esteve Autet -- Chiko +- Esteve Autet `esteve@memw.es` +- Chiko `chiko@envs.net` There is no hierarchy established (yet) but this might be subject to change soon. For any inquiries you can contact any of the emails listed above. From d53032da5810538a72f49648ccdeb158353321ef Mon Sep 17 00:00:00 2001 From: stifskere Date: Fri, 28 Mar 2025 23:16:44 +0100 Subject: [PATCH 054/228] fix: change whose code of conduct this is xD --- CODE_OF_CONDUCT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 18920f3..673d613 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,4 +1,4 @@ -# Contributor Covenant Code of Conduct +# Translatable Code of Conduct ## Our Pledge From 30eefaeeac032c11a7335f0d95d129628f889d41 Mon Sep 17 00:00:00 2001 From: Esteve Autet Alexe Date: Fri, 28 Mar 2025 23:18:40 +0100 Subject: [PATCH 055/228] fix: random capital letter Co-authored-by: Chiko <53100578+chikof@users.noreply.github.com> Signed-off-by: Esteve Autet Alexe --- GOVERNANCE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GOVERNANCE.md b/GOVERNANCE.md index e17f65b..0a80625 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -1,5 +1,5 @@ -# Governance and moderation +# Governance and Moderation This project is mainly maintained by the authors listed in both `translatable/Cargo.toml` and `translatable_proc/Cargo.toml`. From 8804fed4cfa373deac0f860f07a3e1aa303d2327 Mon Sep 17 00:00:00 2001 From: Chiko Date: Sat, 29 Mar 2025 19:41:56 +0000 Subject: [PATCH 056/228] test: add dynamic and static tests for translations functionality --- translatable/tests/dynamic.rs | 12 +++++ .../tests/dynamic/unknown_translations.rs | 0 translatable/tests/dynamic/valid_syntax.rs | 21 +++++++++ translatable/tests/static.rs | 11 +++++ translatable/tests/static/invalid_args.rs | 9 ++++ translatable/tests/static/invalid_args.stderr | 13 ++++++ translatable/tests/static/invalid_syntax.rs | 12 +++++ .../tests/static/invalid_syntax.stderr | 23 ++++++++++ translatable/tests/static/missing_args.rs | 21 +++++++++ translatable/tests/static/missing_args.stderr | 45 +++++++++++++++++++ translatable/tests/test.rs | 32 ------------- .../translations}/test.toml | 1 - 12 files changed, 167 insertions(+), 33 deletions(-) create mode 100644 translatable/tests/dynamic.rs create mode 100644 translatable/tests/dynamic/unknown_translations.rs create mode 100644 translatable/tests/dynamic/valid_syntax.rs create mode 100644 translatable/tests/static.rs create mode 100644 translatable/tests/static/invalid_args.rs create mode 100644 translatable/tests/static/invalid_args.stderr create mode 100644 translatable/tests/static/invalid_syntax.rs create mode 100644 translatable/tests/static/invalid_syntax.stderr create mode 100644 translatable/tests/static/missing_args.rs create mode 100644 translatable/tests/static/missing_args.stderr delete mode 100644 translatable/tests/test.rs rename {translations => translatable/translations}/test.toml (99%) diff --git a/translatable/tests/dynamic.rs b/translatable/tests/dynamic.rs new file mode 100644 index 0000000..e908511 --- /dev/null +++ b/translatable/tests/dynamic.rs @@ -0,0 +1,12 @@ +use trybuild::TestCases; + +#[test] +fn dynamic_tests() { + let t = TestCases::new(); + + println!("{:?}", std::process::Command::new("pwd").output().unwrap()); + + // Compile-time tests for + t.pass("tests/dynamic/valid_syntax.rs"); + +} diff --git a/translatable/tests/dynamic/unknown_translations.rs b/translatable/tests/dynamic/unknown_translations.rs new file mode 100644 index 0000000..e69de29 diff --git a/translatable/tests/dynamic/valid_syntax.rs b/translatable/tests/dynamic/valid_syntax.rs new file mode 100644 index 0000000..1d7fe14 --- /dev/null +++ b/translatable/tests/dynamic/valid_syntax.rs @@ -0,0 +1,21 @@ +use translatable::translation; + +fn main() { + + println!("{:?}", std::process::Command::new("pwd").output().unwrap()); + + let a_lang = "es"; + let a_path = "common.greeting"; + let a_name = "john"; + + // translation!(a_lang, a_path, name = a_name); + // !! https://github.com/dtolnay/trybuild/issues/202 + assert!(translation!(a_lang, a_path, name = a_name).unwrap() == "Β‘Hola john!".into()); + + let b_lang = "en"; + let b_path = "common.greeting"; + let b_name = "Marie"; + + translation!(b_lang, b_path, name = b_name); + +} diff --git a/translatable/tests/static.rs b/translatable/tests/static.rs new file mode 100644 index 0000000..eaeb131 --- /dev/null +++ b/translatable/tests/static.rs @@ -0,0 +1,11 @@ +use trybuild::TestCases; + +#[test] +fn static_tests() { + let t = TestCases::new(); + + // Compile-time tests for invalid translations + t.compile_fail("tests/static/invalid_args.rs"); + t.compile_fail("tests/static/invalid_syntax.rs"); + t.compile_fail("tests/static/missing_args.rs"); +} diff --git a/translatable/tests/static/invalid_args.rs b/translatable/tests/static/invalid_args.rs new file mode 100644 index 0000000..1b21e43 --- /dev/null +++ b/translatable/tests/static/invalid_args.rs @@ -0,0 +1,9 @@ +use translatable::translation; + +fn main() { + // Missing required arguments + translation!("es"); + + // Invalid argument syntax + translation!("es", "path", 42 = "value"); +} diff --git a/translatable/tests/static/invalid_args.stderr b/translatable/tests/static/invalid_args.stderr new file mode 100644 index 0000000..0ed5468 --- /dev/null +++ b/translatable/tests/static/invalid_args.stderr @@ -0,0 +1,13 @@ +error: expected `,` + --> tests/static/invalid_args.rs:5:5 + | +5 | translation!("es"); + | ^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: expected identifier + --> tests/static/invalid_args.rs:8:32 + | +8 | translation!("es", "path", 42 = "value"); + | ^^ diff --git a/translatable/tests/static/invalid_syntax.rs b/translatable/tests/static/invalid_syntax.rs new file mode 100644 index 0000000..ea3483d --- /dev/null +++ b/translatable/tests/static/invalid_syntax.rs @@ -0,0 +1,12 @@ +use translatable::translation; + +fn main() { + // Missing arguments + let _ = translation!("es"); + + // Invalid literal type + let _ = translation!(42, static common::greeting); + + // Malformed static path + let _ = translation!("es", static invalid::path); +} diff --git a/translatable/tests/static/invalid_syntax.stderr b/translatable/tests/static/invalid_syntax.stderr new file mode 100644 index 0000000..487ae64 --- /dev/null +++ b/translatable/tests/static/invalid_syntax.stderr @@ -0,0 +1,23 @@ +error: expected `,` + --> tests/static/invalid_syntax.rs:5:13 + | +5 | let _ = translation!("es"); + | ^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: An IO Error occurred: No such file or directory (os error 2) + --> tests/static/invalid_syntax.rs:8:13 + | +8 | let _ = translation!(42, static common::greeting); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: An IO Error occurred: No such file or directory (os error 2) + --> tests/static/invalid_syntax.rs:11:13 + | +11 | let _ = translation!("es", static invalid::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/static/missing_args.rs b/translatable/tests/static/missing_args.rs new file mode 100644 index 0000000..aca2bb3 --- /dev/null +++ b/translatable/tests/static/missing_args.rs @@ -0,0 +1,21 @@ +use translatable::translation; + +fn main() { + // Test completely empty invocation + let _ = translation!(); + + // Missing path argument + let _ = translation!("es"); + + // Missing language argument + let _ = translation!(static common::greeting); + + // Missing both language and path + let _ = translation!(); + + // Missing interpolation arguments + let _ = translation!("es", static common::greeting); + + // Partial arguments with named params + let _ = translation!("es", static common::greeting, name); +} diff --git a/translatable/tests/static/missing_args.stderr b/translatable/tests/static/missing_args.stderr new file mode 100644 index 0000000..c6b6b49 --- /dev/null +++ b/translatable/tests/static/missing_args.stderr @@ -0,0 +1,45 @@ +error: unexpected end of input, expected an expression + --> tests/static/missing_args.rs:5:13 + | +5 | let _ = translation!(); + | ^^^^^^^^^^^^^^ + | + = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: expected `,` + --> tests/static/missing_args.rs:8:13 + | +8 | let _ = translation!("es"); + | ^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: expected `|` + --> tests/static/missing_args.rs:11:33 + | +11 | let _ = translation!(static common::greeting); + | ^^^^^^ + +error: unexpected end of input, expected an expression + --> tests/static/missing_args.rs:14:13 + | +14 | let _ = translation!(); + | ^^^^^^^^^^^^^^ + | + = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: An IO Error occurred: No such file or directory (os error 2) + --> tests/static/missing_args.rs:17:13 + | +17 | let _ = translation!("es", static common::greeting); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: An IO Error occurred: No such file or directory (os error 2) + --> tests/static/missing_args.rs:20:13 + | +20 | let _ = translation!("es", static common::greeting, name); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/translatable/tests/test.rs b/translatable/tests/test.rs deleted file mode 100644 index e8a3856..0000000 --- a/translatable/tests/test.rs +++ /dev/null @@ -1,32 +0,0 @@ -use translatable::translation; - -#[test] -fn both_static() { - let result = translation!("es", static common::greeting, name = "john"); - - assert!(result == "Β‘Hola john!") -} - -#[test] -fn language_static_path_dynamic() { - let result = translation!("es", "common.greeting", name = "john"); - - assert!(result.unwrap() == "Β‘Hola john!".to_string()) -} - -#[test] -fn language_dynamic_path_static() { - let language = "es"; - let name = "john"; - let result = translation!(language, static common::greeting, name = name); - - assert!(result.unwrap() == "Β‘Hola john!".to_string()) -} - -#[test] -fn both_dynamic() { - let language = "es"; - let result = translation!(language, "common.greeting", lol = 10, name = "john"); - - assert!(result.unwrap() == "Β‘Hola john!".to_string()) -} diff --git a/translations/test.toml b/translatable/translations/test.toml similarity index 99% rename from translations/test.toml rename to translatable/translations/test.toml index 6aecb64..9f223a2 100644 --- a/translations/test.toml +++ b/translatable/translations/test.toml @@ -1,4 +1,3 @@ - [welcome_message] en = "Welcome to our app!" es = "Β‘Bienvenido a nuestra aplicaciΓ³n!" From 58a26c23f42713f801c4c68f2ddcf5c5adce18f0 Mon Sep 17 00:00:00 2001 From: Chiko Date: Sat, 29 Mar 2025 21:53:58 +0000 Subject: [PATCH 057/228] feat(config): add environment variable support for configuration overrides --- translatable_proc/src/data/config.rs | 78 ++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 10 deletions(-) diff --git a/translatable_proc/src/data/config.rs b/translatable_proc/src/data/config.rs index 3d1fc8c..491982f 100644 --- a/translatable_proc/src/data/config.rs +++ b/translatable_proc/src/data/config.rs @@ -3,6 +3,7 @@ //! This module provides functionality to load and manage configuration //! settings for localization/translation workflows from a TOML file. +use std::env::var; use std::fs::read_to_string; use std::io::Error as IoError; use std::sync::OnceLock; @@ -27,6 +28,10 @@ pub enum ConfigError { .unwrap_or_else(|| "".into()) )] ParseToml(#[from] TomlError), + + /// Invalid environment variable value for configuration options + #[error("Invalid value '{1}' for environment variable '{0}'")] + InvalidEnvVarValue(String, String), } /// Wrapper type for locales directory path with validation @@ -41,7 +46,7 @@ impl Default for LocalesPath { } /// File search order strategy -#[derive(Deserialize, Default)] +#[derive(Deserialize, Default, Clone, Copy)] #[serde(rename_all = "snake_case")] pub enum SeekMode { /// Alphabetical order (default) @@ -53,7 +58,7 @@ pub enum SeekMode { } /// Translation conflict resolution strategy -#[derive(Deserialize, Default)] +#[derive(Deserialize, Default, Clone, Copy)] #[serde(rename_all = "snake_case")] pub enum TranslationOverlap { /// Last found translation overwrites previous ones (default) @@ -97,13 +102,29 @@ impl TranslatableConfig { } /// Get current seek mode strategy - pub fn seek_mode(&self) -> &SeekMode { - &self.seek_mode + pub fn seek_mode(&self) -> SeekMode { + self.seek_mode } /// Get current overlap resolution strategy - pub fn overlap(&self) -> &TranslationOverlap { - &self.overlap + pub fn overlap(&self) -> TranslationOverlap { + self.overlap + } + + /// Set the locales path from a string (e.g., from environment variable) + fn set_path(&mut self, path: String) { + self.path = LocalesPath(path); + } + + /// Update seek mode from string value (for environment variable parsing) + fn set_seek_mode(&mut self, mode: SeekMode) { + self.seek_mode = mode; + } + + /// Update overlap strategy from string value (for environment variable + /// parsing) + fn set_overlap(&mut self, strategy: TranslationOverlap) { + self.overlap = strategy; } } @@ -116,20 +137,57 @@ static TRANSLATABLE_CONFIG: OnceLock = OnceLock::new(); /// - Uses `OnceLock` for thread-safe singleton initialization /// - Missing config file is not considered an error /// - Config file must be named `translatable.toml` in root directory +/// - Environment variables take precedence over TOML configuration +/// - Supported environment variables: +/// - `LOCALES_PATH`: Overrides translation directory path +/// - `SEEK_MODE`: Sets file processing order ("alphabetical" or +/// "unalphabetical") +/// - `TRANSLATION_OVERLAP`: Sets conflict strategy ("overwrite" or "ignore") /// /// # Panics /// Will not panic but returns ConfigError for: /// - Malformed TOML syntax /// - Filesystem permission issues +/// - Invalid environment variable values pub fn load_config() -> Result<&'static TranslatableConfig, ConfigError> { if let Some(config) = TRANSLATABLE_CONFIG.get() { return Ok(config); } - let config: TranslatableConfig = - toml_from_str(read_to_string("./translatable.toml") - .unwrap_or("".into()) // if no config file is found use defaults. - .as_str())?; + // Load base configuration from TOML file + let toml_content = read_to_string("./translatable.toml").unwrap_or_default(); + let mut config: TranslatableConfig = toml_from_str(&toml_content)?; + + // Environment variable overrides + // -------------------------------------------------- + // LOCALES_PATH: Highest precedence for translation directory + if let Ok(env_path) = var("LOCALES_PATH") { + config.set_path(env_path); + } + + // SEEK_MODE: Control file processing order + if let Ok(env_seek) = var("SEEK_MODE") { + config.set_seek_mode(match env_seek.to_lowercase().as_str() { + "alphabetical" => SeekMode::Alphabetical, + "unalphabetical" => SeekMode::Unalphabetical, + _ => return Err(ConfigError::InvalidEnvVarValue("SEEK_MODE".into(), env_seek)), + }); + } + + // TRANSLATION_OVERLAP: Manage translation conflicts + if let Ok(env_overlap) = var("TRANSLATION_OVERLAP") { + config.set_overlap(match env_overlap.to_lowercase().as_str() { + "overwrite" => TranslationOverlap::Overwrite, + "ignore" => TranslationOverlap::Ignore, + _ => { + return Err(ConfigError::InvalidEnvVarValue( + "TRANSLATION_OVERLAP".into(), + env_overlap, + )); + }, + }); + } + // Freeze configuration in global cache Ok(TRANSLATABLE_CONFIG.get_or_init(|| config)) } From f97ddbb50fa8dbb410eec71b473e36dfd2359ac6 Mon Sep 17 00:00:00 2001 From: stifskere Date: Mon, 31 Mar 2025 17:27:11 +0200 Subject: [PATCH 058/228] chore: half refactor --- Cargo.lock | 7 ++ Cargo.toml | 2 +- translatable/src/error.rs | 33 +++++++++ translatable/src/lib.rs | 74 +------------------ translatable_proc/Cargo.toml | 2 +- translatable_proc/src/data/translations.rs | 1 - translatable_proc/src/lib.rs | 1 - translatable_shared/Cargo.toml | 12 +++ translatable_shared/src/errors.rs | 0 .../src/languages.rs | 6 +- translatable_shared/src/lib.rs | 3 + translatable_shared/src/nesting_type.rs | 37 ++++++++++ 12 files changed, 101 insertions(+), 77 deletions(-) create mode 100644 translatable/src/error.rs create mode 100644 translatable_shared/Cargo.toml create mode 100644 translatable_shared/src/errors.rs rename {translatable_proc => translatable_shared}/src/languages.rs (99%) create mode 100644 translatable_shared/src/lib.rs create mode 100644 translatable_shared/src/nesting_type.rs diff --git a/Cargo.lock b/Cargo.lock index bc813ff..8794581 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -243,6 +243,13 @@ dependencies = [ "toml", ] +[[package]] +name = "translatable_shared" +version = "0.1.0" +dependencies = [ + "strum", +] + [[package]] name = "trybuild" version = "1.0.104" diff --git a/Cargo.toml b/Cargo.toml index 5c69739..bfc9e5b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] resolver = "2" -members = ["translatable", "translatable_proc"] +members = ["translatable", "translatable_proc", "translatable_shared"] diff --git a/translatable/src/error.rs b/translatable/src/error.rs new file mode 100644 index 0000000..f2d6b25 --- /dev/null +++ b/translatable/src/error.rs @@ -0,0 +1,33 @@ +use thiserror::Error; + +/// Error type for translation resolution failures +/// +/// Returned by the translation macro when dynamic resolution fails. +/// For static resolution failures, errors are reported at compile time. +#[derive(Error, Debug)] +pub enum Error { + /// Invalid ISO 639-1 language code provided + #[error("The language '{0}' is invalid.")] + InvalidLanguage(String), + + /// Translation exists but not available for specified language + #[error("The langauge '{0}' is not available for the path '{1}'")] + LanguageNotAvailable(String, String), + + /// Requested translation path doesn't exist in any translation files + #[error("The path '{0}' was not found in any of the translations files.")] + PathNotFound(String), +} + +impl Error { + /// Returns formatted error message as a String + /// + /// Useful for error reporting and logging. Marked `#[cold]` to hint to the + /// compiler that this path is unlikely to be taken (optimization for error + /// paths). + #[inline] + #[cold] + pub fn cause(&self) -> String { + format!("{self:#}") + } +} diff --git a/translatable/src/lib.rs b/translatable/src/lib.rs index 1f93e2e..72f3d07 100644 --- a/translatable/src/lib.rs +++ b/translatable/src/lib.rs @@ -1,72 +1,6 @@ -use thiserror::Error; +mod error; + +// Export the private error module +pub use error::Error; /// Re-export the procedural macro for crate users pub use translatable_proc::translation; - -/// Error type for translation resolution failures -/// -/// Returned by the translation macro when dynamic resolution fails. -/// For static resolution failures, errors are reported at compile time. -#[derive(Error, Debug)] -pub enum Error { - /// Invalid ISO 639-1 language code provided - #[error("The language '{0}' is invalid.")] - InvalidLanguage(String), - - /// Translation exists but not available for specified language - #[error("The langauge '{0}' is not available for the path '{1}'")] - LanguageNotAvailable(String, String), - - /// Requested translation path doesn't exist in any translation files - #[error("The path '{0}' was not found in any of the translations files.")] - PathNotFound(String), -} - -impl Error { - /// Returns formatted error message as a String - /// - /// Useful for error reporting and logging. Marked `#[cold]` to hint to the - /// compiler that this path is unlikely to be taken (optimization for error - /// paths). - #[inline] - #[cold] - pub fn cause(&self) -> String { - format!("{self:#}") - } -} - -/// Internal implementation details for translation resolution -#[doc(hidden)] -pub mod internal { - use std::collections::HashMap; - - /// Represents nested translation structures - #[doc(hidden)] - pub enum NestingType { - /// Intermediate node containing nested translation objects - Object(HashMap), - /// Leaf node containing actual translations for different languages - Translation(HashMap), - } - - impl NestingType { - /// Resolves a translation path through nested structures - /// - /// # Arguments - /// * `path` - Slice of path segments to resolve - /// - /// # Returns - /// - `Some(&HashMap)` if path resolves to translations - /// - `None` if path is invalid - #[doc(hidden)] - pub fn get_path(&self, path: Vec<&str>) -> Option<&HashMap> { - match self { - Self::Object(nested) => { - let (first, rest) = path.split_first()?; - nested.get(*first)?.get_path(rest.to_vec()) - }, - - Self::Translation(translation) => path.is_empty().then_some(translation), - } - } - } -} diff --git a/translatable_proc/Cargo.toml b/translatable_proc/Cargo.toml index 6ef2923..9e86ed3 100644 --- a/translatable_proc/Cargo.toml +++ b/translatable_proc/Cargo.toml @@ -15,7 +15,7 @@ proc-macro = true proc-macro2 = "1.0.94" quote = "1.0.38" serde = { version = "1.0.218", features = ["derive"] } -strum = { version = "0.27.1", features = ["derive"] } +strum = "0.27.1" syn = { version = "2.0.98", features = ["full"] } thiserror = "2.0.11" toml = "0.8.20" diff --git a/translatable_proc/src/data/translations.rs b/translatable_proc/src/data/translations.rs index 4adf4a9..6017bed 100644 --- a/translatable_proc/src/data/translations.rs +++ b/translatable_proc/src/data/translations.rs @@ -10,7 +10,6 @@ use thiserror::Error; use toml::{Table, Value}; use super::config::{SeekMode, TranslationOverlap, load_config}; -use crate::languages::Iso639a; use crate::translations::errors::TranslationError; /// Errors occurring during TOML-to-translation structure transformation diff --git a/translatable_proc/src/lib.rs b/translatable_proc/src/lib.rs index 1989353..2410d5d 100644 --- a/translatable_proc/src/lib.rs +++ b/translatable_proc/src/lib.rs @@ -12,7 +12,6 @@ use proc_macro::TokenStream; use syn::parse_macro_input; mod data; -mod languages; mod macros; mod translations; diff --git a/translatable_shared/Cargo.toml b/translatable_shared/Cargo.toml new file mode 100644 index 0000000..ca97b2b --- /dev/null +++ b/translatable_shared/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "translatable_shared" +description = "Shared dependencies crate for translatable." +repository = "https://github.com/FlakySL/translatable.rs" +license = "MIT OR Apache-2.0" +readme = "../README-MACROS.md" +version = "0.1.0" +edition = "2024" +authors = ["Esteve Autet ", "Chiko "] + +[dependencies] +strum = { version = "0.27.1", features = ["derive", "strum_macros"] } diff --git a/translatable_shared/src/errors.rs b/translatable_shared/src/errors.rs new file mode 100644 index 0000000..e69de29 diff --git a/translatable_proc/src/languages.rs b/translatable_shared/src/languages.rs similarity index 99% rename from translatable_proc/src/languages.rs rename to translatable_shared/src/languages.rs index 1deef53..df39210 100644 --- a/translatable_proc/src/languages.rs +++ b/translatable_shared/src/languages.rs @@ -8,7 +8,7 @@ use strum::{Display, EnumIter, EnumString, IntoEnumIterator}; /// - Complete ISO 639-1 coverage #[derive(Debug, Clone, EnumIter, Display, EnumString, Eq, Hash, PartialEq)] #[strum(ascii_case_insensitive)] -pub enum Iso639a { +pub enum Language { #[strum(serialize = "Abkhazian", serialize = "ab")] AB, #[strum(serialize = "Afar", serialize = "aa")] @@ -395,7 +395,7 @@ impl Similarities { } } -impl Iso639a { +impl Language { /// This method returns a list of similar languages to the provided one. pub fn get_similarities(lang: &str, max_amount: usize) -> Similarities { let all_similarities = Self::iter() @@ -419,7 +419,7 @@ impl Iso639a { } } -impl PartialEq for Iso639a { +impl PartialEq for Language { fn eq(&self, other: &String) -> bool { format!("{self:?}").to_lowercase() == other.to_lowercase() } diff --git a/translatable_shared/src/lib.rs b/translatable_shared/src/lib.rs new file mode 100644 index 0000000..21410ee --- /dev/null +++ b/translatable_shared/src/lib.rs @@ -0,0 +1,3 @@ + +pub mod languages; +pub mod nesting_type; diff --git a/translatable_shared/src/nesting_type.rs b/translatable_shared/src/nesting_type.rs new file mode 100644 index 0000000..f123eb3 --- /dev/null +++ b/translatable_shared/src/nesting_type.rs @@ -0,0 +1,37 @@ +use std::collections::HashMap; + +// Everything in this module should be +// marked with #[doc(hidden)] as we don't +// want the LSP to be notifying it's existence. + +/// Represents nested translation structures +#[doc(hidden)] +pub enum NestingType { + /// Intermediate node containing nested translation objects + Object(HashMap), + /// Leaf node containing actual translations for different languages + Translation(HashMap), +} + +impl NestingType { + /// Resolves a translation path through nested structures + /// + /// # Arguments + /// * `path` - Slice of path segments to resolve + /// + /// # Returns + /// - `Some(&HashMap)` if path resolves to translations + /// - `None` if path is invalid + #[doc(hidden)] + pub fn get_path(&self, path: Vec<&str>) -> Option<&HashMap> { + match self { + Self::Object(nested) => { + let (first, rest) = path.split_first()?; + nested.get(*first)?.get_path(rest.to_vec()) + }, + + Self::Translation(translation) => path.is_empty().then_some(translation), + } + } +} + From 2baf3270b3145e406843805f96131890ba4b6197 Mon Sep 17 00:00:00 2001 From: stifskere Date: Mon, 31 Mar 2025 18:08:57 +0200 Subject: [PATCH 059/228] feat: end environment support for config --- translatable_proc/src/data/config.rs | 124 +++++++++++---------------- 1 file changed, 50 insertions(+), 74 deletions(-) diff --git a/translatable_proc/src/data/config.rs b/translatable_proc/src/data/config.rs index 491982f..1505ba3 100644 --- a/translatable_proc/src/data/config.rs +++ b/translatable_proc/src/data/config.rs @@ -11,7 +11,8 @@ use std::sync::OnceLock; use serde::Deserialize; use thiserror::Error; use toml::de::Error as TomlError; -use toml::from_str as toml_from_str; +use toml::{from_str as toml_from_str, Table}; +use strum::EnumString; /// Errors that can occur during configuration loading #[derive(Error, Debug)] @@ -30,24 +31,12 @@ pub enum ConfigError { ParseToml(#[from] TomlError), /// Invalid environment variable value for configuration options - #[error("Invalid value '{1}' for environment variable '{0}'")] - InvalidEnvVarValue(String, String), -} - -/// Wrapper type for locales directory path with validation -#[derive(Deserialize)] -pub struct LocalesPath(String); - -impl Default for LocalesPath { - /// Default path to translations directory - fn default() -> Self { - LocalesPath("./translations".into()) - } + #[error("Couldn't parse configuration entry '{1}' for '{0}'")] + InvalidValue(String, String), } /// File search order strategy -#[derive(Deserialize, Default, Clone, Copy)] -#[serde(rename_all = "snake_case")] +#[derive(Default, Clone, Copy, EnumString)] pub enum SeekMode { /// Alphabetical order (default) #[default] @@ -58,8 +47,7 @@ pub enum SeekMode { } /// Translation conflict resolution strategy -#[derive(Deserialize, Default, Clone, Copy)] -#[serde(rename_all = "snake_case")] +#[derive(Default, Clone, Copy, EnumString)] pub enum TranslationOverlap { /// Last found translation overwrites previous ones (default) #[default] @@ -70,35 +58,31 @@ pub enum TranslationOverlap { } /// Main configuration structure for translation system -#[derive(Deserialize)] -pub struct TranslatableConfig { +pub struct MacroConfig { /// Path to directory containing translation files /// /// # Example /// ```toml /// path = "./locales" /// ``` - #[serde(default)] - path: LocalesPath, + path: String, /// File processing order strategy /// /// Default: alphabetical file processing - #[serde(default)] seek_mode: SeekMode, /// Translation conflict resolution strategy /// /// Determines behavior when multiple files contain the same translation /// path - #[serde(default)] overlap: TranslationOverlap, } -impl TranslatableConfig { +impl MacroConfig { /// Get reference to configured locales path pub fn path(&self) -> &str { - &self.path.0 + &self.path } /// Get current seek mode strategy @@ -110,26 +94,10 @@ impl TranslatableConfig { pub fn overlap(&self) -> TranslationOverlap { self.overlap } - - /// Set the locales path from a string (e.g., from environment variable) - fn set_path(&mut self, path: String) { - self.path = LocalesPath(path); - } - - /// Update seek mode from string value (for environment variable parsing) - fn set_seek_mode(&mut self, mode: SeekMode) { - self.seek_mode = mode; - } - - /// Update overlap strategy from string value (for environment variable - /// parsing) - fn set_overlap(&mut self, strategy: TranslationOverlap) { - self.overlap = strategy; - } } /// Global configuration cache -static TRANSLATABLE_CONFIG: OnceLock = OnceLock::new(); +static TRANSLATABLE_CONFIG: OnceLock = OnceLock::new(); /// Load configuration from file or use defaults /// @@ -149,44 +117,52 @@ static TRANSLATABLE_CONFIG: OnceLock = OnceLock::new(); /// - Malformed TOML syntax /// - Filesystem permission issues /// - Invalid environment variable values -pub fn load_config() -> Result<&'static TranslatableConfig, ConfigError> { +pub fn load_config() -> Result<&'static MacroConfig, ConfigError> { if let Some(config) = TRANSLATABLE_CONFIG.get() { return Ok(config); } // Load base configuration from TOML file - let toml_content = read_to_string("./translatable.toml").unwrap_or_default(); - let mut config: TranslatableConfig = toml_from_str(&toml_content)?; - - // Environment variable overrides - // -------------------------------------------------- - // LOCALES_PATH: Highest precedence for translation directory - if let Ok(env_path) = var("LOCALES_PATH") { - config.set_path(env_path); + 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) + } + }}; } - // SEEK_MODE: Control file processing order - if let Ok(env_seek) = var("SEEK_MODE") { - config.set_seek_mode(match env_seek.to_lowercase().as_str() { - "alphabetical" => SeekMode::Alphabetical, - "unalphabetical" => SeekMode::Unalphabetical, - _ => return Err(ConfigError::InvalidEnvVarValue("SEEK_MODE".into(), env_seek)), - }); - } - - // TRANSLATION_OVERLAP: Manage translation conflicts - if let Ok(env_overlap) = var("TRANSLATION_OVERLAP") { - config.set_overlap(match env_overlap.to_lowercase().as_str() { - "overwrite" => TranslationOverlap::Overwrite, - "ignore" => TranslationOverlap::Ignore, - _ => { - return Err(ConfigError::InvalidEnvVarValue( - "TRANSLATION_OVERLAP".into(), - env_overlap, - )); - }, - }); - } + let config = MacroConfig { + path: config_value!("TRANSLATABLE_PATH", "path", "./translatable.toml"), + overlap: config_value!(parse("TRANSLATABLE_OVERLAP", "overlap", TranslationOverlap::Ignore))?, + seek_mode: config_value!(parse("TRANSLATABLE_SEEK_MODE", "seek_mode", SeekMode::Alphabetical))? + }; // Freeze configuration in global cache Ok(TRANSLATABLE_CONFIG.get_or_init(|| config)) From 978c1b4b0a1311af571338d3aa95238c5d25850a Mon Sep 17 00:00:00 2001 From: Chiko Date: Mon, 31 Mar 2025 17:14:16 +0100 Subject: [PATCH 060/228] chore: remove serde and format imports --- Cargo.lock | 1 - translatable_proc/Cargo.toml | 1 - translatable_proc/src/data/config.rs | 38 +++++++++++++--------------- 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bc813ff..5a4a32f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -236,7 +236,6 @@ version = "0.1.0" dependencies = [ "proc-macro2", "quote", - "serde", "strum", "syn", "thiserror", diff --git a/translatable_proc/Cargo.toml b/translatable_proc/Cargo.toml index 6ef2923..5e980df 100644 --- a/translatable_proc/Cargo.toml +++ b/translatable_proc/Cargo.toml @@ -14,7 +14,6 @@ proc-macro = true [dependencies] proc-macro2 = "1.0.94" quote = "1.0.38" -serde = { version = "1.0.218", features = ["derive"] } strum = { version = "0.27.1", features = ["derive"] } syn = { version = "2.0.98", features = ["full"] } thiserror = "2.0.11" diff --git a/translatable_proc/src/data/config.rs b/translatable_proc/src/data/config.rs index 1505ba3..3871351 100644 --- a/translatable_proc/src/data/config.rs +++ b/translatable_proc/src/data/config.rs @@ -8,11 +8,10 @@ use std::fs::read_to_string; use std::io::Error as IoError; use std::sync::OnceLock; -use serde::Deserialize; +use strum::EnumString; use thiserror::Error; +use toml::Table; use toml::de::Error as TomlError; -use toml::{from_str as toml_from_str, Table}; -use strum::EnumString; /// Errors that can occur during configuration loading #[derive(Error, Debug)] @@ -123,35 +122,24 @@ pub fn load_config() -> Result<&'static MacroConfig, ConfigError> { } // Load base configuration from TOML file - let toml_content = read_to_string("./translatable.toml") - .unwrap_or_default() - .parse::
()?; + 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()) - ) + .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()) - ); + .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())) + value.parse().map_err(|_| ConfigError::InvalidValue($key.into(), value.into())) } else { Ok($default) } @@ -160,8 +148,16 @@ pub fn load_config() -> Result<&'static MacroConfig, ConfigError> { let config = MacroConfig { path: config_value!("TRANSLATABLE_PATH", "path", "./translatable.toml"), - overlap: config_value!(parse("TRANSLATABLE_OVERLAP", "overlap", TranslationOverlap::Ignore))?, - seek_mode: config_value!(parse("TRANSLATABLE_SEEK_MODE", "seek_mode", SeekMode::Alphabetical))? + overlap: config_value!(parse( + "TRANSLATABLE_OVERLAP", + "overlap", + TranslationOverlap::Ignore + ))?, + seek_mode: config_value!(parse( + "TRANSLATABLE_SEEK_MODE", + "seek_mode", + SeekMode::Alphabetical + ))?, }; // Freeze configuration in global cache From 5eea0d4a919bde69d90d2d3e94418cbd2564f88c Mon Sep 17 00:00:00 2001 From: Chiko Date: Mon, 31 Mar 2025 17:20:39 +0100 Subject: [PATCH 061/228] fix: path should be a dir not a file --- translatable_proc/src/data/config.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/translatable_proc/src/data/config.rs b/translatable_proc/src/data/config.rs index 3871351..419c18b 100644 --- a/translatable_proc/src/data/config.rs +++ b/translatable_proc/src/data/config.rs @@ -106,10 +106,10 @@ static TRANSLATABLE_CONFIG: OnceLock = OnceLock::new(); /// - Config file must be named `translatable.toml` in root directory /// - Environment variables take precedence over TOML configuration /// - Supported environment variables: -/// - `LOCALES_PATH`: Overrides translation directory path -/// - `SEEK_MODE`: Sets file processing order ("alphabetical" or +/// - `TRANSLATABLE_LOCALES_PATH`: Overrides translation directory path +/// - `TRANSLATABLE_SEEK_MODE`: Sets file processing order ("alphabetical" or /// "unalphabetical") -/// - `TRANSLATION_OVERLAP`: Sets conflict strategy ("overwrite" or "ignore") +/// - `TRANSLATABLE_OVERLAP`: Sets conflict strategy ("overwrite" or "ignore") /// /// # Panics /// Will not panic but returns ConfigError for: @@ -147,7 +147,7 @@ pub fn load_config() -> Result<&'static MacroConfig, ConfigError> { } let config = MacroConfig { - path: config_value!("TRANSLATABLE_PATH", "path", "./translatable.toml"), + path: config_value!("TRANSLATABLE_LOCALES_PATH", "path", "./translations"), overlap: config_value!(parse( "TRANSLATABLE_OVERLAP", "overlap", From 532ff659aaafdb75c6ae52be4ef824c684040bbd Mon Sep 17 00:00:00 2001 From: Chiko Date: Mon, 31 Mar 2025 17:52:48 +0100 Subject: [PATCH 062/228] docs: update security and governance md files for clarity --- GOVERNANCE.md | 1 - SECURITY.md | 19 +++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 0a80625..33cc929 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -1,4 +1,3 @@ - # Governance and Moderation This project is mainly maintained by the authors listed in both `translatable/Cargo.toml` and `translatable_proc/Cargo.toml`. diff --git a/SECURITY.md b/SECURITY.md index 7b1087e..e51cf70 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,9 +1,16 @@ - # Security Vulnerabilities -This library does not directly interact with networking, or anything another person -might be able to do to access the service using this library. +This library does not directly interact with networking, or anything another person might be able to do to access the service using this library. + +**If you find any security issues, please reach out to any of the maintainers listed in our [GOVERNANCE.md].** We take all security reports seriously and will get back to you as soon as possible. + +We also have security measures in place by using automated tools for managing dependencies. +Our project **strongly** relies on [Dependabot] to: +- Check for security vulnerabilities +- Update dependencies when needed +- Maintain all dependencies up to date + +This automated system helps us apply security patches regularly, reducing the need for manual checks on dependencies and ensuring that we are using the latest versions of libraries to prevent security issues. -To update the dependencies and solve vulnerability issues within we use dependabot, -which we believe a safe enough alternative to update all the dependencies -in the project. +[dependabot]: https://docs.github.com/en/code-security/dependabot +[governance.md]: GOVERNANCE.md From adddb8edf39e09c012570e991d808a3305163709 Mon Sep 17 00:00:00 2001 From: Chiko Date: Mon, 31 Mar 2025 18:30:58 +0100 Subject: [PATCH 063/228] chore: remove rstest and add makefile --- Makefile | 4 +++ translatable/tests/dynamic.rs | 5 +--- translatable/tests/dynamic/valid_syntax.rs | 10 ++----- .../tests/static/invalid_syntax.stderr | 26 ++++++++++++------- translatable/tests/static/missing_args.stderr | 16 +++--------- .../{ => tests}/translations/test.toml | 0 6 files changed, 27 insertions(+), 34 deletions(-) create mode 100644 Makefile rename translatable/{ => tests}/translations/test.toml (100%) diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..196aa79 --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ +export TRANSLATABLE_LOCALES_PATH=${PWD}/translatable/tests/translations + +test: + cargo test -p translatable -- --color=always diff --git a/translatable/tests/dynamic.rs b/translatable/tests/dynamic.rs index e908511..7aa4994 100644 --- a/translatable/tests/dynamic.rs +++ b/translatable/tests/dynamic.rs @@ -4,9 +4,6 @@ use trybuild::TestCases; fn dynamic_tests() { let t = TestCases::new(); - println!("{:?}", std::process::Command::new("pwd").output().unwrap()); - - // Compile-time tests for + // Compile-time tests for t.pass("tests/dynamic/valid_syntax.rs"); - } diff --git a/translatable/tests/dynamic/valid_syntax.rs b/translatable/tests/dynamic/valid_syntax.rs index 1d7fe14..2839c6d 100644 --- a/translatable/tests/dynamic/valid_syntax.rs +++ b/translatable/tests/dynamic/valid_syntax.rs @@ -1,21 +1,15 @@ use translatable::translation; fn main() { - - println!("{:?}", std::process::Command::new("pwd").output().unwrap()); - let a_lang = "es"; let a_path = "common.greeting"; let a_name = "john"; - // translation!(a_lang, a_path, name = a_name); - // !! https://github.com/dtolnay/trybuild/issues/202 - assert!(translation!(a_lang, a_path, name = a_name).unwrap() == "Β‘Hola john!".into()); + let _ = translation!(a_lang, a_path, name = a_name); let b_lang = "en"; let b_path = "common.greeting"; let b_name = "Marie"; - translation!(b_lang, b_path, name = b_name); - + let _ = translation!(b_lang, b_path, name = b_name); } diff --git a/translatable/tests/static/invalid_syntax.stderr b/translatable/tests/static/invalid_syntax.stderr index 487ae64..b08c3c5 100644 --- a/translatable/tests/static/invalid_syntax.stderr +++ b/translatable/tests/static/invalid_syntax.stderr @@ -6,18 +6,26 @@ error: expected `,` | = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) -error: An IO Error occurred: No such file or directory (os error 2) - --> tests/static/invalid_syntax.rs:8:13 - | -8 | let _ = translation!(42, static common::greeting); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) - -error: An IO Error occurred: No such file or directory (os error 2) +error: The path 'invalid.path' is not found in any of the translation files as a translation object. --> tests/static/invalid_syntax.rs:11:13 | 11 | let _ = translation!("es", static invalid::path); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `String: From<{integer}>` is not satisfied + --> tests/static/invalid_syntax.rs:8:13 + | +8 | let _ = translation!(42, static common::greeting); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `From<{integer}>` is not implemented for `String` + | + = help: the following other types implement trait `From`: + `String` implements `From<&String>` + `String` implements `From<&mut str>` + `String` implements `From<&str>` + `String` implements `From>` + `String` implements `From>` + `String` implements `From` + = note: required for `{integer}` to implement `Into` + = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/translatable/tests/static/missing_args.stderr b/translatable/tests/static/missing_args.stderr index c6b6b49..c1492f0 100644 --- a/translatable/tests/static/missing_args.stderr +++ b/translatable/tests/static/missing_args.stderr @@ -28,18 +28,8 @@ error: unexpected end of input, expected an expression | = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) -error: An IO Error occurred: No such file or directory (os error 2) - --> tests/static/missing_args.rs:17:13 - | -17 | let _ = translation!("es", static common::greeting); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) - -error: An IO Error occurred: No such file or directory (os error 2) - --> tests/static/missing_args.rs:20:13 +error[E0425]: cannot find value `name` in this scope + --> tests/static/missing_args.rs:20:57 | 20 | let _ = translation!("es", static common::greeting, name); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) + | ^^^^ not found in this scope diff --git a/translatable/translations/test.toml b/translatable/tests/translations/test.toml similarity index 100% rename from translatable/translations/test.toml rename to translatable/tests/translations/test.toml From fe70f81aa4a3fa9d6e57badcac6f5a3f2bc117c3 Mon Sep 17 00:00:00 2001 From: Chiko Date: Mon, 31 Mar 2025 22:31:29 +0100 Subject: [PATCH 064/228] refactor: re-organize tests --- Makefile | 2 +- README.md | 8 ++--- translatable/tests/dynamic.rs | 9 ----- translatable/tests/dynamic/args/empty_args.rs | 9 +++++ translatable/tests/dynamic/args/extra_args.rs | 9 +++++ .../tests/dynamic/args/inherit_args.rs | 10 ++++++ .../tests/dynamic/args/inherit_extra_args.rs | 11 ++++++ .../tests/dynamic/args/invalid_identifier.rs | 9 +++++ .../dynamic/args/invalid_identifier.stderr | 5 +++ translatable/tests/dynamic/args/with_args.rs | 9 +++++ translatable/tests/dynamic/invalid_syntax.rs | 18 ++++++++++ .../tests/dynamic/invalid_syntax.stderr | 23 ++++++++++++ .../tests/dynamic/unknown_translations.rs | 0 translatable/tests/dynamic/valid_syntax.rs | 15 -------- translatable/tests/static.rs | 11 ------ translatable/tests/static/args/empty_args.rs | 6 ++++ translatable/tests/static/args/extra_args.rs | 6 ++++ .../tests/static/args/inherit_args.rs | 7 ++++ .../tests/static/args/inherit_extra_args.rs | 9 +++++ .../tests/static/args/invalid_identifier.rs | 6 ++++ .../static/args/invalid_identifier.stderr | 5 +++ translatable/tests/static/args/with_args.rs | 6 ++++ translatable/tests/static/invalid_args.rs | 9 ----- translatable/tests/static/invalid_args.stderr | 13 ------- translatable/tests/static/missing_args.rs | 21 ----------- translatable/tests/static/missing_args.stderr | 35 ------------------- translatable/tests/tests.rs | 33 +++++++++++++++++ 27 files changed, 186 insertions(+), 118 deletions(-) delete mode 100644 translatable/tests/dynamic.rs create mode 100644 translatable/tests/dynamic/args/empty_args.rs create mode 100644 translatable/tests/dynamic/args/extra_args.rs create mode 100644 translatable/tests/dynamic/args/inherit_args.rs create mode 100644 translatable/tests/dynamic/args/inherit_extra_args.rs create mode 100644 translatable/tests/dynamic/args/invalid_identifier.rs create mode 100644 translatable/tests/dynamic/args/invalid_identifier.stderr create mode 100644 translatable/tests/dynamic/args/with_args.rs create mode 100644 translatable/tests/dynamic/invalid_syntax.rs create mode 100644 translatable/tests/dynamic/invalid_syntax.stderr delete mode 100644 translatable/tests/dynamic/unknown_translations.rs delete mode 100644 translatable/tests/dynamic/valid_syntax.rs delete mode 100644 translatable/tests/static.rs create mode 100644 translatable/tests/static/args/empty_args.rs create mode 100644 translatable/tests/static/args/extra_args.rs create mode 100644 translatable/tests/static/args/inherit_args.rs create mode 100644 translatable/tests/static/args/inherit_extra_args.rs create mode 100644 translatable/tests/static/args/invalid_identifier.rs create mode 100644 translatable/tests/static/args/invalid_identifier.stderr create mode 100644 translatable/tests/static/args/with_args.rs delete mode 100644 translatable/tests/static/invalid_args.rs delete mode 100644 translatable/tests/static/invalid_args.stderr delete mode 100644 translatable/tests/static/missing_args.rs delete mode 100644 translatable/tests/static/missing_args.stderr create mode 100644 translatable/tests/tests.rs diff --git a/Makefile b/Makefile index 196aa79..cd7494b 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ export TRANSLATABLE_LOCALES_PATH=${PWD}/translatable/tests/translations test: - cargo test -p translatable -- --color=always + cargo test -p translatable -- --nocapture --color=always diff --git a/README.md b/README.md index 4a98821..c55a83a 100644 --- a/README.md +++ b/README.md @@ -146,10 +146,10 @@ fn main() { let dynamic_lang = "es"; let dynamic_path = "common.greeting" - assert!(translation!("es", static common::greeting) == "Β‘Hola john!", name = "john"); - assert!(translation!("es", dynamic_path).unwrap() == "Β‘Hola john!".into(), name = "john"); - assert!(translation!(dynamic_lang, static common::greeting).unwrap() == "Β‘Hola john!".into(), name = "john"); - assert!(translation!(dynamic_lang, dynamic_path).unwrap() == "Β‘Hola john!".into(), name = "john"); + assert!(translation!("es", static common::greeting, name = "john") == "Β‘Hola john!"); + assert!(translation!("es", dynamic_path, name = "john").unwrap() == "Β‘Hola john!".into()); + assert!(translation!(dynamic_lang, static common::greeting, name = "john").unwrap() == "Β‘Hola john!".into()); + assert!(translation!(dynamic_lang, dynamic_path, name = "john").unwrap() == "Β‘Hola john!".into()); } ``` diff --git a/translatable/tests/dynamic.rs b/translatable/tests/dynamic.rs deleted file mode 100644 index 7aa4994..0000000 --- a/translatable/tests/dynamic.rs +++ /dev/null @@ -1,9 +0,0 @@ -use trybuild::TestCases; - -#[test] -fn dynamic_tests() { - let t = TestCases::new(); - - // Compile-time tests for - t.pass("tests/dynamic/valid_syntax.rs"); -} diff --git a/translatable/tests/dynamic/args/empty_args.rs b/translatable/tests/dynamic/args/empty_args.rs new file mode 100644 index 0000000..407588e --- /dev/null +++ b/translatable/tests/dynamic/args/empty_args.rs @@ -0,0 +1,9 @@ +use translatable::translation; + +fn main() { + let lang = "en"; + let path = "common.greeting"; + + let res = translation!(lang, path).unwrap(); + assert_eq!(res, "Hello {name}!"); +} diff --git a/translatable/tests/dynamic/args/extra_args.rs b/translatable/tests/dynamic/args/extra_args.rs new file mode 100644 index 0000000..6c4456b --- /dev/null +++ b/translatable/tests/dynamic/args/extra_args.rs @@ -0,0 +1,9 @@ +use translatable::translation; + +fn main() { + let lang = "en"; + let path = "common.greeting"; + + let res = translation!(lang, path, name = "Juan", surname = "Doe").unwrap(); + assert_eq!(res, "Hello Juan!"); +} diff --git a/translatable/tests/dynamic/args/inherit_args.rs b/translatable/tests/dynamic/args/inherit_args.rs new file mode 100644 index 0000000..33654be --- /dev/null +++ b/translatable/tests/dynamic/args/inherit_args.rs @@ -0,0 +1,10 @@ +use translatable::translation; + +fn main() { + let lang = "en"; + let path = "common.greeting"; + let name = "Juan"; + + let res = translation!(lang, path, name).unwrap(); + assert_eq!(res, "Hello Juan!"); +} diff --git a/translatable/tests/dynamic/args/inherit_extra_args.rs b/translatable/tests/dynamic/args/inherit_extra_args.rs new file mode 100644 index 0000000..7177f19 --- /dev/null +++ b/translatable/tests/dynamic/args/inherit_extra_args.rs @@ -0,0 +1,11 @@ +use translatable::translation; + +fn main() { + let lang = "en"; + let path = "common.greeting"; + let name = "Juan"; + let surname = "Doe"; + + let res = translation!(lang, path, name, surname).unwrap(); + assert_eq!(res, "Hello Juan!"); +} diff --git a/translatable/tests/dynamic/args/invalid_identifier.rs b/translatable/tests/dynamic/args/invalid_identifier.rs new file mode 100644 index 0000000..5462471 --- /dev/null +++ b/translatable/tests/dynamic/args/invalid_identifier.rs @@ -0,0 +1,9 @@ +use translatable::translation; + +fn main() { + let lang = "en"; + let path = "common.greeting"; + + // Invalid argument syntax + translation!(lang, path, 42 = "value"); +} diff --git a/translatable/tests/dynamic/args/invalid_identifier.stderr b/translatable/tests/dynamic/args/invalid_identifier.stderr new file mode 100644 index 0000000..9d6f252 --- /dev/null +++ b/translatable/tests/dynamic/args/invalid_identifier.stderr @@ -0,0 +1,5 @@ +error: expected identifier + --> tests/dynamic/args/invalid_identifier.rs:8:30 + | +8 | translation!(lang, path, 42 = "value"); + | ^^ diff --git a/translatable/tests/dynamic/args/with_args.rs b/translatable/tests/dynamic/args/with_args.rs new file mode 100644 index 0000000..3b1c745 --- /dev/null +++ b/translatable/tests/dynamic/args/with_args.rs @@ -0,0 +1,9 @@ +use translatable::translation; + +fn main() { + let lang = "en"; + let path = "common.greeting"; + + let res = translation!(lang, path, name = "Juan").unwrap(); + assert_eq!(res, "Hello Juan!"); +} diff --git a/translatable/tests/dynamic/invalid_syntax.rs b/translatable/tests/dynamic/invalid_syntax.rs new file mode 100644 index 0000000..e877bd7 --- /dev/null +++ b/translatable/tests/dynamic/invalid_syntax.rs @@ -0,0 +1,18 @@ +use translatable::translation; + +fn main() { + // Missing arguments + let lang = "es"; + let _ = translation!(lang); + + // Invalid literal type + let lang = 42; + let path = "common.greeting"; + let _ = translation!(lang, path); + + // Malformed dynamic path + // TODO: I'm 100% sure that this is a bug. + let lang = "es"; + let path = "invalid.path"; + assert!(translation!(lang, path).is_err()); +} diff --git a/translatable/tests/dynamic/invalid_syntax.stderr b/translatable/tests/dynamic/invalid_syntax.stderr new file mode 100644 index 0000000..65e3f05 --- /dev/null +++ b/translatable/tests/dynamic/invalid_syntax.stderr @@ -0,0 +1,23 @@ +error: expected `,` + --> tests/dynamic/invalid_syntax.rs:6:13 + | +6 | let _ = translation!(lang); + | ^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `String: From<{integer}>` is not satisfied + --> tests/dynamic/invalid_syntax.rs:11:13 + | +11 | let _ = translation!(lang, path); + | ^^^^^^^^^^^^^^^^^^^^^^^^ the trait `From<{integer}>` is not implemented for `String` + | + = help: the following other types implement trait `From`: + `String` implements `From<&String>` + `String` implements `From<&mut str>` + `String` implements `From<&str>` + `String` implements `From>` + `String` implements `From>` + `String` implements `From` + = note: required for `{integer}` to implement `Into` + = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/translatable/tests/dynamic/unknown_translations.rs b/translatable/tests/dynamic/unknown_translations.rs deleted file mode 100644 index e69de29..0000000 diff --git a/translatable/tests/dynamic/valid_syntax.rs b/translatable/tests/dynamic/valid_syntax.rs deleted file mode 100644 index 2839c6d..0000000 --- a/translatable/tests/dynamic/valid_syntax.rs +++ /dev/null @@ -1,15 +0,0 @@ -use translatable::translation; - -fn main() { - let a_lang = "es"; - let a_path = "common.greeting"; - let a_name = "john"; - - let _ = translation!(a_lang, a_path, name = a_name); - - let b_lang = "en"; - let b_path = "common.greeting"; - let b_name = "Marie"; - - let _ = translation!(b_lang, b_path, name = b_name); -} diff --git a/translatable/tests/static.rs b/translatable/tests/static.rs deleted file mode 100644 index eaeb131..0000000 --- a/translatable/tests/static.rs +++ /dev/null @@ -1,11 +0,0 @@ -use trybuild::TestCases; - -#[test] -fn static_tests() { - let t = TestCases::new(); - - // Compile-time tests for invalid translations - t.compile_fail("tests/static/invalid_args.rs"); - t.compile_fail("tests/static/invalid_syntax.rs"); - t.compile_fail("tests/static/missing_args.rs"); -} diff --git a/translatable/tests/static/args/empty_args.rs b/translatable/tests/static/args/empty_args.rs new file mode 100644 index 0000000..8762a36 --- /dev/null +++ b/translatable/tests/static/args/empty_args.rs @@ -0,0 +1,6 @@ +use translatable::translation; + +fn main() { + let res = translation!("en", static common::greeting); + assert_eq!(res, "Hello {name}!"); +} diff --git a/translatable/tests/static/args/extra_args.rs b/translatable/tests/static/args/extra_args.rs new file mode 100644 index 0000000..a3e9f59 --- /dev/null +++ b/translatable/tests/static/args/extra_args.rs @@ -0,0 +1,6 @@ +use translatable::translation; + +fn main() { + let res = translation!("en", static common::greeting, name = "Juan", surname = "Doe"); + assert_eq!(res, "Hello Juan!"); +} diff --git a/translatable/tests/static/args/inherit_args.rs b/translatable/tests/static/args/inherit_args.rs new file mode 100644 index 0000000..3f2a7c4 --- /dev/null +++ b/translatable/tests/static/args/inherit_args.rs @@ -0,0 +1,7 @@ +use translatable::translation; + +fn main() { + let name = "Juan"; + let res = translation!("en", static common::greeting, name); + assert_eq!(res, "Hello Juan!"); +} diff --git a/translatable/tests/static/args/inherit_extra_args.rs b/translatable/tests/static/args/inherit_extra_args.rs new file mode 100644 index 0000000..cbab001 --- /dev/null +++ b/translatable/tests/static/args/inherit_extra_args.rs @@ -0,0 +1,9 @@ +use translatable::translation; + +fn main() { + let name = "Juan"; + let surname = "Doe"; + + let res = translation!("en", static common::greeting, name, surname); + assert_eq!(res, "Hello Juan!"); +} diff --git a/translatable/tests/static/args/invalid_identifier.rs b/translatable/tests/static/args/invalid_identifier.rs new file mode 100644 index 0000000..c08449c --- /dev/null +++ b/translatable/tests/static/args/invalid_identifier.rs @@ -0,0 +1,6 @@ +use translatable::translation; + +fn main() { + // Invalid argument syntax + translation!("es", static common::greeting, 42 = "value"); +} diff --git a/translatable/tests/static/args/invalid_identifier.stderr b/translatable/tests/static/args/invalid_identifier.stderr new file mode 100644 index 0000000..a87e974 --- /dev/null +++ b/translatable/tests/static/args/invalid_identifier.stderr @@ -0,0 +1,5 @@ +error: expected identifier + --> tests/static/args/invalid_identifier.rs:5:49 + | +5 | translation!("es", static common::greeting, 42 = "value"); + | ^^ diff --git a/translatable/tests/static/args/with_args.rs b/translatable/tests/static/args/with_args.rs new file mode 100644 index 0000000..4038d86 --- /dev/null +++ b/translatable/tests/static/args/with_args.rs @@ -0,0 +1,6 @@ +use translatable::translation; + +fn main() { + let res = translation!("en", static common::greeting, name = "Juan"); + assert_eq!(res, "Hello Juan!"); +} diff --git a/translatable/tests/static/invalid_args.rs b/translatable/tests/static/invalid_args.rs deleted file mode 100644 index 1b21e43..0000000 --- a/translatable/tests/static/invalid_args.rs +++ /dev/null @@ -1,9 +0,0 @@ -use translatable::translation; - -fn main() { - // Missing required arguments - translation!("es"); - - // Invalid argument syntax - translation!("es", "path", 42 = "value"); -} diff --git a/translatable/tests/static/invalid_args.stderr b/translatable/tests/static/invalid_args.stderr deleted file mode 100644 index 0ed5468..0000000 --- a/translatable/tests/static/invalid_args.stderr +++ /dev/null @@ -1,13 +0,0 @@ -error: expected `,` - --> tests/static/invalid_args.rs:5:5 - | -5 | translation!("es"); - | ^^^^^^^^^^^^^^^^^^ - | - = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) - -error: expected identifier - --> tests/static/invalid_args.rs:8:32 - | -8 | translation!("es", "path", 42 = "value"); - | ^^ diff --git a/translatable/tests/static/missing_args.rs b/translatable/tests/static/missing_args.rs deleted file mode 100644 index aca2bb3..0000000 --- a/translatable/tests/static/missing_args.rs +++ /dev/null @@ -1,21 +0,0 @@ -use translatable::translation; - -fn main() { - // Test completely empty invocation - let _ = translation!(); - - // Missing path argument - let _ = translation!("es"); - - // Missing language argument - let _ = translation!(static common::greeting); - - // Missing both language and path - let _ = translation!(); - - // Missing interpolation arguments - let _ = translation!("es", static common::greeting); - - // Partial arguments with named params - let _ = translation!("es", static common::greeting, name); -} diff --git a/translatable/tests/static/missing_args.stderr b/translatable/tests/static/missing_args.stderr deleted file mode 100644 index c1492f0..0000000 --- a/translatable/tests/static/missing_args.stderr +++ /dev/null @@ -1,35 +0,0 @@ -error: unexpected end of input, expected an expression - --> tests/static/missing_args.rs:5:13 - | -5 | let _ = translation!(); - | ^^^^^^^^^^^^^^ - | - = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) - -error: expected `,` - --> tests/static/missing_args.rs:8:13 - | -8 | let _ = translation!("es"); - | ^^^^^^^^^^^^^^^^^^ - | - = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) - -error: expected `|` - --> tests/static/missing_args.rs:11:33 - | -11 | let _ = translation!(static common::greeting); - | ^^^^^^ - -error: unexpected end of input, expected an expression - --> tests/static/missing_args.rs:14:13 - | -14 | let _ = translation!(); - | ^^^^^^^^^^^^^^ - | - = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) - -error[E0425]: cannot find value `name` in this scope - --> tests/static/missing_args.rs:20:57 - | -20 | let _ = translation!("es", static common::greeting, name); - | ^^^^ not found in this scope diff --git a/translatable/tests/tests.rs b/translatable/tests/tests.rs new file mode 100644 index 0000000..1e65fd8 --- /dev/null +++ b/translatable/tests/tests.rs @@ -0,0 +1,33 @@ +use trybuild::TestCases; + +#[test] +fn static_tests() { + let t = TestCases::new(); + + // implementation + t.compile_fail("tests/static/invalid_syntax.rs"); + + // args + t.pass("tests/static/args/empty_args.rs"); + t.pass("tests/static/args/extra_args.rs"); + t.pass("tests/static/args/inherit_args.rs"); + t.pass("tests/static/args/inherit_extra_args.rs"); + t.compile_fail("tests/static/args/invalid_identifier.rs"); + t.pass("tests/static/args/with_args.rs"); +} + +#[test] +fn dynamic_tests() { + let t = TestCases::new(); + + // implementation + t.compile_fail("tests/dynamic/invalid_syntax.rs"); + + // args + t.pass("tests/dynamic/args/empty_args.rs"); + t.pass("tests/dynamic/args/extra_args.rs"); + t.pass("tests/dynamic/args/inherit_args.rs"); + t.pass("tests/dynamic/args/inherit_extra_args.rs"); + t.compile_fail("tests/dynamic/args/invalid_identifier.rs"); + t.pass("tests/dynamic/args/with_args.rs"); +} From 7fd2c175466e2062259ada0390ce5f0ac999ec40 Mon Sep 17 00:00:00 2001 From: stifskere Date: Fri, 4 Apr 2025 17:23:08 +0200 Subject: [PATCH 065/228] feat: rearange namespaces --- Cargo.lock | 2 ++ translatable/Cargo.toml | 1 + translatable/src/error.rs | 4 ++-- translatable/src/lib.rs | 6 +++++- translatable_proc/Cargo.toml | 1 + .../src/translations/generation.rs | 19 +++++++++++-------- translatable_shared/src/errors.rs | 0 translatable_shared/src/lib.rs | 9 +++++++-- 8 files changed, 29 insertions(+), 13 deletions(-) delete mode 100644 translatable_shared/src/errors.rs diff --git a/Cargo.lock b/Cargo.lock index 8794581..364d0d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -227,6 +227,7 @@ version = "0.1.0" dependencies = [ "thiserror", "translatable_proc", + "translatable_shared", "trybuild", ] @@ -241,6 +242,7 @@ dependencies = [ "syn", "thiserror", "toml", + "translatable_shared", ] [[package]] diff --git a/translatable/Cargo.toml b/translatable/Cargo.toml index dd8202a..785f623 100644 --- a/translatable/Cargo.toml +++ b/translatable/Cargo.toml @@ -18,6 +18,7 @@ keywords = [ [dependencies] thiserror = "2.0.12" translatable_proc = { path = "../translatable_proc" } +translatable_shared = { path = "../translatable_shared/" } [dev-dependencies] trybuild = "1.0.104" diff --git a/translatable/src/error.rs b/translatable/src/error.rs index f2d6b25..3226590 100644 --- a/translatable/src/error.rs +++ b/translatable/src/error.rs @@ -5,7 +5,7 @@ use thiserror::Error; /// Returned by the translation macro when dynamic resolution fails. /// For static resolution failures, errors are reported at compile time. #[derive(Error, Debug)] -pub enum Error { +pub enum RuntimeError { /// Invalid ISO 639-1 language code provided #[error("The language '{0}' is invalid.")] InvalidLanguage(String), @@ -19,7 +19,7 @@ pub enum Error { PathNotFound(String), } -impl Error { +impl RuntimeError { /// Returns formatted error message as a String /// /// Useful for error reporting and logging. Marked `#[cold]` to hint to the diff --git a/translatable/src/lib.rs b/translatable/src/lib.rs index 72f3d07..e8a4de4 100644 --- a/translatable/src/lib.rs +++ b/translatable/src/lib.rs @@ -1,6 +1,10 @@ mod error; // Export the private error module -pub use error::Error; +pub use error::RuntimeError as Error; + /// Re-export the procedural macro for crate users pub use translatable_proc::translation; + +/// Re-export utils used for both runtime and compile-time +pub use translatable_shared::{Language, NestingType}; diff --git a/translatable_proc/Cargo.toml b/translatable_proc/Cargo.toml index 9e86ed3..abcd481 100644 --- a/translatable_proc/Cargo.toml +++ b/translatable_proc/Cargo.toml @@ -19,3 +19,4 @@ strum = "0.27.1" syn = { version = "2.0.98", features = ["full"] } thiserror = "2.0.11" toml = "0.8.20" +translatable_shared = { path = "../translatable_shared/" } diff --git a/translatable_proc/src/translations/generation.rs b/translatable_proc/src/translations/generation.rs index aaddabb..7f64ac9 100644 --- a/translatable_proc/src/translations/generation.rs +++ b/translatable_proc/src/translations/generation.rs @@ -5,9 +5,10 @@ use quote::quote; use strum::IntoEnumIterator; use syn::{Expr, parse2}; +use translatable_shared::Language; + use super::errors::TranslationError; use crate::data::translations::load_translations; -use crate::languages::Iso639a; /// Generates compile-time string replacement logic for a single format /// argument. @@ -88,8 +89,9 @@ fn kwarg_dynamic_replaces(format_kwargs: &HashMap) -> Vec Result { - lang.parse::().map_err(|_| TranslationError::InvalidLanguage(lang.to_string())) +pub fn load_lang_static(lang: &str) -> Result { + lang.parse::() + .map_err(|_| TranslationError::InvalidLanguage(lang.to_string())) } /// Generates runtime validation for a dynamic language expression. @@ -104,11 +106,12 @@ pub fn load_lang_dynamic(lang: TokenStream) -> Result` @@ -135,7 +138,7 @@ pub fn load_lang_dynamic(lang: TokenStream) -> Result, + static_lang: Option, path: String, format_kwargs: HashMap, ) -> Result { diff --git a/translatable_shared/src/errors.rs b/translatable_shared/src/errors.rs deleted file mode 100644 index e69de29..0000000 diff --git a/translatable_shared/src/lib.rs b/translatable_shared/src/lib.rs index 21410ee..93c9dc6 100644 --- a/translatable_shared/src/lib.rs +++ b/translatable_shared/src/lib.rs @@ -1,3 +1,8 @@ -pub mod languages; -pub mod nesting_type; +mod languages; +mod nesting_type; + +/// Export all the structures in the common +/// top-level namespace +pub use crate::languages::Language; +pub use crate::nesting_type::NestingType; From ee281eff3a6cf92d688c133c1e198c56891e0385 Mon Sep 17 00:00:00 2001 From: stifskere Date: Sun, 6 Apr 2025 04:14:39 +0200 Subject: [PATCH 066/228] fix: managed to mitigate all comp time errors --- Cargo.lock | 6 +- translatable_proc/Cargo.toml | 1 - translatable_proc/src/data/translations.rs | 143 +--------------- translatable_proc/src/translations/errors.rs | 8 +- .../src/translations/generation.rs | 13 +- translatable_shared/Cargo.toml | 5 + translatable_shared/src/languages.rs | 16 ++ translatable_shared/src/lib.rs | 4 +- translatable_shared/src/nesting_type.rs | 156 ++++++++++++++++-- 9 files changed, 178 insertions(+), 174 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 364d0d8..a832e6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -238,7 +238,6 @@ dependencies = [ "proc-macro2", "quote", "serde", - "strum", "syn", "thiserror", "toml", @@ -249,7 +248,12 @@ dependencies = [ name = "translatable_shared" version = "0.1.0" dependencies = [ + "proc-macro2", + "quote", "strum", + "syn", + "thiserror", + "toml", ] [[package]] diff --git a/translatable_proc/Cargo.toml b/translatable_proc/Cargo.toml index abcd481..d7fd0b1 100644 --- a/translatable_proc/Cargo.toml +++ b/translatable_proc/Cargo.toml @@ -15,7 +15,6 @@ proc-macro = true proc-macro2 = "1.0.94" quote = "1.0.38" serde = { version = "1.0.218", features = ["derive"] } -strum = "0.27.1" syn = { version = "2.0.98", features = ["full"] } thiserror = "2.0.11" toml = "0.8.20" diff --git a/translatable_proc/src/data/translations.rs b/translatable_proc/src/data/translations.rs index 6017bed..f8378e6 100644 --- a/translatable_proc/src/data/translations.rs +++ b/translatable_proc/src/data/translations.rs @@ -2,45 +2,14 @@ use std::collections::HashMap; use std::fs::{read_dir, read_to_string}; use std::sync::OnceLock; -use proc_macro2::{Span, TokenStream}; -use quote::quote; -use strum::ParseError; -use syn::LitStr; use thiserror::Error; use toml::{Table, Value}; +use translatable_shared::{Language, NestingType}; + use super::config::{SeekMode, TranslationOverlap, load_config}; use crate::translations::errors::TranslationError; -/// Errors occurring during TOML-to-translation structure transformation -#[derive(Error, Debug)] -pub enum TransformError { - /// Mixed content found in nesting node (strings and objects cannot coexist) - #[error("A nesting can contain either strings or other nestings, but not both.")] - InvalidNesting, - - /// Template syntax error with unbalanced braces - #[error("Templates in translations should match '{{' and '}}'")] - UnclosedTemplate, - - /// Invalid value type encountered in translation structure - #[error("Only strings and objects are allowed for nested objects.")] - InvalidValue, - - /// Failed to parse language code from translation key - #[error("Couldn't parse ISO 639-1 string for translation key")] - LanguageParsing(#[from] ParseError), -} - -/// Represents hierarchical translation structure -#[derive(Clone)] -pub enum NestingType { - /// Nested namespace containing other translation objects - Object(HashMap), - /// Leaf node containing actual translations per language - Translation(HashMap), -} - /// Translation association with its source file pub struct AssociatedTranslation { /// Original file path of the translation @@ -80,21 +49,6 @@ fn walk_dir(path: &str) -> Result, TranslationError> { Ok(result) } -/// Validates template brace balancing in translation strings -fn templates_valid(translation: &str) -> bool { - let mut nestings = 0; - - for character in translation.chars() { - match character { - '{' => nestings += 1, - '}' => nestings -= 1, - _ => {}, - } - } - - nestings == 0 -} - /// Loads and caches translations from configured directory /// /// # Returns @@ -141,99 +95,6 @@ pub fn load_translations() -> Result<&'static Vec, Transl Ok(TRANSLATIONS.get_or_init(|| translations)) } -impl NestingType { - /// Resolves a translation path through the nesting hierarchy - /// - /// # Arguments - /// * `path` - Slice of path segments to resolve - /// - /// # Returns - /// Reference to translations if path exists and points to leaf node - pub fn get_path(&self, path: Vec<&str>) -> Option<&HashMap> { - match self { - Self::Object(nested) => { - let (first, rest) = path.split_first()?; - nested.get(*first)?.get_path(rest.to_vec()) - }, - Self::Translation(translation) => path.is_empty().then_some(translation), - } - } -} - -impl From for TokenStream { - /// Converts NestingType to procedural macro output tokens - fn from(val: NestingType) -> Self { - match val { - NestingType::Object(nesting) => { - let entries = nesting.into_iter().map(|(key, value)| -> TokenStream { - let key = LitStr::new(&key, Span::call_site()); - let value: TokenStream = value.into(); - quote! { (#key.to_string(), #value) } - }); - - quote! { - translatable::internal::NestingType::Object(vec![#(#entries),*].into_iter().collect()) - } - }, - - NestingType::Translation(translation) => { - let entries = translation.into_iter().map(|(lang, value)| { - let lang = LitStr::new(&format!("{lang:?}").to_lowercase(), Span::call_site()); - let value = LitStr::new(&value, Span::call_site()); - - quote! { (#lang.to_string(), #value.to_string()) } - }); - - quote! { - translatable::internal::NestingType::Translation(vec![#(#entries),*].into_iter().collect()) - } - }, - } - } -} - -impl TryFrom
for NestingType { - type Error = TransformError; - - /// Converts TOML table to validated translation structure - fn try_from(value: Table) -> Result { - let mut result = None; - - for (key, value) in value { - match value { - Value::String(translation_value) => { - // Initialize result if first entry - let result = result.get_or_insert_with(|| Self::Translation(HashMap::new())); - - match result { - Self::Translation(translation) => { - if !templates_valid(&translation_value) { - return Err(TransformError::UnclosedTemplate); - } - translation.insert(key.parse()?, translation_value); - }, - Self::Object(_) => return Err(TransformError::InvalidNesting), - } - }, - - Value::Table(nesting_value) => { - let result = result.get_or_insert_with(|| Self::Object(HashMap::new())); - - match result { - Self::Object(nesting) => { - nesting.insert(key, Self::try_from(nesting_value)?); - }, - Self::Translation(_) => return Err(TransformError::InvalidNesting), - } - }, - - _ => return Err(TransformError::InvalidValue), - } - } - - result.ok_or(TransformError::InvalidValue) - } -} impl AssociatedTranslation { /// Gets the original file path of the translation diff --git a/translatable_proc/src/translations/errors.rs b/translatable_proc/src/translations/errors.rs index ccb49b8..75852f7 100644 --- a/translatable_proc/src/translations/errors.rs +++ b/translatable_proc/src/translations/errors.rs @@ -4,9 +4,9 @@ use syn::Error as SynError; use thiserror::Error; use toml::de::Error as TomlError; +use translatable_shared::{Language, TransformError}; + use crate::data::config::ConfigError; -use crate::data::translations::TransformError; -use crate::languages::Iso639a; /// Errors that can occur during translation processing. #[derive(Error, Debug)] @@ -37,7 +37,7 @@ pub enum TranslationError { #[error( "'{0}' is not valid ISO 639-1. {similarities}", similarities = { - let similarities = Iso639a::get_similarities(.0, 10); + let similarities = Language::get_similarities(.0, 10); let similarities_format = similarities .similarities() .join("\n"); @@ -67,7 +67,7 @@ pub enum TranslationError { /// Language not available for the specified path #[error("The language '{0:?}' ({0:#}) is not available for the '{1}' translation.")] - LanguageNotAvailable(Iso639a, String), + LanguageNotAvailable(Language, String), /// Error parsing macro. #[error("Error parsing macro.")] diff --git a/translatable_proc/src/translations/generation.rs b/translatable_proc/src/translations/generation.rs index 7f64ac9..d2c8323 100644 --- a/translatable_proc/src/translations/generation.rs +++ b/translatable_proc/src/translations/generation.rs @@ -2,7 +2,6 @@ use std::collections::HashMap; use proc_macro2::TokenStream; use quote::quote; -use strum::IntoEnumIterator; use syn::{Expr, parse2}; use translatable_shared::Language; @@ -105,14 +104,6 @@ pub fn load_lang_static(lang: &str) -> Result { pub fn load_lang_dynamic(lang: TokenStream) -> Result { let lang: Expr = parse2(lang)?; - // Generate list of available language codes - let available_langs = Language::iter() - .map(|language| { - let language = format!("{language:?}"); - - quote! { #language, } - }); - // The `String` explicit type serves as // expression type checking, we accept `impl Into` // for any expression that's not static. @@ -123,7 +114,7 @@ pub fn load_lang_dynamic(lang: TokenStream) -> Result, + static_lang: Option, path: TokenStream, format_kwargs: HashMap, ) -> Result { diff --git a/translatable_shared/Cargo.toml b/translatable_shared/Cargo.toml index ca97b2b..c8133fb 100644 --- a/translatable_shared/Cargo.toml +++ b/translatable_shared/Cargo.toml @@ -9,4 +9,9 @@ edition = "2024" authors = ["Esteve Autet ", "Chiko "] [dependencies] +proc-macro2 = "1.0.94" +quote = "1.0.40" strum = { version = "0.27.1", features = ["derive", "strum_macros"] } +syn = { version = "2.0.100", features = ["full"] } +thiserror = "2.0.12" +toml = "0.8.20" diff --git a/translatable_shared/src/languages.rs b/translatable_shared/src/languages.rs index df39210..fd8d745 100644 --- a/translatable_shared/src/languages.rs +++ b/translatable_shared/src/languages.rs @@ -1,4 +1,7 @@ +use proc_macro2::{Span, TokenStream as TokenStream2}; +use quote::quote; use strum::{Display, EnumIter, EnumString, IntoEnumIterator}; +use syn::Ident; /// ISO 639-1 language code implementation with validation /// @@ -424,3 +427,16 @@ impl PartialEq for Language { format!("{self:?}").to_lowercase() == other.to_lowercase() } } + +/// 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 Into for Language { + fn into(self) -> TokenStream2 { + let ident = Ident::new(&format!("{self:?}"), Span::call_site()); + + quote! { translatable::Language::#ident } + } +} diff --git a/translatable_shared/src/lib.rs b/translatable_shared/src/lib.rs index 93c9dc6..eacd56f 100644 --- a/translatable_shared/src/lib.rs +++ b/translatable_shared/src/lib.rs @@ -4,5 +4,5 @@ mod nesting_type; /// Export all the structures in the common /// top-level namespace -pub use crate::languages::Language; -pub use crate::nesting_type::NestingType; +pub use crate::languages::{Language, Similarities}; +pub use crate::nesting_type::{NestingType, TransformError}; diff --git a/translatable_shared/src/nesting_type.rs b/translatable_shared/src/nesting_type.rs index f123eb3..f99e99d 100644 --- a/translatable_shared/src/nesting_type.rs +++ b/translatable_shared/src/nesting_type.rs @@ -1,37 +1,165 @@ use std::collections::HashMap; -// Everything in this module should be -// marked with #[doc(hidden)] as we don't -// want the LSP to be notifying it's existence. +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use thiserror::Error; +use toml::{Table, Value}; +use strum::ParseError; -/// Represents nested translation structures -#[doc(hidden)] +use crate::Language; + +/// Errors occurring during TOML-to-translation structure transformation +#[derive(Error, Debug)] +pub enum TransformError { + /// Mixed content found in nesting node (strings and objects cannot coexist) + #[error("A nesting can contain either strings or other nestings, but not both.")] + InvalidNesting, + + /// Template syntax error with unbalanced braces + #[error("Templates in translations should match '{{' and '}}'")] + UnclosedTemplate, + + /// Invalid value type encountered in translation structure + #[error("Only strings and objects are allowed for nested objects.")] + InvalidValue, + + /// Failed to parse language code from translation key + #[error("Couldn't parse ISO 639-1 string for translation key")] + LanguageParsing(#[from] ParseError), +} + +/// Represents nested translation structure, +/// as it is on the translation files. +#[derive(Clone)] pub enum NestingType { - /// Intermediate node containing nested translation objects + /// Nested namespace containing other translation objects Object(HashMap), - /// Leaf node containing actual translations for different languages - Translation(HashMap), + /// Leaf node containing actual translations per language + Translation(HashMap), } impl NestingType { - /// Resolves a translation path through nested structures + /// Resolves a translation path through the nesting hierarchy /// /// # Arguments /// * `path` - Slice of path segments to resolve /// /// # Returns - /// - `Some(&HashMap)` if path resolves to translations - /// - `None` if path is invalid - #[doc(hidden)] - pub fn get_path(&self, path: Vec<&str>) -> Option<&HashMap> { + /// Reference to translations if path exists and points to leaf node + pub fn get_path(&self, path: Vec<&str>) -> Option<&HashMap> { match self { Self::Object(nested) => { let (first, rest) = path.split_first()?; nested.get(*first)?.get_path(rest.to_vec()) }, - Self::Translation(translation) => path.is_empty().then_some(translation), } } } +/// 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 Into for NestingType { + fn into(self) -> TokenStream2 { + match self { + Self::Object(nesting) => { + let mapped_nesting = nesting + .into_iter() + .map(|(key, value)| { + let value: TokenStream2 = value.into(); + quote! { (#key, #value) } + }) + .collect::>(); + + quote! {{ + translatable::NestingType::Object( + vec![#(#mapped_nesting),*] + .into_iter() + .collect::>() + ) + }} + } + + Self::Translation(translation) => { + let mapped_translation = translation + .into_iter() + .map(|(key, value)| { + let key: TokenStream2 = key.into(); + quote! { (#key, #value) } + }) + .collect::>(); + + quote! {{ + translatable::NestingType::Translation( + vec![#(#mapped_translation),*] + .into_iter() + .collect::>() + ) + }} + } + } + } +} + +/// Validates template brace balancing in translation strings +fn templates_valid(translation: &str) -> bool { + let mut nestings = 0; + + for character in translation.chars() { + match character { + '{' => nestings += 1, + '}' => nestings -= 1, + _ => {}, + } + } + + nestings == 0 +} + +/// This implementation converts a `toml::Table` into a manageable +/// NestingType. +impl TryFrom
for NestingType { + type Error = TransformError; + + /// Converts TOML table to validated translation structure + fn try_from(value: Table) -> Result { + let mut result = None; + + for (key, value) in value { + match value { + Value::String(translation_value) => { + // Initialize result if first entry + let result = result.get_or_insert_with(|| Self::Translation(HashMap::new())); + + match result { + Self::Translation(translation) => { + if !templates_valid(&translation_value) { + return Err(TransformError::UnclosedTemplate); + } + translation.insert(key.parse()?, translation_value); + }, + Self::Object(_) => return Err(TransformError::InvalidNesting), + } + }, + + Value::Table(nesting_value) => { + let result = result.get_or_insert_with(|| Self::Object(HashMap::new())); + + match result { + Self::Object(nesting) => { + nesting.insert(key, Self::try_from(nesting_value)?); + }, + Self::Translation(_) => return Err(TransformError::InvalidNesting), + } + }, + + _ => return Err(TransformError::InvalidValue), + } + } + + result.ok_or(TransformError::InvalidValue) + } +} From 4330fd8beac05f19cc545b7618dbb78b1e7442c0 Mon Sep 17 00:00:00 2001 From: stifskere Date: Sun, 6 Apr 2025 20:57:42 +0200 Subject: [PATCH 067/228] chore: changed names and module of shared items --- Cargo.lock | 1 + translatable/Cargo.toml | 1 + translatable/src/lib.rs | 11 ++++- translatable/tests/test.rs | 1 + translatable_proc/src/data/translations.rs | 26 +++++------ translatable_proc/src/translations/errors.rs | 6 +-- .../src/translations/generation.rs | 19 ++++---- translatable_shared/src/languages.rs | 2 +- translatable_shared/src/lib.rs | 4 +- translatable_shared/src/nesting_type.rs | 45 ++++++++++--------- 10 files changed, 62 insertions(+), 54 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a832e6f..5f3f385 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -225,6 +225,7 @@ dependencies = [ name = "translatable" version = "0.1.0" dependencies = [ + "strum", "thiserror", "translatable_proc", "translatable_shared", diff --git a/translatable/Cargo.toml b/translatable/Cargo.toml index 785f623..f6614c1 100644 --- a/translatable/Cargo.toml +++ b/translatable/Cargo.toml @@ -16,6 +16,7 @@ keywords = [ ] [dependencies] +strum = { version = "0.27.1", features = ["derive"] } thiserror = "2.0.12" translatable_proc = { path = "../translatable_proc" } translatable_shared = { path = "../translatable_shared/" } diff --git a/translatable/src/lib.rs b/translatable/src/lib.rs index e8a4de4..8d2c02a 100644 --- a/translatable/src/lib.rs +++ b/translatable/src/lib.rs @@ -6,5 +6,12 @@ pub use error::RuntimeError as Error; /// Re-export the procedural macro for crate users pub use translatable_proc::translation; -/// Re-export utils used for both runtime and compile-time -pub use translatable_shared::{Language, NestingType}; +/// This module re-exports structures used by macros +/// that should not but could be used by the users +/// of the library +pub mod shared { + /// Re-export utils used for both runtime and compile-time + pub use translatable_shared::{Language, TranslationNode, LanguageIter}; + + pub use strum::IntoEnumIterator; +} diff --git a/translatable/tests/test.rs b/translatable/tests/test.rs index e8a3856..137ceff 100644 --- a/translatable/tests/test.rs +++ b/translatable/tests/test.rs @@ -1,3 +1,4 @@ +use strum::IntoEnumIterator; use translatable::translation; #[test] diff --git a/translatable_proc/src/data/translations.rs b/translatable_proc/src/data/translations.rs index f8378e6..122559e 100644 --- a/translatable_proc/src/data/translations.rs +++ b/translatable_proc/src/data/translations.rs @@ -1,21 +1,19 @@ -use std::collections::HashMap; use std::fs::{read_dir, read_to_string}; use std::sync::OnceLock; -use thiserror::Error; -use toml::{Table, Value}; +use toml::Table; -use translatable_shared::{Language, NestingType}; +use translatable_shared::TranslationNode; use super::config::{SeekMode, TranslationOverlap, load_config}; -use crate::translations::errors::TranslationError; +use crate::translations::errors::CompileTimeError; /// Translation association with its source file pub struct AssociatedTranslation { /// Original file path of the translation original_path: String, /// Hierarchical translation data - translation_table: NestingType, + translation_table: TranslationNode, } /// Global thread-safe cache for loaded translations @@ -28,7 +26,7 @@ static TRANSLATIONS: OnceLock> = OnceLock::new(); /// /// # Returns /// Vec of file paths or TranslationError -fn walk_dir(path: &str) -> Result, TranslationError> { +fn walk_dir(path: &str) -> Result, CompileTimeError> { let mut stack = vec![path.to_string()]; let mut result = Vec::new(); @@ -39,7 +37,7 @@ fn walk_dir(path: &str) -> Result, TranslationError> { for entry in directory { let path = entry.path(); if path.is_dir() { - stack.push(path.to_str().ok_or(TranslationError::InvalidUnicode)?.to_string()); + stack.push(path.to_str().ok_or(CompileTimeError::InvalidUnicode)?.to_string()); } else { result.push(path.to_string_lossy().to_string()); } @@ -58,7 +56,7 @@ fn walk_dir(path: &str) -> Result, TranslationError> { /// - Uses OnceLock for thread-safe initialization /// - Applies sorting based on configuration /// - Handles file parsing and validation -pub fn load_translations() -> Result<&'static Vec, TranslationError> { +pub fn load_translations() -> Result<&'static Vec, CompileTimeError> { if let Some(translations) = TRANSLATIONS.get() { return Ok(translations); } @@ -77,15 +75,15 @@ pub fn load_translations() -> Result<&'static Vec, Transl .map(|path| { let table = read_to_string(path)? .parse::
() - .map_err(|err| TranslationError::ParseToml(err, path.clone()))?; + .map_err(|err| CompileTimeError::ParseToml(err, path.clone()))?; Ok(AssociatedTranslation { original_path: path.to_string(), - translation_table: NestingType::try_from(table) - .map_err(|err| TranslationError::InvalidTomlFormat(err, path.to_string()))?, + translation_table: TranslationNode::try_from(table) + .map_err(|err| CompileTimeError::InvalidTomlFormat(err, path.to_string()))?, }) }) - .collect::, TranslationError>>()?; + .collect::, CompileTimeError>>()?; // Handle translation overlap configuration if let TranslationOverlap::Overwrite = config.overlap() { @@ -105,7 +103,7 @@ impl AssociatedTranslation { /// Gets reference to the translation data structure #[allow(unused)] - pub fn translation_table(&self) -> &NestingType { + pub fn translation_table(&self) -> &TranslationNode { &self.translation_table } } diff --git a/translatable_proc/src/translations/errors.rs b/translatable_proc/src/translations/errors.rs index 75852f7..ae8fe85 100644 --- a/translatable_proc/src/translations/errors.rs +++ b/translatable_proc/src/translations/errors.rs @@ -4,13 +4,13 @@ use syn::Error as SynError; use thiserror::Error; use toml::de::Error as TomlError; -use translatable_shared::{Language, TransformError}; +use translatable_shared::{Language, TranslationNodeError}; use crate::data::config::ConfigError; /// Errors that can occur during translation processing. #[derive(Error, Debug)] -pub enum TranslationError { +pub enum CompileTimeError { /// Configuration-related error #[error("{0:#}")] Config(#[from] ConfigError), @@ -59,7 +59,7 @@ pub enum TranslationError { /// Invalid TOML structure in specific file #[error("Invalid TOML structure in file {1}: {0}")] - InvalidTomlFormat(TransformError, String), + InvalidTomlFormat(TranslationNodeError, String), /// Path not found in any translation file #[error("The path '{0}' is not found in any of the translation files as a translation object.")] diff --git a/translatable_proc/src/translations/generation.rs b/translatable_proc/src/translations/generation.rs index d2c8323..244b172 100644 --- a/translatable_proc/src/translations/generation.rs +++ b/translatable_proc/src/translations/generation.rs @@ -6,7 +6,7 @@ use syn::{Expr, parse2}; use translatable_shared::Language; -use super::errors::TranslationError; +use super::errors::CompileTimeError; use crate::data::translations::load_translations; /// Generates compile-time string replacement logic for a single format @@ -88,9 +88,9 @@ fn kwarg_dynamic_replaces(format_kwargs: &HashMap) -> Vec Result { +pub fn load_lang_static(lang: &str) -> Result { lang.parse::() - .map_err(|_| TranslationError::InvalidLanguage(lang.to_string())) + .map_err(|_| CompileTimeError::InvalidLanguage(lang.to_string())) } /// Generates runtime validation for a dynamic language expression. @@ -101,7 +101,7 @@ pub fn load_lang_static(lang: &str) -> Result { /// /// # Returns /// TokenStream with code to validate language at runtime -pub fn load_lang_dynamic(lang: TokenStream) -> Result { +pub fn load_lang_dynamic(lang: TokenStream) -> Result { let lang: Expr = parse2(lang)?; // The `String` explicit type serves as @@ -114,8 +114,7 @@ pub fn load_lang_dynamic(lang: TokenStream) -> Result, path: String, format_kwargs: HashMap, -) -> Result { +) -> Result { let translation_object = load_translations()? .iter() .find_map(|association| association.translation_table().get_path(path.split('.').collect())) - .ok_or(TranslationError::PathNotFound(path.to_string()))?; + .ok_or(CompileTimeError::PathNotFound(path.to_string()))?; let replaces = kwarg_dynamic_replaces(&format_kwargs); Ok(match static_lang { Some(language) => { let translation = translation_object .get(&language) - .ok_or(TranslationError::LanguageNotAvailable(language, path))?; + .ok_or(CompileTimeError::LanguageNotAvailable(language, path))?; let static_replaces = format_kwargs .iter() @@ -192,7 +191,7 @@ pub fn load_translation_dynamic( static_lang: Option, path: TokenStream, format_kwargs: HashMap, -) -> Result { +) -> Result { let nestings = load_translations()? .iter() .map(|association| association.translation_table().clone().into()) diff --git a/translatable_shared/src/languages.rs b/translatable_shared/src/languages.rs index fd8d745..e16f2ce 100644 --- a/translatable_shared/src/languages.rs +++ b/translatable_shared/src/languages.rs @@ -437,6 +437,6 @@ impl Into for Language { fn into(self) -> TokenStream2 { let ident = Ident::new(&format!("{self:?}"), Span::call_site()); - quote! { translatable::Language::#ident } + quote! { translatable::shared::Language::#ident } } } diff --git a/translatable_shared/src/lib.rs b/translatable_shared/src/lib.rs index eacd56f..5d733b5 100644 --- a/translatable_shared/src/lib.rs +++ b/translatable_shared/src/lib.rs @@ -4,5 +4,5 @@ mod nesting_type; /// Export all the structures in the common /// top-level namespace -pub use crate::languages::{Language, Similarities}; -pub use crate::nesting_type::{NestingType, TransformError}; +pub use crate::languages::{Language, Similarities, LanguageIter}; +pub use crate::nesting_type::{TranslationNode, TranslationNodeError}; diff --git a/translatable_shared/src/nesting_type.rs b/translatable_shared/src/nesting_type.rs index f99e99d..05fd976 100644 --- a/translatable_shared/src/nesting_type.rs +++ b/translatable_shared/src/nesting_type.rs @@ -10,7 +10,7 @@ use crate::Language; /// Errors occurring during TOML-to-translation structure transformation #[derive(Error, Debug)] -pub enum TransformError { +pub enum TranslationNodeError { /// Mixed content found in nesting node (strings and objects cannot coexist) #[error("A nesting can contain either strings or other nestings, but not both.")] InvalidNesting, @@ -31,14 +31,14 @@ pub enum TransformError { /// Represents nested translation structure, /// as it is on the translation files. #[derive(Clone)] -pub enum NestingType { +pub enum TranslationNode { /// Nested namespace containing other translation objects - Object(HashMap), + Nesting(HashMap), /// Leaf node containing actual translations per language Translation(HashMap), } -impl NestingType { +impl TranslationNode { /// Resolves a translation path through the nesting hierarchy /// /// # Arguments @@ -48,7 +48,7 @@ impl NestingType { /// Reference to translations if path exists and points to leaf node pub fn get_path(&self, path: Vec<&str>) -> Option<&HashMap> { match self { - Self::Object(nested) => { + Self::Nesting(nested) => { let (first, rest) = path.split_first()?; nested.get(*first)?.get_path(rest.to_vec()) }, @@ -62,10 +62,10 @@ impl NestingType { /// /// This is exclusively meant to be used from the /// macro generation context. -impl Into for NestingType { - fn into(self) -> TokenStream2 { - match self { - Self::Object(nesting) => { +impl From for TokenStream2 { + fn from(val: TranslationNode) -> Self { + match val { + TranslationNode::Nesting(nesting) => { let mapped_nesting = nesting .into_iter() .map(|(key, value)| { @@ -75,7 +75,7 @@ impl Into for NestingType { .collect::>(); quote! {{ - translatable::NestingType::Object( + translatable::TranslationNode::Nesting( vec![#(#mapped_nesting),*] .into_iter() .collect::>() @@ -83,7 +83,7 @@ impl Into for NestingType { }} } - Self::Translation(translation) => { + TranslationNode::Translation(translation) => { let mapped_translation = translation .into_iter() .map(|(key, value)| { @@ -93,7 +93,7 @@ impl Into for NestingType { .collect::>(); quote! {{ - translatable::NestingType::Translation( + translatable::TranslationNode::Translation( vec![#(#mapped_translation),*] .into_iter() .collect::>() @@ -121,8 +121,8 @@ fn templates_valid(translation: &str) -> bool { /// This implementation converts a `toml::Table` into a manageable /// NestingType. -impl TryFrom
for NestingType { - type Error = TransformError; +impl TryFrom
for TranslationNode { + type Error = TranslationNodeError; /// Converts TOML table to validated translation structure fn try_from(value: Table) -> Result { @@ -137,29 +137,30 @@ impl TryFrom
for NestingType { match result { Self::Translation(translation) => { if !templates_valid(&translation_value) { - return Err(TransformError::UnclosedTemplate); + return Err(TranslationNodeError::UnclosedTemplate); } translation.insert(key.parse()?, translation_value); - }, - Self::Object(_) => return Err(TransformError::InvalidNesting), + } + + Self::Nesting(_) => return Err(TranslationNodeError::InvalidNesting), } }, Value::Table(nesting_value) => { - let result = result.get_or_insert_with(|| Self::Object(HashMap::new())); + let result = result.get_or_insert_with(|| Self::Nesting(HashMap::new())); match result { - Self::Object(nesting) => { + Self::Nesting(nesting) => { nesting.insert(key, Self::try_from(nesting_value)?); }, - Self::Translation(_) => return Err(TransformError::InvalidNesting), + Self::Translation(_) => return Err(TranslationNodeError::InvalidNesting), } }, - _ => return Err(TransformError::InvalidValue), + _ => return Err(TranslationNodeError::InvalidValue), } } - result.ok_or(TransformError::InvalidValue) + result.ok_or(TranslationNodeError::InvalidValue) } } From 83f767aba9f83255e308b7f55bb427f685391f55 Mon Sep 17 00:00:00 2001 From: stifskere Date: Mon, 7 Apr 2025 00:58:25 +0200 Subject: [PATCH 068/228] chore: created abstraction path for both macros --- translatable_proc/src/data/translations.rs | 2 +- translatable_proc/src/lib.rs | 2 +- .../errors.rs => macro_generation/compile_error.rs} | 0 translatable_proc/src/macro_generation/mod.rs | 2 ++ .../generation.rs => macro_generation/single.rs} | 4 ++-- translatable_proc/src/macros.rs | 4 +--- translatable_proc/src/translations/mod.rs | 2 -- translatable_shared/src/nesting_type.rs | 4 ++-- 8 files changed, 9 insertions(+), 11 deletions(-) rename translatable_proc/src/{translations/errors.rs => macro_generation/compile_error.rs} (100%) create mode 100644 translatable_proc/src/macro_generation/mod.rs rename translatable_proc/src/{translations/generation.rs => macro_generation/single.rs} (98%) delete mode 100644 translatable_proc/src/translations/mod.rs diff --git a/translatable_proc/src/data/translations.rs b/translatable_proc/src/data/translations.rs index 122559e..8a2ad8e 100644 --- a/translatable_proc/src/data/translations.rs +++ b/translatable_proc/src/data/translations.rs @@ -6,7 +6,7 @@ use toml::Table; use translatable_shared::TranslationNode; use super::config::{SeekMode, TranslationOverlap, load_config}; -use crate::translations::errors::CompileTimeError; +use crate::translations::compile_error::CompileTimeError; /// Translation association with its source file pub struct AssociatedTranslation { diff --git a/translatable_proc/src/lib.rs b/translatable_proc/src/lib.rs index 2410d5d..9cdfeee 100644 --- a/translatable_proc/src/lib.rs +++ b/translatable_proc/src/lib.rs @@ -13,7 +13,7 @@ use syn::parse_macro_input; mod data; mod macros; -mod translations; +mod macro_generation; /// Procedural macro for compile-time translation validation /// diff --git a/translatable_proc/src/translations/errors.rs b/translatable_proc/src/macro_generation/compile_error.rs similarity index 100% rename from translatable_proc/src/translations/errors.rs rename to translatable_proc/src/macro_generation/compile_error.rs diff --git a/translatable_proc/src/macro_generation/mod.rs b/translatable_proc/src/macro_generation/mod.rs new file mode 100644 index 0000000..b647e3b --- /dev/null +++ b/translatable_proc/src/macro_generation/mod.rs @@ -0,0 +1,2 @@ +pub mod compile_error; +pub mod single; diff --git a/translatable_proc/src/translations/generation.rs b/translatable_proc/src/macro_generation/single.rs similarity index 98% rename from translatable_proc/src/translations/generation.rs rename to translatable_proc/src/macro_generation/single.rs index 244b172..d835473 100644 --- a/translatable_proc/src/translations/generation.rs +++ b/translatable_proc/src/macro_generation/single.rs @@ -6,7 +6,7 @@ use syn::{Expr, parse2}; use translatable_shared::Language; -use super::errors::CompileTimeError; +use super::compile_error::CompileTimeError; use crate::data::translations::load_translations; /// Generates compile-time string replacement logic for a single format @@ -115,7 +115,7 @@ pub fn load_lang_dynamic(lang: TokenStream) -> Result for TokenStream2 { .collect::>(); quote! {{ - translatable::TranslationNode::Nesting( + translatable::shared::TranslationNode::Nesting( vec![#(#mapped_nesting),*] .into_iter() .collect::>() @@ -93,7 +93,7 @@ impl From for TokenStream2 { .collect::>(); quote! {{ - translatable::TranslationNode::Translation( + translatable::shared::TranslationNode::Translation( vec![#(#mapped_translation),*] .into_iter() .collect::>() From 1d08dd811d02a1b342dec6f7a31ab1ad4f86c213 Mon Sep 17 00:00:00 2001 From: stifskere Date: Mon, 7 Apr 2025 01:11:36 +0200 Subject: [PATCH 069/228] chore: change import layout to render inputs in a new line when too large --- rustfmt.toml | 1 + translatable/src/lib.rs | 6 ++---- translatable_proc/src/data/translations.rs | 4 +--- translatable_proc/src/lib.rs | 2 +- .../src/macro_generation/compile_error.rs | 1 - .../src/macro_generation/single.rs | 4 +--- translatable_proc/src/macros.rs | 17 +++++++++++++++-- translatable_shared/src/lib.rs | 3 +-- translatable_shared/src/nesting_type.rs | 8 ++++---- 9 files changed, 26 insertions(+), 20 deletions(-) diff --git a/rustfmt.toml b/rustfmt.toml index 4d191db..4951559 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -17,6 +17,7 @@ imports_granularity = "Module" group_imports = "StdExternalCrate" reorder_imports = true reorder_modules = true +imports_layout = "HorizontalVertical" # Wrapping & line breaking wrap_comments = true diff --git a/translatable/src/lib.rs b/translatable/src/lib.rs index 8d2c02a..7ffe0cc 100644 --- a/translatable/src/lib.rs +++ b/translatable/src/lib.rs @@ -2,7 +2,6 @@ mod error; // Export the private error module pub use error::RuntimeError as Error; - /// Re-export the procedural macro for crate users pub use translatable_proc::translation; @@ -10,8 +9,7 @@ pub use translatable_proc::translation; /// that should not but could be used by the users /// of the library pub mod shared { - /// Re-export utils used for both runtime and compile-time - pub use translatable_shared::{Language, TranslationNode, LanguageIter}; - pub use strum::IntoEnumIterator; + /// Re-export utils used for both runtime and compile-time + pub use translatable_shared::{Language, LanguageIter, TranslationNode}; } diff --git a/translatable_proc/src/data/translations.rs b/translatable_proc/src/data/translations.rs index 8a2ad8e..5bf4cbe 100644 --- a/translatable_proc/src/data/translations.rs +++ b/translatable_proc/src/data/translations.rs @@ -2,11 +2,10 @@ use std::fs::{read_dir, read_to_string}; use std::sync::OnceLock; use toml::Table; - use translatable_shared::TranslationNode; use super::config::{SeekMode, TranslationOverlap, load_config}; -use crate::translations::compile_error::CompileTimeError; +use crate::macro_generation::compile_error::CompileTimeError; /// Translation association with its source file pub struct AssociatedTranslation { @@ -93,7 +92,6 @@ pub fn load_translations() -> Result<&'static Vec, Compil Ok(TRANSLATIONS.get_or_init(|| translations)) } - impl AssociatedTranslation { /// Gets the original file path of the translation #[allow(unused)] diff --git a/translatable_proc/src/lib.rs b/translatable_proc/src/lib.rs index 9cdfeee..c989e02 100644 --- a/translatable_proc/src/lib.rs +++ b/translatable_proc/src/lib.rs @@ -12,8 +12,8 @@ use proc_macro::TokenStream; use syn::parse_macro_input; mod data; -mod macros; mod macro_generation; +mod macros; /// Procedural macro for compile-time translation validation /// diff --git a/translatable_proc/src/macro_generation/compile_error.rs b/translatable_proc/src/macro_generation/compile_error.rs index ae8fe85..93f6152 100644 --- a/translatable_proc/src/macro_generation/compile_error.rs +++ b/translatable_proc/src/macro_generation/compile_error.rs @@ -3,7 +3,6 @@ use std::io::Error as IoError; use syn::Error as SynError; use thiserror::Error; use toml::de::Error as TomlError; - use translatable_shared::{Language, TranslationNodeError}; use crate::data::config::ConfigError; diff --git a/translatable_proc/src/macro_generation/single.rs b/translatable_proc/src/macro_generation/single.rs index d835473..fde02d7 100644 --- a/translatable_proc/src/macro_generation/single.rs +++ b/translatable_proc/src/macro_generation/single.rs @@ -3,7 +3,6 @@ use std::collections::HashMap; use proc_macro2::TokenStream; use quote::quote; use syn::{Expr, parse2}; - use translatable_shared::Language; use super::compile_error::CompileTimeError; @@ -89,8 +88,7 @@ fn kwarg_dynamic_replaces(format_kwargs: &HashMap) -> Vec Result { - lang.parse::() - .map_err(|_| CompileTimeError::InvalidLanguage(lang.to_string())) + lang.parse::().map_err(|_| CompileTimeError::InvalidLanguage(lang.to_string())) } /// Generates runtime validation for a dynamic language expression. diff --git a/translatable_proc/src/macros.rs b/translatable_proc/src/macros.rs index eebfb8c..a8791a5 100644 --- a/translatable_proc/src/macros.rs +++ b/translatable_proc/src/macros.rs @@ -7,11 +7,24 @@ use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; use syn::token::Static; use syn::{ - Expr, ExprLit, ExprPath, Ident, Lit, MetaNameValue, Path, Result as SynResult, Token, + Expr, + ExprLit, + ExprPath, + Ident, + Lit, + MetaNameValue, + Path, + Result as SynResult, + Token, parse_quote, }; -use crate::macro_generation::single::{load_lang_dynamic, load_lang_static, load_translation_dynamic, load_translation_static}; +use crate::macro_generation::single::{ + load_lang_dynamic, + load_lang_static, + load_translation_dynamic, + load_translation_static, +}; /// Represents raw input arguments for the translation macro /// diff --git a/translatable_shared/src/lib.rs b/translatable_shared/src/lib.rs index 5d733b5..65d94a1 100644 --- a/translatable_shared/src/lib.rs +++ b/translatable_shared/src/lib.rs @@ -1,8 +1,7 @@ - mod languages; mod nesting_type; /// Export all the structures in the common /// top-level namespace -pub use crate::languages::{Language, Similarities, LanguageIter}; +pub use crate::languages::{Language, LanguageIter, Similarities}; pub use crate::nesting_type::{TranslationNode, TranslationNodeError}; diff --git a/translatable_shared/src/nesting_type.rs b/translatable_shared/src/nesting_type.rs index fa753bc..37dcc1c 100644 --- a/translatable_shared/src/nesting_type.rs +++ b/translatable_shared/src/nesting_type.rs @@ -2,9 +2,9 @@ use std::collections::HashMap; use proc_macro2::TokenStream as TokenStream2; use quote::quote; +use strum::ParseError; use thiserror::Error; use toml::{Table, Value}; -use strum::ParseError; use crate::Language; @@ -81,7 +81,7 @@ impl From for TokenStream2 { .collect::>() ) }} - } + }, TranslationNode::Translation(translation) => { let mapped_translation = translation @@ -99,7 +99,7 @@ impl From for TokenStream2 { .collect::>() ) }} - } + }, } } } @@ -140,7 +140,7 @@ impl TryFrom
for TranslationNode { return Err(TranslationNodeError::UnclosedTemplate); } translation.insert(key.parse()?, translation_value); - } + }, Self::Nesting(_) => return Err(TranslationNodeError::InvalidNesting), } From 046e982f9faa307443f52aa4c9ded9c8e4807500 Mon Sep 17 00:00:00 2001 From: stifskere Date: Tue, 8 Apr 2025 07:55:18 +0200 Subject: [PATCH 070/228] feat: separated macro parsing and generation logic && formated with cargo fmt --- translatable_proc/src/lib.rs | 5 +- translatable_proc/src/macro_generation/mod.rs | 2 +- .../{single.rs => translation.rs} | 0 .../src/macro_input/input_type.rs | 24 ++++ translatable_proc/src/macro_input/mod.rs | 2 + .../src/macro_input/translation.rs | 105 ++++++++++++++++++ .../src/{macros.rs => macro_parsing_old.rs} | 2 +- 7 files changed, 136 insertions(+), 4 deletions(-) rename translatable_proc/src/macro_generation/{single.rs => translation.rs} (100%) create mode 100644 translatable_proc/src/macro_input/input_type.rs create mode 100644 translatable_proc/src/macro_input/mod.rs create mode 100644 translatable_proc/src/macro_input/translation.rs rename translatable_proc/src/{macros.rs => macro_parsing_old.rs} (99%) diff --git a/translatable_proc/src/lib.rs b/translatable_proc/src/lib.rs index c989e02..840131b 100644 --- a/translatable_proc/src/lib.rs +++ b/translatable_proc/src/lib.rs @@ -7,13 +7,14 @@ //! - Configurable loading strategies //! - Procedural macro for compile-time checking -use macros::{RawMacroArgs, translation_macro}; +use macro_parsing_old::{RawMacroArgs, translation_macro}; use proc_macro::TokenStream; use syn::parse_macro_input; mod data; mod macro_generation; -mod macros; +mod macro_input; +mod macro_parsing_old; /// Procedural macro for compile-time translation validation /// diff --git a/translatable_proc/src/macro_generation/mod.rs b/translatable_proc/src/macro_generation/mod.rs index b647e3b..d9d3468 100644 --- a/translatable_proc/src/macro_generation/mod.rs +++ b/translatable_proc/src/macro_generation/mod.rs @@ -1,2 +1,2 @@ pub mod compile_error; -pub mod single; +pub mod translation; diff --git a/translatable_proc/src/macro_generation/single.rs b/translatable_proc/src/macro_generation/translation.rs similarity index 100% rename from translatable_proc/src/macro_generation/single.rs rename to translatable_proc/src/macro_generation/translation.rs diff --git a/translatable_proc/src/macro_input/input_type.rs b/translatable_proc/src/macro_input/input_type.rs new file mode 100644 index 0000000..b022d9b --- /dev/null +++ b/translatable_proc/src/macro_input/input_type.rs @@ -0,0 +1,24 @@ +use proc_macro2::TokenStream as TokenStream2; + +/// This enum abstracts (in the programming sense) +/// the logic on separating between what's considered +/// dynamic and static while parsing the abstract +/// (in the conceptual sense) macro input. +pub enum InputType { + Static(T), + Dynamic(TokenStream2), +} + +impl> InputType { + /// This method allows converting the + /// enum value whether it's conceptually + /// dynamic or static into its dynamic + /// represented as a `TokenStream` + #[allow(unused)] + fn dynamic(self) -> TokenStream2 { + match self { + Self::Static(value) => value.into(), + Self::Dynamic(value) => value, + } + } +} diff --git a/translatable_proc/src/macro_input/mod.rs b/translatable_proc/src/macro_input/mod.rs new file mode 100644 index 0000000..1725820 --- /dev/null +++ b/translatable_proc/src/macro_input/mod.rs @@ -0,0 +1,2 @@ +pub mod input_type; +pub mod translation; diff --git a/translatable_proc/src/macro_input/translation.rs b/translatable_proc/src/macro_input/translation.rs new file mode 100644 index 0000000..39a2159 --- /dev/null +++ b/translatable_proc/src/macro_input/translation.rs @@ -0,0 +1,105 @@ +use std::collections::HashMap; + +use proc_macro2::TokenStream as TokenStream2; +use quote::ToTokens; +use syn::token::Static; +use syn::{parse2, Error as SynError, LitStr, Path, PathArguments, Result as SynResult, Token, Ident}; +use syn::parse::{Parse, ParseStream}; +use translatable_shared::Language; + +use super::input_type::InputType; + +/// The `TranslationMacroArgs` struct is used to represent +/// the `translation!` macro parsed arguments, it's sole +/// purpose is to be used with `parse_macro_input!` with the +/// `Parse` implementation the structure has. +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 + /// `TokenStream`. + 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, +} + +/// The implementation is used to achieve the +/// sole purpose this structure has, which is being +/// used in a `parse_macro_input!` call. +impl Parse for TranslationMacroArgs { + fn parse(input: ParseStream) -> SynResult { + let language_arg = input.parse::()?; + let parsed_langauge_arg = match parse2::(language_arg.clone()) { + Ok(literal) => match literal.value().parse::() { + Ok(language) => InputType::Static(language), + + Err(_) => Err(SynError::new_spanned( + literal, + "This literal is an invalid ISO 639-1 string and cannot be parsed." + ))? + }, + + Err(_) => InputType::Dynamic(language_arg) + }; + + input.parse::()?; + + let next_token = input.parse::()?; + let parsed_path_arg = match parse2::(next_token.clone()) { + Ok(_) => { + let language_arg = input.parse::()? + .segments + .into_iter() + .map(|segment| match segment.arguments { + PathArguments::None => Ok(segment.ident.to_string()), + + other => Err(SynError::new_spanned( + other, + "A translation doesn't have generic arguments" + )), + }) + .collect::, _>>()?; + + InputType::Static(language_arg) + } + + Err(_) => InputType::Dynamic(next_token) + }; + + let mut replacements = HashMap::new(); + if input.parse::().is_ok() { + while !input.is_empty() { + let key = input.parse::()?; + let value = match input.parse::() { + Ok(_) => input.parse::()?, + + Err(_) => key + .clone() + .into_token_stream() + }; + + replacements.insert(key.to_string(), value); + } + } + + Ok(Self { + language: parsed_langauge_arg, + path: parsed_path_arg, + replacements + }) + } +} diff --git a/translatable_proc/src/macros.rs b/translatable_proc/src/macro_parsing_old.rs similarity index 99% rename from translatable_proc/src/macros.rs rename to translatable_proc/src/macro_parsing_old.rs index a8791a5..ded392f 100644 --- a/translatable_proc/src/macros.rs +++ b/translatable_proc/src/macro_parsing_old.rs @@ -19,7 +19,7 @@ use syn::{ parse_quote, }; -use crate::macro_generation::single::{ +use crate::macro_generation::translation::{ load_lang_dynamic, load_lang_static, load_translation_dynamic, From 3d7f79c160671b5855321d941eda4a8161e3dfdf Mon Sep 17 00:00:00 2001 From: stifskere Date: Tue, 8 Apr 2025 08:10:34 +0200 Subject: [PATCH 071/228] feat: implemented thiserror to parse `translatable!` macro input in a private maneer --- .../src/macro_input/translation.rs | 44 +++++++++++++++---- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/translatable_proc/src/macro_input/translation.rs b/translatable_proc/src/macro_input/translation.rs index 39a2159..ebfcecb 100644 --- a/translatable_proc/src/macro_input/translation.rs +++ b/translatable_proc/src/macro_input/translation.rs @@ -5,15 +5,35 @@ use quote::ToTokens; use syn::token::Static; use syn::{parse2, Error as SynError, LitStr, Path, PathArguments, Result as SynResult, Token, Ident}; use syn::parse::{Parse, ParseStream}; +use thiserror::Error; use translatable_shared::Language; use super::input_type::InputType; +/// Used to represent errors on parsing a `TranslationMacroArgs` +/// using `parse_macro_input!`. +/// +/// The enum implements a helper function to convert itself +/// to a `syn` spanned error, so this enum isn't directly +/// exposed as the `syn::Error` instance takes place. +#[derive(Error, Debug)] +enum TranslationMacroArgsError { + /// 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), + + /// Extra tokens were found while parsing a static path for + /// the `translation!` macro, specifically generic arguments. + #[error("This translation path contains generic arguments, and cannot be parsed")] + InvalidPathContainsGenerics +} + /// The `TranslationMacroArgs` struct is used to represent /// the `translation!` macro parsed arguments, it's sole /// purpose is to be used with `parse_macro_input!` with the /// `Parse` implementation the structure has. -struct TranslationMacroArgs { +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 @@ -37,6 +57,12 @@ struct TranslationMacroArgs { replacements: HashMap, } +impl TranslationMacroArgsError { + pub fn into_syn_error(self, span: T) -> SynError { + SynError::new_spanned(span, self.to_string()) + } +} + /// The implementation is used to achieve the /// sole purpose this structure has, which is being /// used in a `parse_macro_input!` call. @@ -47,10 +73,10 @@ impl Parse for TranslationMacroArgs { Ok(literal) => match literal.value().parse::() { Ok(language) => InputType::Static(language), - Err(_) => Err(SynError::new_spanned( - literal, - "This literal is an invalid ISO 639-1 string and cannot be parsed." - ))? + Err(_) => Err( + TranslationMacroArgsError::InvalidIsoLiteral(literal.value()) + .into_syn_error(literal) + )? }, Err(_) => InputType::Dynamic(language_arg) @@ -67,10 +93,10 @@ impl Parse for TranslationMacroArgs { .map(|segment| match segment.arguments { PathArguments::None => Ok(segment.ident.to_string()), - other => Err(SynError::new_spanned( - other, - "A translation doesn't have generic arguments" - )), + other => Err( + TranslationMacroArgsError::InvalidPathContainsGenerics + .into_syn_error(other) + ), }) .collect::, _>>()?; From 4df850bc70a7b89477d19707c3f08bfc7e264ed7 Mon Sep 17 00:00:00 2001 From: stifskere Date: Wed, 9 Apr 2025 14:18:41 +0200 Subject: [PATCH 072/228] feat: refactored translation loading --- translatable_proc/src/data/translations.rs | 102 ++++--- .../src/macro_generation/compile_error.rs | 74 ----- translatable_proc/src/macro_generation/mod.rs | 1 - .../src/macro_generation/translation.rs | 255 ------------------ .../src/macro_input/translation.rs | 42 +-- translatable_shared/src/lib.rs | 10 +- .../src/translations/collection.rs | 52 ++++ translatable_shared/src/translations/mod.rs | 2 + .../{nesting_type.rs => translations/node.rs} | 11 +- 9 files changed, 151 insertions(+), 398 deletions(-) delete mode 100644 translatable_proc/src/macro_generation/compile_error.rs create mode 100644 translatable_shared/src/translations/collection.rs create mode 100644 translatable_shared/src/translations/mod.rs rename translatable_shared/src/{nesting_type.rs => translations/node.rs} (94%) diff --git a/translatable_proc/src/data/translations.rs b/translatable_proc/src/data/translations.rs index 5bf4cbe..ae6c586 100644 --- a/translatable_proc/src/data/translations.rs +++ b/translatable_proc/src/data/translations.rs @@ -1,22 +1,59 @@ use std::fs::{read_dir, read_to_string}; +use std::io::Error as IoError; use std::sync::OnceLock; +use thiserror::Error; use toml::Table; -use translatable_shared::TranslationNode; - -use super::config::{SeekMode, TranslationOverlap, load_config}; -use crate::macro_generation::compile_error::CompileTimeError; - -/// Translation association with its source file -pub struct AssociatedTranslation { - /// Original file path of the translation - original_path: String, - /// Hierarchical translation data - translation_table: TranslationNode, +use toml::de::Error as TomlError; +use translatable_shared::{TranslationNode, TranslationNodeCollection, TranslationNodeError}; + +use super::config::{ConfigError, SeekMode, TranslationOverlap, load_config}; + +/// Contains error definitions for what may go wrong while +/// loading a translation. +#[derive(Error, Debug)] +pub enum TranslationDataError { + /// Represents a generic IO error, if a file couldn't + /// be opened, a path could not be found... This error + /// will be inferred from an `std::io::Error`. + #[error("There was a problem with an IO operation: {0:#}")] + SystemIo(#[from] IoError), + + /// Used to convert any configuration loading error + /// into a `TranslationDataError`, error messages are + /// handled by the `ConfigError` itself. + #[error("{0:#}")] + LoadConfig(#[from] ConfigError), + + /// Specific conversion error for when a path can't be converted + /// to a manipulable string because it contains invalid + /// unicode characters. + #[error("Couldn't open path, found invalid unicode characters")] + InvalidUnicode, + + /// Represents a TOML deserialization error, this happens while + /// loading files and converting their content to TOML. + /// + /// # Arguments + /// * `.0` - The RAW toml::de::Error returned by the deserialization + /// function. + /// * `.1` - The path where the file was originally found. + #[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), + + #[error("{0:#}")] + Node(#[from] TranslationNodeError), } /// Global thread-safe cache for loaded translations -static TRANSLATIONS: OnceLock> = OnceLock::new(); +static TRANSLATIONS: OnceLock = OnceLock::new(); /// Recursively walks directory to find all translation files /// @@ -25,7 +62,7 @@ static TRANSLATIONS: OnceLock> = OnceLock::new(); /// /// # Returns /// Vec of file paths or TranslationError -fn walk_dir(path: &str) -> Result, CompileTimeError> { +fn walk_dir(path: &str) -> Result, TranslationDataError> { let mut stack = vec![path.to_string()]; let mut result = Vec::new(); @@ -36,7 +73,7 @@ fn walk_dir(path: &str) -> Result, CompileTimeError> { for entry in directory { let path = entry.path(); if path.is_dir() { - stack.push(path.to_str().ok_or(CompileTimeError::InvalidUnicode)?.to_string()); + stack.push(path.to_str().ok_or(TranslationDataError::InvalidUnicode)?.to_string()); } else { result.push(path.to_string_lossy().to_string()); } @@ -55,7 +92,7 @@ fn walk_dir(path: &str) -> Result, CompileTimeError> { /// - Uses OnceLock for thread-safe initialization /// - Applies sorting based on configuration /// - Handles file parsing and validation -pub fn load_translations() -> Result<&'static Vec, CompileTimeError> { +pub fn load_translations() -> Result<&'static TranslationNodeCollection, TranslationDataError> { if let Some(translations) = TRANSLATIONS.get() { return Ok(translations); } @@ -65,43 +102,22 @@ pub fn load_translations() -> Result<&'static Vec, Compil // Apply sorting based on configuration translation_paths.sort_by_key(|path| path.to_lowercase()); - if let SeekMode::Unalphabetical = config.seek_mode() { + if matches!(config.seek_mode(), SeekMode::Unalphabetical) + || matches!(config.overlap(), TranslationOverlap::Overwrite) + { translation_paths.reverse(); } - let mut translations = translation_paths + let translations = translation_paths .iter() .map(|path| { let table = read_to_string(path)? .parse::
() - .map_err(|err| CompileTimeError::ParseToml(err, path.clone()))?; + .map_err(|err| TranslationDataError::ParseToml(err, path.clone()))?; - Ok(AssociatedTranslation { - original_path: path.to_string(), - translation_table: TranslationNode::try_from(table) - .map_err(|err| CompileTimeError::InvalidTomlFormat(err, path.to_string()))?, - }) + Ok((path.clone(), TranslationNode::try_from(table)?)) }) - .collect::, CompileTimeError>>()?; - - // Handle translation overlap configuration - if let TranslationOverlap::Overwrite = config.overlap() { - translations.reverse(); - } + .collect::>()?; Ok(TRANSLATIONS.get_or_init(|| translations)) } - -impl AssociatedTranslation { - /// Gets the original file path of the translation - #[allow(unused)] - pub fn original_path(&self) -> &str { - &self.original_path - } - - /// Gets reference to the translation data structure - #[allow(unused)] - pub fn translation_table(&self) -> &TranslationNode { - &self.translation_table - } -} diff --git a/translatable_proc/src/macro_generation/compile_error.rs b/translatable_proc/src/macro_generation/compile_error.rs deleted file mode 100644 index 93f6152..0000000 --- a/translatable_proc/src/macro_generation/compile_error.rs +++ /dev/null @@ -1,74 +0,0 @@ -use std::io::Error as IoError; - -use syn::Error as SynError; -use thiserror::Error; -use toml::de::Error as TomlError; -use translatable_shared::{Language, TranslationNodeError}; - -use crate::data::config::ConfigError; - -/// Errors that can occur during translation processing. -#[derive(Error, Debug)] -pub enum CompileTimeError { - /// Configuration-related error - #[error("{0:#}")] - Config(#[from] ConfigError), - - /// IO operation error - #[error("An IO Error occurred: {0:#}")] - Io(#[from] IoError), - - /// Path contains invalid Unicode characters - #[error("The path contains invalid unicode characters.")] - InvalidUnicode, - - /// TOML parsing error with location information - #[error( - "Toml parse error '{}'{}", - .0.message(), - .0.span() - .map(|l| format!(" in {}:{}:{}", .1, l.start, l.end)) - .unwrap_or("".into()) - )] - ParseToml(TomlError, String), - - /// Invalid language code error with suggestions - #[error( - "'{0}' is not valid ISO 639-1. {similarities}", - similarities = { - let similarities = Language::get_similarities(.0, 10); - let similarities_format = similarities - .similarities() - .join("\n"); - - if similarities_format.is_empty() { - "".into() - } else { - let including_format = format!("These are some valid languages including '{}':\n{similarities_format}", .0); - - if similarities.overflow_by() > 0 { - format!("{including_format}\n... and {} more.", similarities.overflow_by()) - } else { - including_format - } - } - } - )] - InvalidLanguage(String), - - /// Invalid TOML structure in specific file - #[error("Invalid TOML structure in file {1}: {0}")] - InvalidTomlFormat(TranslationNodeError, String), - - /// Path not found in any translation file - #[error("The path '{0}' is not found in any of the translation files as a translation object.")] - PathNotFound(String), - - /// Language not available for the specified path - #[error("The language '{0:?}' ({0:#}) is not available for the '{1}' translation.")] - LanguageNotAvailable(Language, String), - - /// Error parsing macro. - #[error("Error parsing macro.")] - MacroError(#[from] SynError), -} diff --git a/translatable_proc/src/macro_generation/mod.rs b/translatable_proc/src/macro_generation/mod.rs index d9d3468..f5648e8 100644 --- a/translatable_proc/src/macro_generation/mod.rs +++ b/translatable_proc/src/macro_generation/mod.rs @@ -1,2 +1 @@ -pub mod compile_error; pub mod translation; diff --git a/translatable_proc/src/macro_generation/translation.rs b/translatable_proc/src/macro_generation/translation.rs index fde02d7..8b13789 100644 --- a/translatable_proc/src/macro_generation/translation.rs +++ b/translatable_proc/src/macro_generation/translation.rs @@ -1,256 +1 @@ -use std::collections::HashMap; -use proc_macro2::TokenStream; -use quote::quote; -use syn::{Expr, parse2}; -use translatable_shared::Language; - -use super::compile_error::CompileTimeError; -use crate::data::translations::load_translations; - -/// Generates compile-time string replacement logic for a single format -/// argument. -/// -/// Implements a three-step replacement strategy to safely handle nested -/// templates: -/// 1. Temporarily replace `{{key}}` with `\x01{key}\x01` to protect wrapper -/// braces -/// 2. Replace `{key}` with the provided value -/// 3. Restore original `{key}` syntax from temporary markers -/// -/// # Arguments -/// * `key` - Template placeholder name (without braces) -/// * `value` - Expression to substitute, must implement `std::fmt::Display` -/// -/// # Example -/// For key = "name" and value = `user.first_name`: -/// ```rust -/// let template = "{{name}} is a user"; -/// -/// template -/// .replace("{{name}}", "\x01{name}\x01") -/// .replace("{name}", &format!("{:#}", "Juan")) -/// .replace("\x01{name}\x01", "{name}"); -/// ``` -fn kwarg_static_replaces(key: &str, value: &TokenStream) -> TokenStream { - quote! { - .replace( - format!("{{{{{}}}}}", #key).as_str(), // Replace {{key}} -> a temporary placeholder - format!("\x01{{{}}}\x01", #key).as_str() - ) - .replace( - format!("{{{}}}", #key).as_str(), // Replace {key} -> value - format!("{:#}", #value).as_str() - ) - .replace( - format!("\x01{{{}}}\x01", #key).as_str(), // Restore {key} from the placeholder - format!("{{{}}}", #key).as_str() - ) - } -} - -/// Generates runtime-safe template substitution chain for multiple format -/// arguments. -/// -/// Creates an iterator of chained replacement operations that will be applied -/// sequentially at runtime while preserving nested template syntax. -/// -/// # Arguments -/// * `format_kwargs` - Key/value pairs where: -/// - Key: Template placeholder name -/// - Value: Runtime expression implementing `Display` -/// -/// # Note -/// The replacement order is important to prevent accidental substitution in -/// nested templates. All replacements are wrapped in `Option::map` to handle -/// potential `None` values from translation lookup. -fn kwarg_dynamic_replaces(format_kwargs: &HashMap) -> Vec { - format_kwargs - .iter() - .map(|(key, value)| { - let static_replaces = kwarg_static_replaces(key, value); - quote! { - .map(|translation| translation - #static_replaces - ) - } - }) - .collect::>() -} - -/// Parses a static language string into an Iso639a enum instance with -/// compile-time validation. -/// -/// # Arguments -/// * `lang` - A string slice representing the language code to parse -/// -/// # Returns -/// - `Ok(Iso639a)` if valid language code -/// - `Err(TranslationError)` if parsing fails -pub fn load_lang_static(lang: &str) -> Result { - lang.parse::().map_err(|_| CompileTimeError::InvalidLanguage(lang.to_string())) -} - -/// Generates runtime validation for a dynamic language expression. -/// -/// # Arguments -/// * `lang` - TokenStream representing an expression that implements -/// `Into` -/// -/// # Returns -/// TokenStream with code to validate language at runtime -pub fn load_lang_dynamic(lang: TokenStream) -> Result { - let lang: Expr = parse2(lang)?; - - // The `String` explicit type serves as - // expression type checking, we accept `impl Into` - // for any expression that's not static. - Ok(quote! { - #[doc(hidden)] - let language: String = (#lang).into(); - #[doc(hidden)] - let language = language.to_lowercase(); - - #[doc(hidden)] - let valid_lang = translatable::shared::Language::iter() - .any(|lang| lang == language); - }) -} - -/// Loads translations for static language resolution -/// -/// # Arguments -/// * `static_lang` - Optional predefined language -/// * `path` - Translation key path as dot-separated string -/// -/// # Returns -/// TokenStream with either direct translation or language lookup logic -pub fn load_translation_static( - static_lang: Option, - path: String, - format_kwargs: HashMap, -) -> Result { - let translation_object = load_translations()? - .iter() - .find_map(|association| association.translation_table().get_path(path.split('.').collect())) - .ok_or(CompileTimeError::PathNotFound(path.to_string()))?; - let replaces = kwarg_dynamic_replaces(&format_kwargs); - - Ok(match static_lang { - Some(language) => { - let translation = translation_object - .get(&language) - .ok_or(CompileTimeError::LanguageNotAvailable(language, path))?; - - let static_replaces = format_kwargs - .iter() - .map(|(key, value)| kwarg_static_replaces(key, value)) - .collect::>(); - - quote! {{ - #translation - #(#static_replaces)* - }} - }, - - None => { - let translation_object = translation_object.iter().map(|(key, value)| { - let key = format!("{key:?}").to_lowercase(); - quote! { (#key, #value) } - }); - - quote! {{ - if valid_lang { - vec![#(#translation_object),*] - .into_iter() - .collect::>() - .get(language.as_str()) - .ok_or(translatable::Error::LanguageNotAvailable(language, #path.to_string())) - .cloned() - .map(|translation| translation.to_string()) - #(#replaces)* - } else { - Err(translatable::Error::InvalidLanguage(language)) - } - }} - }, - }) -} - -/// Loads translations for dynamic language and path resolution -/// -/// # Arguments -/// * `static_lang` - Optional predefined language -/// * `path` - TokenStream representing dynamic path expression -/// -/// # Returns -/// TokenStream with runtime translation resolution logic -pub fn load_translation_dynamic( - static_lang: Option, - path: TokenStream, - format_kwargs: HashMap, -) -> Result { - let nestings = load_translations()? - .iter() - .map(|association| association.translation_table().clone().into()) - .collect::>(); - - let translation_quote = quote! { - #[doc(hidden)] - let path: String = #path.into(); - - #[doc(hidden)] - let nested_translations = vec![#(#nestings),*]; - - #[doc(hidden)] - let translation = nested_translations - .iter() - .find_map(|nesting| nesting.get_path( - path - .split('.') - .collect() - )); - }; - - let replaces = kwarg_dynamic_replaces(&format_kwargs); - - Ok(match static_lang { - Some(language) => { - let language = format!("{language:?}").to_lowercase(); - - quote! {{ - #translation_quote - - if let Some(translation) = translation { - translation - .get(#language) - .ok_or(translatable::Error::LanguageNotAvailable(#language.to_string(), path)) - .cloned() - #(#replaces)* - } else { - Err(translatable::Error::PathNotFound(path)) - } - }} - }, - - None => { - quote! {{ - #translation_quote - - if valid_lang { - if let Some(translation) = translation { - translation - .get(&language) - .ok_or(translatable::Error::LanguageNotAvailable(language, path)) - .cloned() - #(#replaces)* - } else { - Err(translatable::Error::PathNotFound(path)) - } - } else { - Err(translatable::Error::InvalidLanguage(language)) - } - }} - }, - }) -} diff --git a/translatable_proc/src/macro_input/translation.rs b/translatable_proc/src/macro_input/translation.rs index ebfcecb..249cbd8 100644 --- a/translatable_proc/src/macro_input/translation.rs +++ b/translatable_proc/src/macro_input/translation.rs @@ -2,9 +2,18 @@ use std::collections::HashMap; use proc_macro2::TokenStream as TokenStream2; use quote::ToTokens; -use syn::token::Static; -use syn::{parse2, Error as SynError, LitStr, Path, PathArguments, Result as SynResult, Token, Ident}; use syn::parse::{Parse, ParseStream}; +use syn::token::Static; +use syn::{ + Error as SynError, + Ident, + LitStr, + Path, + PathArguments, + Result as SynResult, + Token, + parse2, +}; use thiserror::Error; use translatable_shared::Language; @@ -26,7 +35,7 @@ enum TranslationMacroArgsError { /// Extra tokens were found while parsing a static path for /// the `translation!` macro, specifically generic arguments. #[error("This translation path contains generic arguments, and cannot be parsed")] - InvalidPathContainsGenerics + InvalidPathContainsGenerics, } /// The `TranslationMacroArgs` struct is used to represent @@ -73,13 +82,11 @@ impl Parse for TranslationMacroArgs { Ok(literal) => match literal.value().parse::() { Ok(language) => InputType::Static(language), - Err(_) => Err( - TranslationMacroArgsError::InvalidIsoLiteral(literal.value()) - .into_syn_error(literal) - )? + Err(_) => Err(TranslationMacroArgsError::InvalidIsoLiteral(literal.value()) + .into_syn_error(literal))?, }, - Err(_) => InputType::Dynamic(language_arg) + Err(_) => InputType::Dynamic(language_arg), }; input.parse::()?; @@ -87,23 +94,22 @@ impl Parse for TranslationMacroArgs { let next_token = input.parse::()?; let parsed_path_arg = match parse2::(next_token.clone()) { Ok(_) => { - let language_arg = input.parse::()? + let language_arg = input + .parse::()? .segments .into_iter() .map(|segment| match segment.arguments { PathArguments::None => Ok(segment.ident.to_string()), - other => Err( - TranslationMacroArgsError::InvalidPathContainsGenerics - .into_syn_error(other) - ), + other => Err(TranslationMacroArgsError::InvalidPathContainsGenerics + .into_syn_error(other)), }) .collect::, _>>()?; InputType::Static(language_arg) - } + }, - Err(_) => InputType::Dynamic(next_token) + Err(_) => InputType::Dynamic(next_token), }; let mut replacements = HashMap::new(); @@ -113,9 +119,7 @@ impl Parse for TranslationMacroArgs { let value = match input.parse::() { Ok(_) => input.parse::()?, - Err(_) => key - .clone() - .into_token_stream() + Err(_) => key.clone().into_token_stream(), }; replacements.insert(key.to_string(), value); @@ -125,7 +129,7 @@ impl Parse for TranslationMacroArgs { Ok(Self { language: parsed_langauge_arg, path: parsed_path_arg, - replacements + replacements, }) } } diff --git a/translatable_shared/src/lib.rs b/translatable_shared/src/lib.rs index 65d94a1..1acfaf4 100644 --- a/translatable_shared/src/lib.rs +++ b/translatable_shared/src/lib.rs @@ -1,7 +1,13 @@ mod languages; -mod nesting_type; +mod translations; /// Export all the structures in the common /// top-level namespace pub use crate::languages::{Language, LanguageIter, Similarities}; -pub use crate::nesting_type::{TranslationNode, TranslationNodeError}; +pub use crate::translations::collection::TranslationNodeCollection; +pub use crate::translations::node::{ + TranslationNesting, + TranslationNode, + TranslationNodeError, + TranslationObject, +}; diff --git a/translatable_shared/src/translations/collection.rs b/translatable_shared/src/translations/collection.rs new file mode 100644 index 0000000..e4d3ee8 --- /dev/null +++ b/translatable_shared/src/translations/collection.rs @@ -0,0 +1,52 @@ +use super::node::{TranslationNesting, TranslationObject}; +use crate::TranslationNode; + +/// Wraps a collection of translation nodes, these translation nodes +/// are usually directly loaded files, and the keys to access them +/// independently are the complete system path. The collection +/// permits searching for translations by iterating all the files +/// in the specified configuration order, so most likely you don't +/// need to seek for a translation independently. +pub struct TranslationNodeCollection(TranslationNesting); + +impl TranslationNodeCollection { + /// 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. + #[cold] + #[allow(unused)] + pub fn get_node(&self, path: &str) -> Option<&TranslationNode> { + self.0.get(&path.to_string()) + } + + /// 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: &[&str]) -> Option<&TranslationObject> { + self.0.values().find_map(|node| node.find_path(path)) + } +} + +/// 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()) + } +} diff --git a/translatable_shared/src/translations/mod.rs b/translatable_shared/src/translations/mod.rs new file mode 100644 index 0000000..38ce8ff --- /dev/null +++ b/translatable_shared/src/translations/mod.rs @@ -0,0 +1,2 @@ +pub mod collection; +pub mod node; diff --git a/translatable_shared/src/nesting_type.rs b/translatable_shared/src/translations/node.rs similarity index 94% rename from translatable_shared/src/nesting_type.rs rename to translatable_shared/src/translations/node.rs index 37dcc1c..dce962c 100644 --- a/translatable_shared/src/nesting_type.rs +++ b/translatable_shared/src/translations/node.rs @@ -28,14 +28,17 @@ pub enum TranslationNodeError { LanguageParsing(#[from] ParseError), } +pub type TranslationNesting = HashMap; +pub type TranslationObject = HashMap; + /// Represents nested translation structure, /// as it is on the translation files. #[derive(Clone)] pub enum TranslationNode { /// Nested namespace containing other translation objects - Nesting(HashMap), + Nesting(TranslationNesting), /// Leaf node containing actual translations per language - Translation(HashMap), + Translation(TranslationObject), } impl TranslationNode { @@ -46,11 +49,11 @@ impl TranslationNode { /// /// # Returns /// Reference to translations if path exists and points to leaf node - pub fn get_path(&self, path: Vec<&str>) -> Option<&HashMap> { + pub fn find_path(&self, path: &[&str]) -> Option<&TranslationObject> { match self { Self::Nesting(nested) => { let (first, rest) = path.split_first()?; - nested.get(*first)?.get_path(rest.to_vec()) + nested.get(*first)?.find_path(rest) }, Self::Translation(translation) => path.is_empty().then_some(translation), } From c66f9673b74547ddc339fe74a31716ca5233af01 Mon Sep 17 00:00:00 2001 From: stifskere Date: Wed, 9 Apr 2025 21:47:19 +0200 Subject: [PATCH 073/228] feat: changed Into implementations for ToTokens which has a blanket anyways --- translatable_shared/src/languages.rs | 8 +++---- .../src/translations/collection.rs | 2 +- translatable_shared/src/translations/node.rs | 24 +++++++++---------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/translatable_shared/src/languages.rs b/translatable_shared/src/languages.rs index e16f2ce..a7792dc 100644 --- a/translatable_shared/src/languages.rs +++ b/translatable_shared/src/languages.rs @@ -1,5 +1,5 @@ use proc_macro2::{Span, TokenStream as TokenStream2}; -use quote::quote; +use quote::{quote, ToTokens, TokenStreamExt}; use strum::{Display, EnumIter, EnumString, IntoEnumIterator}; use syn::Ident; @@ -433,10 +433,10 @@ impl PartialEq for Language { /// /// This is exclusively meant to be used from the /// macro generation context. -impl Into for Language { - fn into(self) -> TokenStream2 { +impl ToTokens for Language { + fn to_tokens(&self, tokens: &mut TokenStream2) { let ident = Ident::new(&format!("{self:?}"), Span::call_site()); - quote! { translatable::shared::Language::#ident } + tokens.append_all(quote! { translatable::shared::Language::#ident }) } } diff --git a/translatable_shared/src/translations/collection.rs b/translatable_shared/src/translations/collection.rs index e4d3ee8..43d5e38 100644 --- a/translatable_shared/src/translations/collection.rs +++ b/translatable_shared/src/translations/collection.rs @@ -38,7 +38,7 @@ impl TranslationNodeCollection { /// # Returns /// A translation object containing a specific translation /// in all it's available languages. - pub fn find_path(&self, path: &[&str]) -> Option<&TranslationObject> { + pub fn find_path(&self, path: &Vec) -> Option<&TranslationObject> { self.0.values().find_map(|node| node.find_path(path)) } } diff --git a/translatable_shared/src/translations/node.rs b/translatable_shared/src/translations/node.rs index dce962c..ce8207d 100644 --- a/translatable_shared/src/translations/node.rs +++ b/translatable_shared/src/translations/node.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use proc_macro2::TokenStream as TokenStream2; -use quote::quote; +use quote::{quote, ToTokens, TokenStreamExt}; use strum::ParseError; use thiserror::Error; use toml::{Table, Value}; @@ -49,11 +49,11 @@ impl TranslationNode { /// /// # Returns /// Reference to translations if path exists and points to leaf node - pub fn find_path(&self, path: &[&str]) -> Option<&TranslationObject> { + pub fn find_path(&self, path: &Vec) -> Option<&TranslationObject> { match self { Self::Nesting(nested) => { let (first, rest) = path.split_first()?; - nested.get(*first)?.find_path(rest) + nested.get(first)?.find_path(&rest.to_vec()) }, Self::Translation(translation) => path.is_empty().then_some(translation), } @@ -65,43 +65,43 @@ impl TranslationNode { /// /// This is exclusively meant to be used from the /// macro generation context. -impl From for TokenStream2 { - fn from(val: TranslationNode) -> Self { - match val { +impl ToTokens for TranslationNode { + fn to_tokens(&self, tokens: &mut TokenStream2) { + match self { TranslationNode::Nesting(nesting) => { let mapped_nesting = nesting .into_iter() .map(|(key, value)| { - let value: TokenStream2 = value.into(); + let value = value.to_token_stream(); quote! { (#key, #value) } }) .collect::>(); - quote! {{ + tokens.append_all(quote! {{ translatable::shared::TranslationNode::Nesting( vec![#(#mapped_nesting),*] .into_iter() .collect::>() ) - }} + }}); }, TranslationNode::Translation(translation) => { let mapped_translation = translation .into_iter() .map(|(key, value)| { - let key: TokenStream2 = key.into(); + let key = key.into_token_stream(); quote! { (#key, #value) } }) .collect::>(); - quote! {{ + tokens.append_all(quote! {{ translatable::shared::TranslationNode::Translation( vec![#(#mapped_translation),*] .into_iter() .collect::>() ) - }} + }}); }, } } From cef035f91accf5b5eaff13a4a484e930735298e6 Mon Sep 17 00:00:00 2001 From: stifskere Date: Thu, 10 Apr 2025 00:33:56 +0200 Subject: [PATCH 074/228] feat: macro generation refactor complete --- translatable/src/error.rs | 18 +- translatable/src/lib.rs | 8 +- translatable_proc/src/lib.rs | 9 +- .../src/macro_generation/translation.rs | 89 +++++++ .../src/macro_input/input_type.rs | 2 + .../src/macro_input/translation.rs | 20 ++ translatable_proc/src/macro_parsing_old.rs | 246 ------------------ translatable_proc/src/utils/collections.rs | 19 ++ translatable_proc/src/utils/errors.rs | 36 +++ translatable_proc/src/utils/mod.rs | 2 + translatable_shared/src/languages.rs | 2 +- .../src/translations/collection.rs | 30 ++- translatable_shared/src/translations/node.rs | 2 +- 13 files changed, 220 insertions(+), 263 deletions(-) delete mode 100644 translatable_proc/src/macro_parsing_old.rs create mode 100644 translatable_proc/src/utils/collections.rs create mode 100644 translatable_proc/src/utils/errors.rs create mode 100644 translatable_proc/src/utils/mod.rs diff --git a/translatable/src/error.rs b/translatable/src/error.rs index 3226590..d1abd70 100644 --- a/translatable/src/error.rs +++ b/translatable/src/error.rs @@ -1,4 +1,7 @@ +use std::collections::HashMap; + use thiserror::Error; +use translatable_shared::{Language, TranslationNodeError}; /// Error type for translation resolution failures /// @@ -6,17 +9,14 @@ use thiserror::Error; /// For static resolution failures, errors are reported at compile time. #[derive(Error, Debug)] pub enum RuntimeError { - /// Invalid ISO 639-1 language code provided - #[error("The language '{0}' is invalid.")] - InvalidLanguage(String), - - /// Translation exists but not available for specified language - #[error("The langauge '{0}' is not available for the path '{1}'")] - LanguageNotAvailable(String, String), + #[error("{0:#}")] + TranslationNode(#[from] TranslationNodeError), - /// Requested translation path doesn't exist in any translation files - #[error("The path '{0}' was not found in any of the translations files.")] + #[error("The path '{0}' could not be found")] PathNotFound(String), + + #[error("The language '{0:?}' ('{0:#}') is not available for the path '{1}'")] + LanguageNotAvailable(Language, String), } impl RuntimeError { diff --git a/translatable/src/lib.rs b/translatable/src/lib.rs index 7ffe0cc..d57d71f 100644 --- a/translatable/src/lib.rs +++ b/translatable/src/lib.rs @@ -11,5 +11,11 @@ pub use translatable_proc::translation; pub mod shared { pub use strum::IntoEnumIterator; /// Re-export utils used for both runtime and compile-time - pub use translatable_shared::{Language, LanguageIter, TranslationNode}; + pub use translatable_shared::{ + Language, + LanguageIter, + TranslationNode, + TranslationNodeCollection, + TranslationNodeError, + }; } diff --git a/translatable_proc/src/lib.rs b/translatable_proc/src/lib.rs index 840131b..dded908 100644 --- a/translatable_proc/src/lib.rs +++ b/translatable_proc/src/lib.rs @@ -7,14 +7,17 @@ //! - Configurable loading strategies //! - Procedural macro for compile-time checking -use macro_parsing_old::{RawMacroArgs, translation_macro}; use proc_macro::TokenStream; use syn::parse_macro_input; + +use crate::macro_input::translation::TranslationMacroArgs; +use crate::macro_generation::translation::translation_macro; + mod data; mod macro_generation; mod macro_input; -mod macro_parsing_old; +mod utils; /// Procedural macro for compile-time translation validation /// @@ -28,5 +31,5 @@ mod macro_parsing_old; /// - Translation path (supports static analysis) #[proc_macro] pub fn translation(input: TokenStream) -> TokenStream { - translation_macro(parse_macro_input!(input as RawMacroArgs).into()).into() + translation_macro(parse_macro_input!(input as TranslationMacroArgs).into()).into() } diff --git a/translatable_proc/src/macro_generation/translation.rs b/translatable_proc/src/macro_generation/translation.rs index 8b13789..cafef49 100644 --- a/translatable_proc/src/macro_generation/translation.rs +++ b/translatable_proc/src/macro_generation/translation.rs @@ -1 +1,90 @@ +use proc_macro2::TokenStream as TokenStream2; +use quote::{ToTokens, quote}; +use thiserror::Error; +use translatable_shared::Language; +use crate::data::translations::load_translations; +use crate::macro_input::input_type::InputType; +use crate::macro_input::translation::TranslationMacroArgs; +use crate::utils::collections::map_to_tokens; +use crate::utils::errors::handle_macro_result; + +#[derive(Error, Debug)] +enum MacroCompileError { + #[error("The path '{0}' could not be found")] + PathNotFound(String), + + #[error("The language '{0:?}' ('{0:#}') is not available for the path '{1}'")] + LanguageNotAvailable(Language, String), +} + +pub fn translation_macro(input: TranslationMacroArgs) -> TokenStream2 { + let translations = handle_macro_result!(load_translations()); + + if let InputType::Static(language) = input.language() { + if let InputType::Static(path) = input.path() { + let static_path_display = path.join("::"); + + let translation_object = translations + .find_path(&path) + .ok_or_else(|| MacroCompileError::PathNotFound(static_path_display.clone())); + + let translation = + handle_macro_result!(translation_object).get(language).ok_or_else(|| { + MacroCompileError::LanguageNotAvailable( + language.clone(), + static_path_display.clone(), + ) + }); + + return handle_macro_result!(translation).into_token_stream(); + } + } + + let language = match input.language() { + InputType::Static(language) => language.clone().to_token_stream(), + InputType::Dynamic(language) => quote! { + translatable::shared::Language::from(#language) + }, + }; + + let translation_object = match input.path() { + InputType::Static(path) => { + let static_path_display = path.join("::"); + + let translation_object = translations + .find_path(path) + .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.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::RuntimeError::PathNotFound(path.join("::")))? + } + }, + }; + + quote! { + (|| { + #translation_object + .get(&#language) + .ok_or_else(|| translatable::RuntimeError::LanguageNotAvailable(#language.clone(), path.join("::")))? + })() + } +} diff --git a/translatable_proc/src/macro_input/input_type.rs b/translatable_proc/src/macro_input/input_type.rs index b022d9b..2656b91 100644 --- a/translatable_proc/src/macro_input/input_type.rs +++ b/translatable_proc/src/macro_input/input_type.rs @@ -14,6 +14,8 @@ impl> InputType { /// enum value whether it's conceptually /// dynamic or static into its dynamic /// represented as a `TokenStream` + #[cold] + #[inline] #[allow(unused)] fn dynamic(self) -> TokenStream2 { match self { diff --git a/translatable_proc/src/macro_input/translation.rs b/translatable_proc/src/macro_input/translation.rs index 249cbd8..29942b3 100644 --- a/translatable_proc/src/macro_input/translation.rs +++ b/translatable_proc/src/macro_input/translation.rs @@ -133,3 +133,23 @@ impl Parse for TranslationMacroArgs { }) } } + +impl TranslationMacroArgs { + #[inline] + #[allow(unused)] + pub fn language(&self) -> &InputType { + &self.language + } + + #[inline] + #[allow(unused)] + pub fn path(&self) -> &InputType> { + &self.path + } + + #[inline] + #[allow(unused)] + pub fn replacements(&self) -> &HashMap { + &self.replacements + } +} diff --git a/translatable_proc/src/macro_parsing_old.rs b/translatable_proc/src/macro_parsing_old.rs deleted file mode 100644 index ded392f..0000000 --- a/translatable_proc/src/macro_parsing_old.rs +++ /dev/null @@ -1,246 +0,0 @@ -use std::collections::HashMap; -use std::fmt::Display; - -use proc_macro2::TokenStream; -use quote::{ToTokens, quote}; -use syn::parse::{Parse, ParseStream}; -use syn::punctuated::Punctuated; -use syn::token::Static; -use syn::{ - Expr, - ExprLit, - ExprPath, - Ident, - Lit, - MetaNameValue, - Path, - Result as SynResult, - Token, - parse_quote, -}; - -use crate::macro_generation::translation::{ - load_lang_dynamic, - load_lang_static, - load_translation_dynamic, - load_translation_static, -}; - -/// Represents raw input arguments for the translation macro -/// -/// Parses input in the format: `(language_spec, static translation_path)` -/// -/// # Syntax -/// - `language_spec`: String literal or expression implementing `Into` -/// - `translation_path`: Path expression (either static or dynamic) -pub struct RawMacroArgs { - /// Language specification (either literal string or expression) - language: Expr, - /// Comma separator between arguments - _comma: Token![,], - /// Optional `static` keyword marker for path resolution - static_marker: Option, - /// Translation path (either static path or dynamic expression) - path: Expr, - /// Optional comma separator for additional arguments - _comma2: Option, - /// Format arguments for string interpolation - format_kwargs: Punctuated, -} - -/// Represents the type of translation path resolution -pub enum PathType { - /// Runtime-resolved path expression - OnScopeExpression(TokenStream), - /// Compile-time resolved path string - CompileTimePath(String), -} - -/// Represents the type of language specification -pub enum LanguageType { - /// Runtime-resolved language expression - OnScopeExpression(TokenStream), - /// Compile-time validated language literal - CompileTimeLiteral(String), -} - -/// Processed translation arguments ready for code generation -pub struct TranslationArgs { - /// Language resolution type - language: LanguageType, - /// Path resolution type - path: PathType, - /// Format arguments for string interpolation - format_kwargs: HashMap, -} - -impl Parse for RawMacroArgs { - fn parse(input: ParseStream) -> SynResult { - let language = input.parse()?; - let _comma = input.parse()?; - let static_marker = input.parse()?; - let path = input.parse()?; - - // Parse optional comma before format arguments - let _comma2 = if input.peek(Token![,]) { Some(input.parse()?) } else { None }; - - let mut format_kwargs = Punctuated::new(); - - // Parse format arguments if comma was present - if _comma2.is_some() { - while !input.is_empty() { - let lookahead = input.lookahead1(); - - // Handle both identifier-based and arbitrary key-value pairs - if lookahead.peek(Ident) { - let key: Ident = input.parse()?; - let eq_token: Token![=] = input.parse().unwrap_or(Token![=](key.span())); - let mut value = input.parse::(); - - if let Ok(value) = &mut value { - let key_string = key.to_string(); - if key_string == value.to_token_stream().to_string() { - // let warning = format!( - // "redundant field initialier, use - // `{key_string}` instead of `{key_string} = {key_string}`" - // ); - - // Generate warning for redundant initializer - *value = parse_quote! {{ - // compile_warn!(#warning); - // !!! https://internals.rust-lang.org/t/pre-rfc-add-compile-warning-macro/9370 !!! - #value - }} - } - } - - let value = value.unwrap_or(parse_quote!(#key)); - - format_kwargs.push(MetaNameValue { path: Path::from(key), eq_token, value }); - } else { - format_kwargs.push(input.parse()?); - } - - // Continue parsing while commas are present - if input.peek(Token![,]) { - input.parse::()?; - } else { - break; - } - } - }; - - Ok(RawMacroArgs { - language, - _comma, - static_marker, - path, - _comma2, - format_kwargs, - }) - } -} - -impl From for TranslationArgs { - fn from(val: RawMacroArgs) -> Self { - let is_path_static = val.static_marker.is_some(); - - TranslationArgs { - // Extract language specification - language: match val.language { - // Handle string literals for compile-time validation - Expr::Lit(ExprLit { lit: Lit::Str(lit_str), .. }) => { - LanguageType::CompileTimeLiteral(lit_str.value()) - }, - // Preserve other expressions for runtime resolution - other => LanguageType::OnScopeExpression(quote!(#other)), - }, - - // Extract path specification - path: match val.path { - // Convert path expressions to strings when static marker present - Expr::Path(ExprPath { path, .. }) if is_path_static => { - // Convert path segments to a dot-separated string - let path_str = path.segments.iter().map(|s| s.ident.to_string()).fold( - String::new(), - |mut acc, s| { - if !acc.is_empty() { - acc.push('.'); - } - acc.push_str(&s); - acc - }, - ); - PathType::CompileTimePath(path_str) - }, - - // Preserve dynamic path expressions - path => PathType::OnScopeExpression(quote!(#path)), - }, - - // Convert format arguments to HashMap with string keys - format_kwargs: val - .format_kwargs - .iter() - .map(|pair| { - ( - // Extract key as identifier or stringified path - pair.path - .get_ident() - .map(|i| i.to_string()) - .unwrap_or_else(|| pair.path.to_token_stream().to_string()), - // Store value as token stream - pair.value.to_token_stream(), - ) - }) - .collect(), - } - } -} - -/// Generates translation code based on processed arguments -/// -/// # Arguments -/// - `args`: Processed translation arguments -/// -/// # Returns -/// TokenStream with either: -/// - Compiled translation string -/// - Runtime translation resolution logic -/// - Compile errors for invalid inputs -pub fn translation_macro(args: TranslationArgs) -> TokenStream { - let TranslationArgs { language, path, format_kwargs } = args; - - // Process language specification - let (lang_expr, static_lang) = match language { - LanguageType::CompileTimeLiteral(lang) => ( - None, - match load_lang_static(&lang) { - Ok(lang) => Some(lang), - Err(e) => return error_token(&e), - }, - ), - LanguageType::OnScopeExpression(lang) => { - (Some(load_lang_dynamic(lang).map_err(|e| error_token(&e))), None) - }, - }; - - // Process translation path - let translation_expr = match path { - PathType::CompileTimePath(p) => load_translation_static(static_lang, p, format_kwargs), - PathType::OnScopeExpression(p) => load_translation_dynamic(static_lang, p, format_kwargs), - }; - - match (lang_expr, translation_expr) { - (Some(Ok(lang)), Ok(trans)) => quote! {{ #lang #trans }}, - (Some(Err(e)), _) => e, - (None, Ok(trans)) => trans, - (_, Err(e)) => error_token(&e), - } -} - -/// Helper function to create compile error tokens -fn error_token(e: &impl Display) -> TokenStream { - let msg = format!("{e:#}"); - quote! { compile_error!(#msg) } -} diff --git a/translatable_proc/src/utils/collections.rs b/translatable_proc/src/utils/collections.rs new file mode 100644 index 0000000..f032bd6 --- /dev/null +++ b/translatable_proc/src/utils/collections.rs @@ -0,0 +1,19 @@ +use std::collections::HashMap; + +use proc_macro2::TokenStream as TokenStream2; +use quote::{ToTokens, quote}; + +pub fn map_to_tokens(map: &HashMap) -> TokenStream2 { + let map = map.into_iter().map(|(key, value)| { + let key = key.into_token_stream(); + let value = value.into_token_stream(); + + quote! { (#key, #value) } + }); + + quote! { + vec![#(#map),*] + .iter() + .collect::>() + } +} diff --git a/translatable_proc/src/utils/errors.rs b/translatable_proc/src/utils/errors.rs new file mode 100644 index 0000000..bc5a215 --- /dev/null +++ b/translatable_proc/src/utils/errors.rs @@ -0,0 +1,36 @@ +use std::fmt::Display; + +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; + +/// Implements a helper function to convert +/// anything that implements Display into +/// a generated `compile_error!` in macros. +pub trait IntoCompileError +where + Self: Display, +{ + /// Transforms the value into a string + /// and wraps `compile_error!` into it + /// for it to be returned when an error + /// happens + fn into_compile_error(&self) -> TokenStream2 { + let message = self.to_string(); + quote! { std::compile_error!(#message) } + } +} + +impl IntoCompileError for T {} + +macro_rules! handle_macro_result { + ($val:expr) => {{ + use $crate::utils::errors::IntoCompileError; + + match $val { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return error.into_compile_error(), + } + }}; +} + +pub(crate) use handle_macro_result; diff --git a/translatable_proc/src/utils/mod.rs b/translatable_proc/src/utils/mod.rs new file mode 100644 index 0000000..e1d0e77 --- /dev/null +++ b/translatable_proc/src/utils/mod.rs @@ -0,0 +1,2 @@ +pub mod collections; +pub mod errors; diff --git a/translatable_shared/src/languages.rs b/translatable_shared/src/languages.rs index a7792dc..956eb22 100644 --- a/translatable_shared/src/languages.rs +++ b/translatable_shared/src/languages.rs @@ -1,5 +1,5 @@ use proc_macro2::{Span, TokenStream as TokenStream2}; -use quote::{quote, ToTokens, TokenStreamExt}; +use quote::{ToTokens, TokenStreamExt, quote}; use strum::{Display, EnumIter, EnumString, IntoEnumIterator}; use syn::Ident; diff --git a/translatable_shared/src/translations/collection.rs b/translatable_shared/src/translations/collection.rs index 43d5e38..ff5550d 100644 --- a/translatable_shared/src/translations/collection.rs +++ b/translatable_shared/src/translations/collection.rs @@ -1,4 +1,9 @@ -use super::node::{TranslationNesting, TranslationObject}; +use std::collections::HashMap; + +use proc_macro2::TokenStream as TokenStream2; +use quote::{ToTokens, TokenStreamExt, quote}; + +use super::node::TranslationObject; use crate::TranslationNode; /// Wraps a collection of translation nodes, these translation nodes @@ -7,7 +12,7 @@ use crate::TranslationNode; /// permits searching for translations by iterating all the files /// in the specified configuration order, so most likely you don't /// need to seek for a translation independently. -pub struct TranslationNodeCollection(TranslationNesting); +pub struct TranslationNodeCollection(HashMap); impl TranslationNodeCollection { /// This method may be used to load a translation @@ -50,3 +55,24 @@ impl FromIterator<(String, TranslationNode)> for TranslationNodeCollection { Self(iter.into_iter().collect()) } } + +impl ToTokens for TranslationNodeCollection { + fn to_tokens(&self, tokens: &mut TokenStream2) { + let node_collection = self.0.iter().map(|(key, value)| { + let key = key.to_token_stream(); + let value = value.to_token_stream(); + + quote! { + (#key.to_string(), #value) + } + }); + + tokens.append_all(quote! { + translatable::shared::TranslationNodeCollection( + vec![#(#node_collection),*] + .iter() + .collect() + ) + }); + } +} diff --git a/translatable_shared/src/translations/node.rs b/translatable_shared/src/translations/node.rs index ce8207d..5f1b891 100644 --- a/translatable_shared/src/translations/node.rs +++ b/translatable_shared/src/translations/node.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use proc_macro2::TokenStream as TokenStream2; -use quote::{quote, ToTokens, TokenStreamExt}; +use quote::{ToTokens, TokenStreamExt, quote}; use strum::ParseError; use thiserror::Error; use toml::{Table, Value}; From c3352320ab5dc19d3d28dccd7426204e75626e7e Mon Sep 17 00:00:00 2001 From: stifskere Date: Thu, 10 Apr 2025 15:43:30 +0200 Subject: [PATCH 075/228] feat: fixed all lifetime and type errors generated by the macro --- translatable/src/lib.rs | 3 ++ translatable/tests/test.rs | 12 +++---- translatable_proc/src/lib.rs | 3 +- .../src/macro_generation/translation.rs | 20 +++++++----- .../src/macro_input/translation.rs | 31 +++++++++++-------- translatable_proc/src/utils/collections.rs | 4 +-- .../src/translations/collection.rs | 11 ++++--- translatable_shared/src/translations/node.rs | 8 +++-- 8 files changed, 53 insertions(+), 39 deletions(-) diff --git a/translatable/src/lib.rs b/translatable/src/lib.rs index d57d71f..c165248 100644 --- a/translatable/src/lib.rs +++ b/translatable/src/lib.rs @@ -2,6 +2,9 @@ mod error; // Export the private error module pub use error::RuntimeError as Error; +/// Re-export the language in the crate top level as +/// it's a macro parameter +pub use shared::Language; /// Re-export the procedural macro for crate users pub use translatable_proc::translation; diff --git a/translatable/tests/test.rs b/translatable/tests/test.rs index 137ceff..07e2772 100644 --- a/translatable/tests/test.rs +++ b/translatable/tests/test.rs @@ -1,5 +1,4 @@ -use strum::IntoEnumIterator; -use translatable::translation; +use translatable::{Language, translation}; #[test] fn both_static() { @@ -10,24 +9,21 @@ fn both_static() { #[test] fn language_static_path_dynamic() { - let result = translation!("es", "common.greeting", name = "john"); + let result = translation!("es", vec!["common", "greeting"], name = "john"); assert!(result.unwrap() == "Β‘Hola john!".to_string()) } #[test] fn language_dynamic_path_static() { - let language = "es"; let name = "john"; - let result = translation!(language, static common::greeting, name = name); + let result = translation!(Language::ES, static common::greeting, name); assert!(result.unwrap() == "Β‘Hola john!".to_string()) } #[test] fn both_dynamic() { - let language = "es"; - let result = translation!(language, "common.greeting", lol = 10, name = "john"); - + let result = translation!(Language::ES, vec!["common", "greeting"], name = "john"); assert!(result.unwrap() == "Β‘Hola john!".to_string()) } diff --git a/translatable_proc/src/lib.rs b/translatable_proc/src/lib.rs index dded908..b5759eb 100644 --- a/translatable_proc/src/lib.rs +++ b/translatable_proc/src/lib.rs @@ -10,9 +10,8 @@ use proc_macro::TokenStream; use syn::parse_macro_input; - -use crate::macro_input::translation::TranslationMacroArgs; use crate::macro_generation::translation::translation_macro; +use crate::macro_input::translation::TranslationMacroArgs; mod data; mod macro_generation; diff --git a/translatable_proc/src/macro_generation/translation.rs b/translatable_proc/src/macro_generation/translation.rs index cafef49..5c234b6 100644 --- a/translatable_proc/src/macro_generation/translation.rs +++ b/translatable_proc/src/macro_generation/translation.rs @@ -60,7 +60,7 @@ pub fn translation_macro(input: TranslationMacroArgs) -> TokenStream2 { quote! { #[doc(hidden)] - let path: Vec = vec![#(#path.to_string()),*]; + let path: Vec<_> = vec![#(#path.to_string()),*]; #translations_tokens } @@ -71,20 +71,26 @@ pub fn translation_macro(input: TranslationMacroArgs) -> TokenStream2 { quote! { #[doc(hidden)] - let path: Vec = #path; + let path: Vec<_> = #path; #translations_tokens .find_path(&path) - .ok_or_else(|| translatable::RuntimeError::PathNotFound(path.join("::")))? + .ok_or_else(|| translatable::Error::PathNotFound(path.join("::")))? } }, }; quote! { - (|| { - #translation_object - .get(&#language) - .ok_or_else(|| translatable::RuntimeError::LanguageNotAvailable(#language.clone(), path.join("::")))? + (|| -> Result { + std::result::Result::Ok({ + #[doc(hidden)] + let language = #language; + + #translation_object + .get(&language) + .ok_or_else(|| translatable::Error::LanguageNotAvailable(language, path.join("::")))? + .to_string() + }) })() } } diff --git a/translatable_proc/src/macro_input/translation.rs b/translatable_proc/src/macro_input/translation.rs index 29942b3..3eb543c 100644 --- a/translatable_proc/src/macro_input/translation.rs +++ b/translatable_proc/src/macro_input/translation.rs @@ -1,3 +1,4 @@ +use core::panic; use std::collections::HashMap; use proc_macro2::TokenStream as TokenStream2; @@ -6,7 +7,10 @@ use syn::parse::{Parse, ParseStream}; use syn::token::Static; use syn::{ Error as SynError, + Expr, + ExprLit, Ident, + Lit, LitStr, Path, PathArguments, @@ -77,22 +81,22 @@ impl TranslationMacroArgsError { /// used in a `parse_macro_input!` call. impl Parse for TranslationMacroArgs { fn parse(input: ParseStream) -> SynResult { - let language_arg = input.parse::()?; - let parsed_langauge_arg = match parse2::(language_arg.clone()) { - Ok(literal) => match literal.value().parse::() { - Ok(language) => InputType::Static(language), - - Err(_) => Err(TranslationMacroArgsError::InvalidIsoLiteral(literal.value()) - .into_syn_error(literal))?, + let parsed_langauge_arg = match input.parse::()? { + Expr::Lit(ExprLit { lit: Lit::Str(literal), .. }) => { + match literal.value().parse::() { + Ok(language) => InputType::Static(language), + + Err(_) => Err(TranslationMacroArgsError::InvalidIsoLiteral(literal.value()) + .into_syn_error(literal))?, + } }, - Err(_) => InputType::Dynamic(language_arg), + other => InputType::Dynamic(other.into_token_stream()), }; input.parse::()?; - let next_token = input.parse::()?; - let parsed_path_arg = match parse2::(next_token.clone()) { + let parsed_path_arg = match input.parse::() { Ok(_) => { let language_arg = input .parse::()? @@ -109,15 +113,16 @@ impl Parse for TranslationMacroArgs { InputType::Static(language_arg) }, - Err(_) => InputType::Dynamic(next_token), + Err(_) => InputType::Dynamic(input.parse::()?.to_token_stream()), }; let mut replacements = HashMap::new(); - if input.parse::().is_ok() { + if input.peek(Token![,]) { while !input.is_empty() { + input.parse::()?; let key = input.parse::()?; let value = match input.parse::() { - Ok(_) => input.parse::()?, + Ok(_) => input.parse::()?.to_token_stream(), Err(_) => key.clone().into_token_stream(), }; diff --git a/translatable_proc/src/utils/collections.rs b/translatable_proc/src/utils/collections.rs index f032bd6..989229f 100644 --- a/translatable_proc/src/utils/collections.rs +++ b/translatable_proc/src/utils/collections.rs @@ -13,7 +13,7 @@ pub fn map_to_tokens(map: &HashMap) -> TokenStre quote! { vec![#(#map),*] - .iter() - .collect::>() + .into_iter() + .collect::>() } } diff --git a/translatable_shared/src/translations/collection.rs b/translatable_shared/src/translations/collection.rs index ff5550d..c159236 100644 --- a/translatable_shared/src/translations/collection.rs +++ b/translatable_shared/src/translations/collection.rs @@ -15,6 +15,10 @@ use crate::TranslationNode; pub struct TranslationNodeCollection(HashMap); impl TranslationNodeCollection { + pub fn new(collection: HashMap) -> Self { + Self(collection) + } + /// 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. @@ -25,7 +29,6 @@ impl TranslationNodeCollection { /// # Returns /// A top level translation node, containing all the translations /// in that specific file. - #[cold] #[allow(unused)] pub fn get_node(&self, path: &str) -> Option<&TranslationNode> { self.0.get(&path.to_string()) @@ -43,7 +46,7 @@ impl TranslationNodeCollection { /// # Returns /// A translation object containing a specific translation /// in all it's available languages. - pub fn find_path(&self, path: &Vec) -> Option<&TranslationObject> { + pub fn find_path(&self, path: &Vec) -> Option<&TranslationObject> { self.0.values().find_map(|node| node.find_path(path)) } } @@ -68,9 +71,9 @@ impl ToTokens for TranslationNodeCollection { }); tokens.append_all(quote! { - translatable::shared::TranslationNodeCollection( + translatable::shared::TranslationNodeCollection::new( vec![#(#node_collection),*] - .iter() + .into_iter() .collect() ) }); diff --git a/translatable_shared/src/translations/node.rs b/translatable_shared/src/translations/node.rs index 5f1b891..0353e51 100644 --- a/translatable_shared/src/translations/node.rs +++ b/translatable_shared/src/translations/node.rs @@ -49,7 +49,9 @@ impl TranslationNode { /// /// # Returns /// Reference to translations if path exists and points to leaf node - pub fn find_path(&self, path: &Vec) -> Option<&TranslationObject> { + 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()?; @@ -73,7 +75,7 @@ impl ToTokens for TranslationNode { .into_iter() .map(|(key, value)| { let value = value.to_token_stream(); - quote! { (#key, #value) } + quote! { (#key.to_string(), #value) } }) .collect::>(); @@ -91,7 +93,7 @@ impl ToTokens for TranslationNode { .into_iter() .map(|(key, value)| { let key = key.into_token_stream(); - quote! { (#key, #value) } + quote! { (#key, #value.to_string()) } }) .collect::>(); From 63c2d27caedebeab1d708350f6ddd5ed4ac5d92f Mon Sep 17 00:00:00 2001 From: stifskere Date: Thu, 10 Apr 2025 17:12:29 +0200 Subject: [PATCH 076/228] feat: deleted serde :evil: --- Cargo.lock | 1 - translatable_proc/Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5f3f385..17c1053 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -238,7 +238,6 @@ version = "0.1.0" dependencies = [ "proc-macro2", "quote", - "serde", "syn", "thiserror", "toml", diff --git a/translatable_proc/Cargo.toml b/translatable_proc/Cargo.toml index d7fd0b1..bfe86d6 100644 --- a/translatable_proc/Cargo.toml +++ b/translatable_proc/Cargo.toml @@ -14,7 +14,6 @@ proc-macro = true [dependencies] proc-macro2 = "1.0.94" quote = "1.0.38" -serde = { version = "1.0.218", features = ["derive"] } syn = { version = "2.0.98", features = ["full"] } thiserror = "2.0.11" toml = "0.8.20" From 7660774f9a4961283ea40b85b95ad60689e667c0 Mon Sep 17 00:00:00 2001 From: stifskere Date: Thu, 10 Apr 2025 17:35:59 +0200 Subject: [PATCH 077/228] fix: re-add strum into translatable_proc --- Cargo.lock | 1 + translatable_proc/Cargo.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 17c1053..776f488 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -238,6 +238,7 @@ version = "0.1.0" dependencies = [ "proc-macro2", "quote", + "strum", "syn", "thiserror", "toml", diff --git a/translatable_proc/Cargo.toml b/translatable_proc/Cargo.toml index bfe86d6..c5a4129 100644 --- a/translatable_proc/Cargo.toml +++ b/translatable_proc/Cargo.toml @@ -14,6 +14,7 @@ proc-macro = true [dependencies] proc-macro2 = "1.0.94" quote = "1.0.38" +strum = { version = "0.27.1", features = ["derive"] } syn = { version = "2.0.98", features = ["full"] } thiserror = "2.0.11" toml = "0.8.20" From d784c9cbdae2badfb3a996a7860ba92dbe78a6b8 Mon Sep 17 00:00:00 2001 From: stifskere Date: Thu, 10 Apr 2025 22:45:34 +0200 Subject: [PATCH 078/228] chore: sync --- translatable/src/lib.rs | 1 - .../src/macro_generation/translation.rs | 19 ++++++++++++++++++- .../src/macro_input/translation.rs | 9 +++------ translatable_proc/src/utils/mod.rs | 1 + translatable_proc/src/utils/templating.rs | 2 ++ 5 files changed, 24 insertions(+), 8 deletions(-) create mode 100644 translatable_proc/src/utils/templating.rs diff --git a/translatable/src/lib.rs b/translatable/src/lib.rs index c165248..4a53707 100644 --- a/translatable/src/lib.rs +++ b/translatable/src/lib.rs @@ -12,7 +12,6 @@ pub use translatable_proc::translation; /// that should not but could be used by the users /// of the library pub mod shared { - pub use strum::IntoEnumIterator; /// Re-export utils used for both runtime and compile-time pub use translatable_shared::{ Language, diff --git a/translatable_proc/src/macro_generation/translation.rs b/translatable_proc/src/macro_generation/translation.rs index 5c234b6..13cd89d 100644 --- a/translatable_proc/src/macro_generation/translation.rs +++ b/translatable_proc/src/macro_generation/translation.rs @@ -37,7 +37,24 @@ pub fn translation_macro(input: TranslationMacroArgs) -> TokenStream2 { ) }); - return handle_macro_result!(translation).into_token_stream(); + let translation = handle_macro_result!(translation).into_token_stream(); + let replacements = input.replacements(); + + return if replacements.is_empty() { + translation + } else { + let replacements = replacements + .iter() + .map(|(key, value)| quote! { + #[doc(hidden)] + let #key = #value; + }); + + quote! {{ + #(#replacements)* + format!(#translation) + }} + }; } } diff --git a/translatable_proc/src/macro_input/translation.rs b/translatable_proc/src/macro_input/translation.rs index 3eb543c..78d1894 100644 --- a/translatable_proc/src/macro_input/translation.rs +++ b/translatable_proc/src/macro_input/translation.rs @@ -1,4 +1,3 @@ -use core::panic; use std::collections::HashMap; use proc_macro2::TokenStream as TokenStream2; @@ -11,12 +10,10 @@ use syn::{ ExprLit, Ident, Lit, - LitStr, Path, PathArguments, Result as SynResult, Token, - parse2, }; use thiserror::Error; use translatable_shared::Language; @@ -67,7 +64,7 @@ pub struct TranslationMacroArgs { /// /// 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, + replacements: HashMap, } impl TranslationMacroArgsError { @@ -127,7 +124,7 @@ impl Parse for TranslationMacroArgs { Err(_) => key.clone().into_token_stream(), }; - replacements.insert(key.to_string(), value); + replacements.insert(key, value); } } @@ -154,7 +151,7 @@ impl TranslationMacroArgs { #[inline] #[allow(unused)] - pub fn replacements(&self) -> &HashMap { + pub fn replacements(&self) -> &HashMap { &self.replacements } } diff --git a/translatable_proc/src/utils/mod.rs b/translatable_proc/src/utils/mod.rs index e1d0e77..a558d2e 100644 --- a/translatable_proc/src/utils/mod.rs +++ b/translatable_proc/src/utils/mod.rs @@ -1,2 +1,3 @@ pub mod collections; pub mod errors; +pub mod templating; diff --git a/translatable_proc/src/utils/templating.rs b/translatable_proc/src/utils/templating.rs new file mode 100644 index 0000000..139597f --- /dev/null +++ b/translatable_proc/src/utils/templating.rs @@ -0,0 +1,2 @@ + + From 965d3b92d51eb3ec306ea0b69922eeedb2a2531a Mon Sep 17 00:00:00 2001 From: stifskere Date: Fri, 11 Apr 2025 23:46:00 +0200 Subject: [PATCH 079/228] feat: moved `utils` to `translatable_shared` and reordered files --- translatable_proc/src/lib.rs | 1 - .../src/macro_generation/translation.rs | 8 ++++---- .../src/{languages.rs => language.rs} | 0 translatable_shared/src/lib.rs | 16 +++------------- .../src/macros}/collections.rs | 18 ++++++++++++++++++ .../src/macros}/errors.rs | 3 +-- .../src/macros}/mod.rs | 0 .../src/macros}/templating.rs | 1 - .../src/translations/collection.rs | 3 +-- translatable_shared/src/translations/node.rs | 2 +- 10 files changed, 28 insertions(+), 24 deletions(-) rename translatable_shared/src/{languages.rs => language.rs} (100%) rename {translatable_proc/src/utils => translatable_shared/src/macros}/collections.rs (55%) rename {translatable_proc/src/utils => translatable_shared/src/macros}/errors.rs (96%) rename {translatable_proc/src/utils => translatable_shared/src/macros}/mod.rs (100%) rename {translatable_proc/src/utils => translatable_shared/src/macros}/templating.rs (50%) diff --git a/translatable_proc/src/lib.rs b/translatable_proc/src/lib.rs index b5759eb..0bd8c66 100644 --- a/translatable_proc/src/lib.rs +++ b/translatable_proc/src/lib.rs @@ -16,7 +16,6 @@ use crate::macro_input::translation::TranslationMacroArgs; mod data; mod macro_generation; mod macro_input; -mod utils; /// Procedural macro for compile-time translation validation /// diff --git a/translatable_proc/src/macro_generation/translation.rs b/translatable_proc/src/macro_generation/translation.rs index 13cd89d..e126edf 100644 --- a/translatable_proc/src/macro_generation/translation.rs +++ b/translatable_proc/src/macro_generation/translation.rs @@ -43,12 +43,12 @@ pub fn translation_macro(input: TranslationMacroArgs) -> TokenStream2 { return if replacements.is_empty() { translation } else { - let replacements = replacements - .iter() - .map(|(key, value)| quote! { + let replacements = replacements.iter().map(|(key, value)| { + quote! { #[doc(hidden)] let #key = #value; - }); + } + }); quote! {{ #(#replacements)* diff --git a/translatable_shared/src/languages.rs b/translatable_shared/src/language.rs similarity index 100% rename from translatable_shared/src/languages.rs rename to translatable_shared/src/language.rs diff --git a/translatable_shared/src/lib.rs b/translatable_shared/src/lib.rs index 1acfaf4..6e90850 100644 --- a/translatable_shared/src/lib.rs +++ b/translatable_shared/src/lib.rs @@ -1,13 +1,3 @@ -mod languages; -mod translations; - -/// Export all the structures in the common -/// top-level namespace -pub use crate::languages::{Language, LanguageIter, Similarities}; -pub use crate::translations::collection::TranslationNodeCollection; -pub use crate::translations::node::{ - TranslationNesting, - TranslationNode, - TranslationNodeError, - TranslationObject, -}; +pub mod language; +pub mod macros; +pub mod translations; diff --git a/translatable_proc/src/utils/collections.rs b/translatable_shared/src/macros/collections.rs similarity index 55% rename from translatable_proc/src/utils/collections.rs rename to translatable_shared/src/macros/collections.rs index 989229f..aa18097 100644 --- a/translatable_proc/src/utils/collections.rs +++ b/translatable_shared/src/macros/collections.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::hash::Hash; use proc_macro2::TokenStream as TokenStream2; use quote::{ToTokens, quote}; @@ -17,3 +18,20 @@ pub fn map_to_tokens(map: &HashMap) -> TokenStre .collect::>() } } + +#[inline] +pub fn map_transform_to_tokens(map: &HashMap, predicate: F) -> TokenStream2 +where + K: ToTokens, + V: ToTokens, + Kr: Eq + ToTokens + Hash, + Vr: ToTokens + Hash, + F: Fn(&K, &V) -> (Kr, Vr), +{ + map_to_tokens( + &map + .iter() + .map(|(key, value)| predicate(key, value)) + .collect::>() + ) +} diff --git a/translatable_proc/src/utils/errors.rs b/translatable_shared/src/macros/errors.rs similarity index 96% rename from translatable_proc/src/utils/errors.rs rename to translatable_shared/src/macros/errors.rs index bc5a215..94de8ab 100644 --- a/translatable_proc/src/utils/errors.rs +++ b/translatable_shared/src/macros/errors.rs @@ -22,6 +22,7 @@ where impl IntoCompileError for T {} +#[macro_export] macro_rules! handle_macro_result { ($val:expr) => {{ use $crate::utils::errors::IntoCompileError; @@ -32,5 +33,3 @@ macro_rules! handle_macro_result { } }}; } - -pub(crate) use handle_macro_result; diff --git a/translatable_proc/src/utils/mod.rs b/translatable_shared/src/macros/mod.rs similarity index 100% rename from translatable_proc/src/utils/mod.rs rename to translatable_shared/src/macros/mod.rs diff --git a/translatable_proc/src/utils/templating.rs b/translatable_shared/src/macros/templating.rs similarity index 50% rename from translatable_proc/src/utils/templating.rs rename to translatable_shared/src/macros/templating.rs index 139597f..8b13789 100644 --- a/translatable_proc/src/utils/templating.rs +++ b/translatable_shared/src/macros/templating.rs @@ -1,2 +1 @@ - diff --git a/translatable_shared/src/translations/collection.rs b/translatable_shared/src/translations/collection.rs index c159236..21519b2 100644 --- a/translatable_shared/src/translations/collection.rs +++ b/translatable_shared/src/translations/collection.rs @@ -3,8 +3,7 @@ use std::collections::HashMap; use proc_macro2::TokenStream as TokenStream2; use quote::{ToTokens, TokenStreamExt, quote}; -use super::node::TranslationObject; -use crate::TranslationNode; +use super::node::{TranslationNode, TranslationObject}; /// Wraps a collection of translation nodes, these translation nodes /// are usually directly loaded files, and the keys to access them diff --git a/translatable_shared/src/translations/node.rs b/translatable_shared/src/translations/node.rs index 0353e51..0a6be2a 100644 --- a/translatable_shared/src/translations/node.rs +++ b/translatable_shared/src/translations/node.rs @@ -6,7 +6,7 @@ use strum::ParseError; use thiserror::Error; use toml::{Table, Value}; -use crate::Language; +use crate::language::Language; /// Errors occurring during TOML-to-translation structure transformation #[derive(Error, Debug)] From 255331f65ff83556a77ec07814ecab1a81ffcb18 Mon Sep 17 00:00:00 2001 From: stifskere Date: Sat, 12 Apr 2025 10:00:22 +0200 Subject: [PATCH 080/228] feat: implemeted collection abstraction elsewhere --- .../src/macro_input/input_type.rs | 1 - translatable_shared/src/macros/collections.rs | 21 +++++----- .../src/translations/collection.rs | 21 +++------- translatable_shared/src/translations/node.rs | 39 +++++-------------- 4 files changed, 24 insertions(+), 58 deletions(-) diff --git a/translatable_proc/src/macro_input/input_type.rs b/translatable_proc/src/macro_input/input_type.rs index 2656b91..455dc8b 100644 --- a/translatable_proc/src/macro_input/input_type.rs +++ b/translatable_proc/src/macro_input/input_type.rs @@ -14,7 +14,6 @@ impl> InputType { /// enum value whether it's conceptually /// dynamic or static into its dynamic /// represented as a `TokenStream` - #[cold] #[inline] #[allow(unused)] fn dynamic(self) -> TokenStream2 { diff --git a/translatable_shared/src/macros/collections.rs b/translatable_shared/src/macros/collections.rs index aa18097..98351b0 100644 --- a/translatable_shared/src/macros/collections.rs +++ b/translatable_shared/src/macros/collections.rs @@ -1,9 +1,9 @@ use std::collections::HashMap; -use std::hash::Hash; use proc_macro2::TokenStream as TokenStream2; use quote::{ToTokens, quote}; +#[inline] pub fn map_to_tokens(map: &HashMap) -> TokenStream2 { let map = map.into_iter().map(|(key, value)| { let key = key.into_token_stream(); @@ -20,18 +20,17 @@ pub fn map_to_tokens(map: &HashMap) -> TokenStre } #[inline] -pub fn map_transform_to_tokens(map: &HashMap, predicate: F) -> TokenStream2 +pub fn map_transform_to_tokens(map: &HashMap, predicate: F) -> TokenStream2 where K: ToTokens, V: ToTokens, - Kr: Eq + ToTokens + Hash, - Vr: ToTokens + Hash, - F: Fn(&K, &V) -> (Kr, Vr), + F: Fn(&K, &V) -> TokenStream2, { - map_to_tokens( - &map - .iter() - .map(|(key, value)| predicate(key, value)) - .collect::>() - ) + let processed = map.iter().map(|(key, value)| predicate(key, value)); + + quote! { + vec![#(#processed),*] + .into_iter() + .collect::>() + } } diff --git a/translatable_shared/src/translations/collection.rs b/translatable_shared/src/translations/collection.rs index 21519b2..c4d1b93 100644 --- a/translatable_shared/src/translations/collection.rs +++ b/translatable_shared/src/translations/collection.rs @@ -4,6 +4,7 @@ use proc_macro2::TokenStream as TokenStream2; use quote::{ToTokens, TokenStreamExt, quote}; use super::node::{TranslationNode, TranslationObject}; +use crate::macros::collections::map_transform_to_tokens; /// Wraps a collection of translation nodes, these translation nodes /// are usually directly loaded files, and the keys to access them @@ -60,21 +61,9 @@ impl FromIterator<(String, TranslationNode)> for TranslationNodeCollection { impl ToTokens for TranslationNodeCollection { fn to_tokens(&self, tokens: &mut TokenStream2) { - let node_collection = self.0.iter().map(|(key, value)| { - let key = key.to_token_stream(); - let value = value.to_token_stream(); - - quote! { - (#key.to_string(), #value) - } - }); - - tokens.append_all(quote! { - translatable::shared::TranslationNodeCollection::new( - vec![#(#node_collection),*] - .into_iter() - .collect() - ) - }); + tokens.append_all(map_transform_to_tokens( + &self.0, + |key, value| quote! { (#key.to_string(), #value) }, + )); } } diff --git a/translatable_shared/src/translations/node.rs b/translatable_shared/src/translations/node.rs index 0a6be2a..a3ade41 100644 --- a/translatable_shared/src/translations/node.rs +++ b/translatable_shared/src/translations/node.rs @@ -7,6 +7,7 @@ use thiserror::Error; use toml::{Table, Value}; use crate::language::Language; +use crate::macros::collections::map_transform_to_tokens; /// Errors occurring during TOML-to-translation structure transformation #[derive(Error, Debug)] @@ -71,39 +72,17 @@ impl ToTokens for TranslationNode { fn to_tokens(&self, tokens: &mut TokenStream2) { match self { TranslationNode::Nesting(nesting) => { - let mapped_nesting = nesting - .into_iter() - .map(|(key, value)| { - let value = value.to_token_stream(); - quote! { (#key.to_string(), #value) } - }) - .collect::>(); - - tokens.append_all(quote! {{ - translatable::shared::TranslationNode::Nesting( - vec![#(#mapped_nesting),*] - .into_iter() - .collect::>() - ) - }}); + tokens.append_all(map_transform_to_tokens( + nesting, + |key, value| quote! { (#key.to_string(), #value) }, + )); }, TranslationNode::Translation(translation) => { - let mapped_translation = translation - .into_iter() - .map(|(key, value)| { - let key = key.into_token_stream(); - quote! { (#key, #value.to_string()) } - }) - .collect::>(); - - tokens.append_all(quote! {{ - translatable::shared::TranslationNode::Translation( - vec![#(#mapped_translation),*] - .into_iter() - .collect::>() - ) - }}); + tokens.append_all(map_transform_to_tokens( + translation, + |key, value| quote! { (#key, #value.to_string()) }, + )); }, } } From 88b7cdb006f22df19e042809ae838458a3043e63 Mon Sep 17 00:00:00 2001 From: stifskere Date: Sun, 13 Apr 2025 18:00:18 +0200 Subject: [PATCH 081/228] fix: re-added specifications on collection generation --- translatable/src/error.rs | 28 +++++++++------- translatable/src/lib.rs | 33 ++++++++----------- translatable_proc/src/data/translations.rs | 3 +- .../src/macro_generation/translation.rs | 8 ++--- .../src/macro_input/translation.rs | 2 +- translatable_shared/src/language.rs | 2 +- translatable_shared/src/macros/errors.rs | 2 +- .../src/translations/collection.rs | 10 ++++-- translatable_shared/src/translations/node.rs | 20 ++++++++--- 9 files changed, 63 insertions(+), 45 deletions(-) diff --git a/translatable/src/error.rs b/translatable/src/error.rs index d1abd70..d1611a8 100644 --- a/translatable/src/error.rs +++ b/translatable/src/error.rs @@ -1,12 +1,10 @@ -use std::collections::HashMap; - use thiserror::Error; -use translatable_shared::{Language, TranslationNodeError}; +use translatable_shared::language::Language; +use translatable_shared::translations::node::TranslationNodeError; -/// Error type for translation resolution failures -/// -/// Returned by the translation macro when dynamic resolution fails. -/// For static resolution failures, errors are reported at compile time. +/// This enum is used to debug runtime errors generated +/// by the macros in runtime, the error message can be obtained +/// using the `Display` trait of the enum itself. #[derive(Error, Debug)] pub enum RuntimeError { #[error("{0:#}")] @@ -20,13 +18,19 @@ pub enum RuntimeError { } impl RuntimeError { - /// Returns formatted error message as a String + /// This method makes use of the `Display` implemeted in + /// `Error` to display the formatted cause String of + /// the specific error. /// - /// Useful for error reporting and logging. Marked `#[cold]` to hint to the - /// compiler that this path is unlikely to be taken (optimization for error - /// paths). - #[inline] + /// This method is marked as `cold`, because in the application + /// there should be the least amount of errors possible, + /// when displaying the error, please do in a lazy + /// error handling method such as `ok_or_else` or `inspect_err`. + /// + /// # Returns + /// The cause heap allocated String. #[cold] + #[inline] pub fn cause(&self) -> String { format!("{self:#}") } diff --git a/translatable/src/lib.rs b/translatable/src/lib.rs index 4a53707..3782e8e 100644 --- a/translatable/src/lib.rs +++ b/translatable/src/lib.rs @@ -1,23 +1,18 @@ mod error; -// Export the private error module +// A re-export to the runtime error +// enum for user availabilty and +// debugging. pub use error::RuntimeError as Error; -/// Re-export the language in the crate top level as -/// it's a macro parameter -pub use shared::Language; -/// Re-export the procedural macro for crate users +/// A re-export from the Language enum +/// for users to dynamically parse +/// when using dynamic arguments. +pub use shared::language::Language; +/// A re-export to the translation macro +/// exported in the proc_macro module. pub use translatable_proc::translation; - -/// This module re-exports structures used by macros -/// that should not but could be used by the users -/// of the library -pub mod shared { - /// Re-export utils used for both runtime and compile-time - pub use translatable_shared::{ - Language, - LanguageIter, - TranslationNode, - TranslationNodeCollection, - TranslationNodeError, - }; -} +/// A re-export of all the shared modules +/// as declared in the shared crate used +/// for macro generation. +#[doc(hidden)] +pub use translatable_shared as shared; diff --git a/translatable_proc/src/data/translations.rs b/translatable_proc/src/data/translations.rs index ae6c586..eec3506 100644 --- a/translatable_proc/src/data/translations.rs +++ b/translatable_proc/src/data/translations.rs @@ -5,7 +5,8 @@ use std::sync::OnceLock; use thiserror::Error; use toml::Table; use toml::de::Error as TomlError; -use translatable_shared::{TranslationNode, TranslationNodeCollection, TranslationNodeError}; +use translatable_shared::translations::collection::TranslationNodeCollection; +use translatable_shared::translations::node::{TranslationNode, TranslationNodeError}; use super::config::{ConfigError, SeekMode, TranslationOverlap, load_config}; diff --git a/translatable_proc/src/macro_generation/translation.rs b/translatable_proc/src/macro_generation/translation.rs index e126edf..cd69a2c 100644 --- a/translatable_proc/src/macro_generation/translation.rs +++ b/translatable_proc/src/macro_generation/translation.rs @@ -1,13 +1,13 @@ use proc_macro2::TokenStream as TokenStream2; use quote::{ToTokens, quote}; use thiserror::Error; -use translatable_shared::Language; +use translatable_shared::handle_macro_result; +use translatable_shared::language::Language; +use translatable_shared::macros::collections::map_to_tokens; use crate::data::translations::load_translations; use crate::macro_input::input_type::InputType; use crate::macro_input::translation::TranslationMacroArgs; -use crate::utils::collections::map_to_tokens; -use crate::utils::errors::handle_macro_result; #[derive(Error, Debug)] enum MacroCompileError { @@ -61,7 +61,7 @@ pub fn translation_macro(input: TranslationMacroArgs) -> TokenStream2 { let language = match input.language() { InputType::Static(language) => language.clone().to_token_stream(), InputType::Dynamic(language) => quote! { - translatable::shared::Language::from(#language) + translatable::shared::language::Language::from(#language) }, }; diff --git a/translatable_proc/src/macro_input/translation.rs b/translatable_proc/src/macro_input/translation.rs index 78d1894..2bf8d72 100644 --- a/translatable_proc/src/macro_input/translation.rs +++ b/translatable_proc/src/macro_input/translation.rs @@ -16,7 +16,7 @@ use syn::{ Token, }; use thiserror::Error; -use translatable_shared::Language; +use translatable_shared::language::Language; use super::input_type::InputType; diff --git a/translatable_shared/src/language.rs b/translatable_shared/src/language.rs index 956eb22..4feafbe 100644 --- a/translatable_shared/src/language.rs +++ b/translatable_shared/src/language.rs @@ -437,6 +437,6 @@ 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::Language::#ident }) + tokens.append_all(quote! { translatable::shared::language::Language::#ident }) } } diff --git a/translatable_shared/src/macros/errors.rs b/translatable_shared/src/macros/errors.rs index 94de8ab..42842fb 100644 --- a/translatable_shared/src/macros/errors.rs +++ b/translatable_shared/src/macros/errors.rs @@ -25,7 +25,7 @@ impl IntoCompileError for T {} #[macro_export] macro_rules! handle_macro_result { ($val:expr) => {{ - use $crate::utils::errors::IntoCompileError; + use $crate::macros::errors::IntoCompileError; match $val { std::result::Result::Ok(value) => value, diff --git a/translatable_shared/src/translations/collection.rs b/translatable_shared/src/translations/collection.rs index c4d1b93..1e40de1 100644 --- a/translatable_shared/src/translations/collection.rs +++ b/translatable_shared/src/translations/collection.rs @@ -61,9 +61,15 @@ impl FromIterator<(String, TranslationNode)> for TranslationNodeCollection { impl ToTokens for TranslationNodeCollection { fn to_tokens(&self, tokens: &mut TokenStream2) { - tokens.append_all(map_transform_to_tokens( + 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/node.rs b/translatable_shared/src/translations/node.rs index a3ade41..7ee2c7c 100644 --- a/translatable_shared/src/translations/node.rs +++ b/translatable_shared/src/translations/node.rs @@ -72,17 +72,29 @@ impl ToTokens for TranslationNode { fn to_tokens(&self, tokens: &mut TokenStream2) { match self { TranslationNode::Nesting(nesting) => { - tokens.append_all(map_transform_to_tokens( + 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) => { - tokens.append_all(map_transform_to_tokens( + let map = map_transform_to_tokens( translation, |key, value| quote! { (#key, #value.to_string()) }, - )); + ); + + tokens.append_all(quote! { + translatable::shared::translations::node::TranslationNode::Translation( + #map + ) + }); }, } } From f2d899dca3002a0ae713135a928a3ba1cc579c29 Mon Sep 17 00:00:00 2001 From: Esteve Autet Alexe Date: Tue, 15 Apr 2025 01:46:32 +0200 Subject: [PATCH 082/228] feat: add dependabot workflow Signed-off-by: Esteve Autet Alexe --- .github/dependabot.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..201a8df --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "weekly" + From 87cde028db955ec0798a1afc522a0dd2fb34fcc4 Mon Sep 17 00:00:00 2001 From: stifskere Date: Tue, 15 Apr 2025 02:37:04 +0200 Subject: [PATCH 083/228] feat: implement translation templating --- Cargo.lock | 7 + translatable/src/error.rs | 4 + translatable/tests/test.rs | 27 ++-- .../src/macro_generation/translation.rs | 36 +++-- translatable_shared/Cargo.toml | 1 + translatable_shared/src/macros/templating.rs | 131 ++++++++++++++++++ .../src/translations/collection.rs | 6 +- translatable_shared/src/translations/node.rs | 24 +--- translations/test.toml | 4 +- 9 files changed, 183 insertions(+), 57 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 776f488..5c6210e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -255,6 +255,7 @@ dependencies = [ "syn", "thiserror", "toml", + "unicode-xid", ] [[package]] @@ -278,6 +279,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "winapi-util" version = "0.1.9" diff --git a/translatable/src/error.rs b/translatable/src/error.rs index d1611a8..a7f701b 100644 --- a/translatable/src/error.rs +++ b/translatable/src/error.rs @@ -1,5 +1,6 @@ use thiserror::Error; use translatable_shared::language::Language; +use translatable_shared::macros::templating::TemplateError; use translatable_shared::translations::node::TranslationNodeError; /// This enum is used to debug runtime errors generated @@ -15,6 +16,9 @@ pub enum RuntimeError { #[error("The language '{0:?}' ('{0:#}') is not available for the path '{1}'")] LanguageNotAvailable(Language, String), + + #[error("An error has occurred while parsing templates: {0:#}")] + TemplateMissmatch(#[from] TemplateError), } impl RuntimeError { diff --git a/translatable/tests/test.rs b/translatable/tests/test.rs index 07e2772..1284126 100644 --- a/translatable/tests/test.rs +++ b/translatable/tests/test.rs @@ -1,29 +1,32 @@ use translatable::{Language, translation}; +const NAME: &str = "John"; +const SURNAME: &str = "Doe"; +const RESULT: &str = "Β‘Hola John Doe!"; + #[test] fn both_static() { - let result = translation!("es", static common::greeting, name = "john"); - - assert!(result == "Β‘Hola john!") + let result = translation!("es", static common::greeting, name = NAME, surname = SURNAME); + assert!(result.unwrap() == RESULT); } #[test] fn language_static_path_dynamic() { - let result = translation!("es", vec!["common", "greeting"], name = "john"); - - assert!(result.unwrap() == "Β‘Hola john!".to_string()) + let result = translation!("es", vec!["common", "greeting"], name = NAME, surname = SURNAME); + assert!(result.unwrap() == RESULT); } #[test] fn language_dynamic_path_static() { - let name = "john"; - let result = translation!(Language::ES, static common::greeting, name); - - assert!(result.unwrap() == "Β‘Hola john!".to_string()) + 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 = "john"); - assert!(result.unwrap() == "Β‘Hola john!".to_string()) + let result = + translation!(Language::ES, vec!["common", "greeting"], name = NAME, surname = SURNAME); + assert!(result.unwrap() == RESULT) } diff --git a/translatable_proc/src/macro_generation/translation.rs b/translatable_proc/src/macro_generation/translation.rs index cd69a2c..773538e 100644 --- a/translatable_proc/src/macro_generation/translation.rs +++ b/translatable_proc/src/macro_generation/translation.rs @@ -21,6 +21,9 @@ enum MacroCompileError { pub fn translation_macro(input: TranslationMacroArgs) -> TokenStream2 { let translations = handle_macro_result!(load_translations()); + let template_replacements = + input.replacements().iter().map(|(key, value)| quote! { #key = #value }); + if let InputType::Static(language) = input.language() { if let InputType::Static(path) = input.path() { let static_path_display = path.join("::"); @@ -38,22 +41,12 @@ pub fn translation_macro(input: TranslationMacroArgs) -> TokenStream2 { }); let translation = handle_macro_result!(translation).into_token_stream(); - let replacements = input.replacements(); - - return if replacements.is_empty() { - translation - } else { - let replacements = replacements.iter().map(|(key, value)| { - quote! { - #[doc(hidden)] - let #key = #value; - } - }); - quote! {{ - #(#replacements)* - format!(#translation) - }} + return quote! { + translatable::shared::replace_templates!( + #translation, + #(#template_replacements),* + ) }; } } @@ -103,10 +96,15 @@ pub fn translation_macro(input: TranslationMacroArgs) -> TokenStream2 { #[doc(hidden)] let language = #language; - #translation_object - .get(&language) - .ok_or_else(|| translatable::Error::LanguageNotAvailable(language, path.join("::")))? - .to_string() + translatable::shared::replace_templates!( + { + #translation_object + .get(&language) + .ok_or_else(|| translatable::Error::LanguageNotAvailable(language, path.join("::")))? + .to_string() + }, + #(#template_replacements),* + )? }) })() } diff --git a/translatable_shared/Cargo.toml b/translatable_shared/Cargo.toml index c8133fb..62e4efb 100644 --- a/translatable_shared/Cargo.toml +++ b/translatable_shared/Cargo.toml @@ -15,3 +15,4 @@ strum = { version = "0.27.1", features = ["derive", "strum_macros"] } syn = { version = "2.0.100", features = ["full"] } thiserror = "2.0.12" toml = "0.8.20" +unicode-xid = "0.2.6" diff --git a/translatable_shared/src/macros/templating.rs b/translatable_shared/src/macros/templating.rs index 8b13789..283cf09 100644 --- a/translatable_shared/src/macros/templating.rs +++ b/translatable_shared/src/macros/templating.rs @@ -1 +1,132 @@ +use std::collections::HashMap; +use std::ops::Range; +use std::str::FromStr; +use syn::{Ident, parse_str}; +use thiserror::Error; +use unicode_xid::UnicodeXID; + +#[derive(Error, Debug)] +pub enum TemplateError { + // Runtime errors + #[error("Found unclosed brace at index {0}")] + Unclosed(usize), + + #[error("Found template with key '{0}' which is an invalid identifier")] + InvalidIdent(String), + + // Compile errors + #[error("Found template with a non compliant XID key, found invalid start character '{0}'")] + InvalidxIdStart(char), + + #[error("Found template with a non compliant XID key, found invalid rest character '{0}'")] + InvalidxIdRest(char), +} + +pub struct FormatString { + original: String, + spans: HashMap>, +} + +impl FormatString { + pub fn replace_with(mut self, values: HashMap) -> String { + let mut offset = 0isize; + + for (key, range) in self.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; + + self.original.replace_range(start..end, value); + + offset += value.len() as isize - (range.end - range.start) as isize; + } + } + + self.original + } +} + +impl FromStr for FormatString { + type Err = TemplateError; + + fn from_str(s: &str) -> Result { + let original = s.to_string(); + let mut spans = HashMap::new(); + + let mut last_bracket_idx = 0usize; + let mut current_tmpl_key = String::new(); + for (i, c) in original.chars().enumerate() { + match (c, last_bracket_idx) { + // if last template index is anything but the last character + // set it as last index. + ('{', lbi) if lbi != i.saturating_sub(1) => last_bracket_idx = i, + // if last template index is the last character + // ignore current as is escaped. + ('{', lbi) if lbi == i.saturating_sub(1) => last_bracket_idx = 0, + + // if last template index is not 0 and we find + // a closing bracket complete a range. + ('}', lbi) if lbi != 0 => { + spans.insert( + parse_str::(¤t_tmpl_key) + .map_err(|_| TemplateError::InvalidIdent(current_tmpl_key))? + .to_string(), + (last_bracket_idx + 1)..(i + 2), // inclusive + ); + + last_bracket_idx = 0; + current_tmpl_key = String::new(); + }, + + (c, lbi) if lbi != 0 => current_tmpl_key += &{ c.to_string() }, + + _ => {}, + } + } + + if last_bracket_idx != 0 { + Err(TemplateError::Unclosed(last_bracket_idx)) + } else { + Ok(FormatString { original, spans }) + } + } +} + +#[macro_export] +macro_rules! replace_templates { + ($orig:expr, $($key:ident = $value:expr),* $(,)?) => {{ + $orig.parse::<$crate::macros::templating::FormatString>() + .map(|parsed| parsed + .replace_with( + vec![$((stringify!($key).to_string(), $value.to_string())),*] + .into_iter() + .collect::>() + ) + ) + }} +} + +pub fn validate_format_string(original: &str) -> Result<(), TemplateError> { + let mut last_bracket_idx = 0usize; + + for (i, c) in original.chars().enumerate() { + match (c, last_bracket_idx) { + ('{', lbi) if lbi != i.saturating_sub(1) => last_bracket_idx = i, + ('{', lbi) if lbi == i.saturating_sub(1) => last_bracket_idx = 0, + ('}', lbi) if lbi != 0 => last_bracket_idx = 0, + + (c, lbi) if i > 0 && lbi == i - 1 && !c.is_xid_start() => { + return Err(TemplateError::InvalidxIdStart(c)); + }, + + (c, lbi) if lbi != 0 && !c.is_xid_continue() => { + return Err(TemplateError::InvalidxIdRest(c)); + }, + + _ => {}, + } + } + + Ok(()) +} diff --git a/translatable_shared/src/translations/collection.rs b/translatable_shared/src/translations/collection.rs index 1e40de1..0daaa4e 100644 --- a/translatable_shared/src/translations/collection.rs +++ b/translatable_shared/src/translations/collection.rs @@ -61,10 +61,8 @@ impl FromIterator<(String, TranslationNode)> for TranslationNodeCollection { 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) }, - ); + 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( diff --git a/translatable_shared/src/translations/node.rs b/translatable_shared/src/translations/node.rs index 7ee2c7c..8f5579e 100644 --- a/translatable_shared/src/translations/node.rs +++ b/translatable_shared/src/translations/node.rs @@ -8,6 +8,7 @@ use toml::{Table, Value}; use crate::language::Language; use crate::macros::collections::map_transform_to_tokens; +use crate::macros::templating::{TemplateError, validate_format_string}; /// Errors occurring during TOML-to-translation structure transformation #[derive(Error, Debug)] @@ -17,8 +18,8 @@ pub enum TranslationNodeError { InvalidNesting, /// Template syntax error with unbalanced braces - #[error("Templates in translations should match '{{' and '}}'")] - UnclosedTemplate, + #[error("Tempalte validation failed: {0:#}")] + UnclosedTemplate(#[from] TemplateError), /// Invalid value type encountered in translation structure #[error("Only strings and objects are allowed for nested objects.")] @@ -100,21 +101,6 @@ impl ToTokens for TranslationNode { } } -/// Validates template brace balancing in translation strings -fn templates_valid(translation: &str) -> bool { - let mut nestings = 0; - - for character in translation.chars() { - match character { - '{' => nestings += 1, - '}' => nestings -= 1, - _ => {}, - } - } - - nestings == 0 -} - /// This implementation converts a `toml::Table` into a manageable /// NestingType. impl TryFrom
for TranslationNode { @@ -132,9 +118,7 @@ impl TryFrom
for TranslationNode { match result { Self::Translation(translation) => { - if !templates_valid(&translation_value) { - return Err(TranslationNodeError::UnclosedTemplate); - } + validate_format_string(&translation_value)?; translation.insert(key.parse()?, translation_value); }, diff --git a/translations/test.toml b/translations/test.toml index 6aecb64..1d45fc1 100644 --- a/translations/test.toml +++ b/translations/test.toml @@ -4,5 +4,5 @@ en = "Welcome to our app!" es = "Β‘Bienvenido a nuestra aplicaciΓ³n!" [common.greeting] -en = "Hello {name}!" -es = "Β‘Hola {name}!" +en = "Hello {name} {surname}!" +es = "Β‘Hola {name} {surname}!" From ea7af8373b1e95ec2d18321f8ae4410d787511ae Mon Sep 17 00:00:00 2001 From: stifskere Date: Tue, 15 Apr 2025 10:37:23 +0200 Subject: [PATCH 084/228] feat: move language and templating to misc --- translatable/src/error.rs | 4 ++-- translatable/src/lib.rs | 2 +- translatable_proc/src/macro_generation/translation.rs | 4 ++-- translatable_proc/src/macro_input/translation.rs | 2 +- translatable_shared/src/lib.rs | 2 +- translatable_shared/src/macros/mod.rs | 1 - translatable_shared/src/{ => misc}/language.rs | 2 +- translatable_shared/src/misc/mod.rs | 2 ++ translatable_shared/src/{macros => misc}/templating.rs | 2 +- translatable_shared/src/translations/node.rs | 4 ++-- 10 files changed, 13 insertions(+), 12 deletions(-) rename translatable_shared/src/{ => misc}/language.rs (99%) create mode 100644 translatable_shared/src/misc/mod.rs rename translatable_shared/src/{macros => misc}/templating.rs (98%) diff --git a/translatable/src/error.rs b/translatable/src/error.rs index a7f701b..dc7b193 100644 --- a/translatable/src/error.rs +++ b/translatable/src/error.rs @@ -1,6 +1,6 @@ use thiserror::Error; -use translatable_shared::language::Language; -use translatable_shared::macros::templating::TemplateError; +use translatable_shared::misc::language::Language; +use translatable_shared::misc::templating::TemplateError; use translatable_shared::translations::node::TranslationNodeError; /// This enum is used to debug runtime errors generated diff --git a/translatable/src/lib.rs b/translatable/src/lib.rs index 3782e8e..d828bee 100644 --- a/translatable/src/lib.rs +++ b/translatable/src/lib.rs @@ -7,7 +7,7 @@ pub use error::RuntimeError as Error; /// A re-export from the Language enum /// for users to dynamically parse /// when using dynamic arguments. -pub use shared::language::Language; +pub use shared::misc::language::Language; /// A re-export to the translation macro /// exported in the proc_macro module. pub use translatable_proc::translation; diff --git a/translatable_proc/src/macro_generation/translation.rs b/translatable_proc/src/macro_generation/translation.rs index 773538e..5e83a48 100644 --- a/translatable_proc/src/macro_generation/translation.rs +++ b/translatable_proc/src/macro_generation/translation.rs @@ -2,8 +2,8 @@ use proc_macro2::TokenStream as TokenStream2; use quote::{ToTokens, quote}; use thiserror::Error; use translatable_shared::handle_macro_result; -use translatable_shared::language::Language; use translatable_shared::macros::collections::map_to_tokens; +use translatable_shared::misc::language::Language; use crate::data::translations::load_translations; use crate::macro_input::input_type::InputType; @@ -54,7 +54,7 @@ pub fn translation_macro(input: TranslationMacroArgs) -> TokenStream2 { let language = match input.language() { InputType::Static(language) => language.clone().to_token_stream(), InputType::Dynamic(language) => quote! { - translatable::shared::language::Language::from(#language) + translatable::shared::misc::language::Language::from(#language) }, }; diff --git a/translatable_proc/src/macro_input/translation.rs b/translatable_proc/src/macro_input/translation.rs index 2bf8d72..6349098 100644 --- a/translatable_proc/src/macro_input/translation.rs +++ b/translatable_proc/src/macro_input/translation.rs @@ -16,7 +16,7 @@ use syn::{ Token, }; use thiserror::Error; -use translatable_shared::language::Language; +use translatable_shared::misc::language::Language; use super::input_type::InputType; diff --git a/translatable_shared/src/lib.rs b/translatable_shared/src/lib.rs index 6e90850..da1fd09 100644 --- a/translatable_shared/src/lib.rs +++ b/translatable_shared/src/lib.rs @@ -1,3 +1,3 @@ -pub mod language; pub mod macros; +pub mod misc; pub mod translations; diff --git a/translatable_shared/src/macros/mod.rs b/translatable_shared/src/macros/mod.rs index a558d2e..e1d0e77 100644 --- a/translatable_shared/src/macros/mod.rs +++ b/translatable_shared/src/macros/mod.rs @@ -1,3 +1,2 @@ pub mod collections; pub mod errors; -pub mod templating; diff --git a/translatable_shared/src/language.rs b/translatable_shared/src/misc/language.rs similarity index 99% rename from translatable_shared/src/language.rs rename to translatable_shared/src/misc/language.rs index 4feafbe..ca07390 100644 --- a/translatable_shared/src/language.rs +++ b/translatable_shared/src/misc/language.rs @@ -437,6 +437,6 @@ 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::language::Language::#ident }) + tokens.append_all(quote! { translatable::shared::misc::language::Language::#ident }) } } diff --git a/translatable_shared/src/misc/mod.rs b/translatable_shared/src/misc/mod.rs new file mode 100644 index 0000000..d7724b6 --- /dev/null +++ b/translatable_shared/src/misc/mod.rs @@ -0,0 +1,2 @@ +pub mod language; +pub mod templating; diff --git a/translatable_shared/src/macros/templating.rs b/translatable_shared/src/misc/templating.rs similarity index 98% rename from translatable_shared/src/macros/templating.rs rename to translatable_shared/src/misc/templating.rs index 283cf09..93581e1 100644 --- a/translatable_shared/src/macros/templating.rs +++ b/translatable_shared/src/misc/templating.rs @@ -96,7 +96,7 @@ impl FromStr for FormatString { #[macro_export] macro_rules! replace_templates { ($orig:expr, $($key:ident = $value:expr),* $(,)?) => {{ - $orig.parse::<$crate::macros::templating::FormatString>() + $orig.parse::<$crate::misc::templating::FormatString>() .map(|parsed| parsed .replace_with( vec![$((stringify!($key).to_string(), $value.to_string())),*] diff --git a/translatable_shared/src/translations/node.rs b/translatable_shared/src/translations/node.rs index 8f5579e..00ab4e6 100644 --- a/translatable_shared/src/translations/node.rs +++ b/translatable_shared/src/translations/node.rs @@ -6,9 +6,9 @@ use strum::ParseError; use thiserror::Error; use toml::{Table, Value}; -use crate::language::Language; use crate::macros::collections::map_transform_to_tokens; -use crate::macros::templating::{TemplateError, validate_format_string}; +use crate::misc::language::Language; +use crate::misc::templating::{TemplateError, validate_format_string}; /// Errors occurring during TOML-to-translation structure transformation #[derive(Error, Debug)] From d76bda3ff8c6441f126ec0415c7281429356210f Mon Sep 17 00:00:00 2001 From: stifskere Date: Tue, 15 Apr 2025 11:30:14 +0200 Subject: [PATCH 085/228] fix: replace hardcoded values offsets for char_to_byte index conversion in templating --- translatable_shared/src/misc/templating.rs | 34 ++++++++++++---------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/translatable_shared/src/misc/templating.rs b/translatable_shared/src/misc/templating.rs index 93581e1..6ed106d 100644 --- a/translatable_shared/src/misc/templating.rs +++ b/translatable_shared/src/misc/templating.rs @@ -54,39 +54,43 @@ impl FromStr for FormatString { let original = s.to_string(); let mut spans = HashMap::new(); - let mut last_bracket_idx = 0usize; + 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 (i, c) in original.chars().enumerate() { + for (char_idx, c) in original.chars().enumerate() { match (c, last_bracket_idx) { - // if last template index is anything but the last character - // set it as last index. - ('{', lbi) if lbi != i.saturating_sub(1) => last_bracket_idx = i, // if last template index is the last character // ignore current as is escaped. - ('{', lbi) if lbi == i.saturating_sub(1) => last_bracket_idx = 0, + ('{', 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. - ('}', lbi) if lbi != 0 => { + ('}', Some(open_idx)) => { + let key = current_tmpl_key.clone(); + spans.insert( - parse_str::(¤t_tmpl_key) - .map_err(|_| TemplateError::InvalidIdent(current_tmpl_key))? + parse_str::(&key) + .map_err(|_| TemplateError::InvalidIdent(key))? .to_string(), - (last_bracket_idx + 1)..(i + 2), // inclusive + char_to_byte[open_idx]..char_to_byte[char_idx + 1], // inclusive ); - last_bracket_idx = 0; - current_tmpl_key = String::new(); + last_bracket_idx = None; + current_tmpl_key.clear(); }, - (c, lbi) if lbi != 0 => current_tmpl_key += &{ c.to_string() }, + (c, Some(_)) => current_tmpl_key.push(c), _ => {}, } } - if last_bracket_idx != 0 { - Err(TemplateError::Unclosed(last_bracket_idx)) + if let Some(lbi) = last_bracket_idx { + Err(TemplateError::Unclosed(lbi)) } else { Ok(FormatString { original, spans }) } From d8ca7e996973ea0ba96d4ae7622e6a167fb5f717 Mon Sep 17 00:00:00 2001 From: chikof Date: Tue, 15 Apr 2025 19:24:24 +0100 Subject: [PATCH 086/228] fix: string slicing order This ensures that the string is being replaced from start to finish. --- translatable_shared/src/misc/templating.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/translatable_shared/src/misc/templating.rs b/translatable_shared/src/misc/templating.rs index 6ed106d..e664ff8 100644 --- a/translatable_shared/src/misc/templating.rs +++ b/translatable_shared/src/misc/templating.rs @@ -30,9 +30,12 @@ pub struct FormatString { impl FormatString { pub fn replace_with(mut self, values: HashMap) -> String { + let mut replacements: Vec<(String, std::ops::Range)> = + self.spans.into_iter().collect(); + replacements.sort_by_key(|(_key, range)| range.start); let mut offset = 0isize; - for (key, range) in self.spans { + for (key, range) in replacements { 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; From f76e92009977c64d927bbef6fe60ca6815d44581 Mon Sep 17 00:00:00 2001 From: chikof Date: Tue, 15 Apr 2025 19:29:04 +0100 Subject: [PATCH 087/228] chore: range is already imported --- translatable_shared/src/misc/templating.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/translatable_shared/src/misc/templating.rs b/translatable_shared/src/misc/templating.rs index e664ff8..13410f0 100644 --- a/translatable_shared/src/misc/templating.rs +++ b/translatable_shared/src/misc/templating.rs @@ -30,7 +30,7 @@ pub struct FormatString { impl FormatString { pub fn replace_with(mut self, values: HashMap) -> String { - let mut replacements: Vec<(String, std::ops::Range)> = + let mut replacements: Vec<(String, Range)> = self.spans.into_iter().collect(); replacements.sort_by_key(|(_key, range)| range.start); let mut offset = 0isize; From 365db456cf8d0b4077309e8c8a899122a9708f3b Mon Sep 17 00:00:00 2001 From: stifskere Date: Tue, 15 Apr 2025 21:33:48 +0200 Subject: [PATCH 088/228] chore: replace type definition by torpedo --- translatable_shared/src/misc/templating.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/translatable_shared/src/misc/templating.rs b/translatable_shared/src/misc/templating.rs index 13410f0..0b41d3d 100644 --- a/translatable_shared/src/misc/templating.rs +++ b/translatable_shared/src/misc/templating.rs @@ -30,9 +30,9 @@ pub struct FormatString { impl FormatString { pub fn replace_with(mut self, values: HashMap) -> String { - let mut replacements: Vec<(String, Range)> = - self.spans.into_iter().collect(); + let mut replacements = self.spans.into_iter().collect::)>>(); replacements.sort_by_key(|(_key, range)| range.start); + let mut offset = 0isize; for (key, range) in replacements { From 51e6243f65e30e62e1fb3771e024ebb9691ee410 Mon Sep 17 00:00:00 2001 From: stifskere Date: Wed, 16 Apr 2025 05:02:01 +0200 Subject: [PATCH 089/228] chore: review fixes --- translatable_proc/src/data/translations.rs | 2 +- .../src/macro_generation/translation.rs | 2 +- .../src/macro_input/translation.rs | 4 +- translatable_shared/src/macros/collections.rs | 2 +- translatable_shared/src/misc/language.rs | 122 +++++++++--------- .../src/translations/collection.rs | 2 +- 6 files changed, 67 insertions(+), 67 deletions(-) diff --git a/translatable_proc/src/data/translations.rs b/translatable_proc/src/data/translations.rs index eec3506..5e08697 100644 --- a/translatable_proc/src/data/translations.rs +++ b/translatable_proc/src/data/translations.rs @@ -45,7 +45,7 @@ pub enum TranslationDataError { span = _0 .span() .map(|range| format!("on {}:{}", range.start, range.end)) - .unwrap_or_else(|| String::new()) + .unwrap_or_else(String::new) )] ParseToml(TomlError, String), diff --git a/translatable_proc/src/macro_generation/translation.rs b/translatable_proc/src/macro_generation/translation.rs index 5e83a48..a719887 100644 --- a/translatable_proc/src/macro_generation/translation.rs +++ b/translatable_proc/src/macro_generation/translation.rs @@ -29,7 +29,7 @@ pub fn translation_macro(input: TranslationMacroArgs) -> TokenStream2 { let static_path_display = path.join("::"); let translation_object = translations - .find_path(&path) + .find_path(path) .ok_or_else(|| MacroCompileError::PathNotFound(static_path_display.clone())); let translation = diff --git a/translatable_proc/src/macro_input/translation.rs b/translatable_proc/src/macro_input/translation.rs index 6349098..a96b5c4 100644 --- a/translatable_proc/src/macro_input/translation.rs +++ b/translatable_proc/src/macro_input/translation.rs @@ -78,7 +78,7 @@ impl TranslationMacroArgsError { /// used in a `parse_macro_input!` call. impl Parse for TranslationMacroArgs { fn parse(input: ParseStream) -> SynResult { - let parsed_langauge_arg = match input.parse::()? { + let parsed_language_arg = match input.parse::()? { Expr::Lit(ExprLit { lit: Lit::Str(literal), .. }) => { match literal.value().parse::() { Ok(language) => InputType::Static(language), @@ -129,7 +129,7 @@ impl Parse for TranslationMacroArgs { } Ok(Self { - language: parsed_langauge_arg, + language: parsed_language_arg, path: parsed_path_arg, replacements, }) diff --git a/translatable_shared/src/macros/collections.rs b/translatable_shared/src/macros/collections.rs index 98351b0..6ef93d7 100644 --- a/translatable_shared/src/macros/collections.rs +++ b/translatable_shared/src/macros/collections.rs @@ -5,7 +5,7 @@ use quote::{ToTokens, quote}; #[inline] pub fn map_to_tokens(map: &HashMap) -> TokenStream2 { - let map = map.into_iter().map(|(key, value)| { + let map = map.iter().map(|(key, value)| { let key = key.into_token_stream(); let value = value.into_token_stream(); diff --git a/translatable_shared/src/misc/language.rs b/translatable_shared/src/misc/language.rs index ca07390..d1b58b9 100644 --- a/translatable_shared/src/misc/language.rs +++ b/translatable_shared/src/misc/language.rs @@ -3,6 +3,67 @@ use quote::{ToTokens, TokenStreamExt, quote}; use strum::{Display, EnumIter, EnumString, IntoEnumIterator}; use syn::Ident; +/// This struct represents a list of similar languages to the provided one. +pub struct Similarities { + /// Indicates how many languages are not included in the list. + overflow_by: usize, + /// List of similar languages. + similarities: Vec, +} + +impl Similarities { + pub fn overflow_by(&self) -> usize { + self.overflow_by + } + + pub fn similarities(&self) -> &[T] { + &self.similarities + } +} + +impl Language { + /// This method returns a list of similar languages to the provided one. + pub fn get_similarities(lang: &str, max_amount: usize) -> Similarities { + let all_similarities = Self::iter() + .map(|variant| format!("{variant:#} ({variant:?})")) + .filter(|variant| variant.contains(lang)) + .collect::>(); + + let overflow_by = all_similarities.len() as i32 - max_amount as i32; + + if overflow_by > 0 { + Similarities { + similarities: all_similarities.into_iter().take(max_amount).collect(), + overflow_by: overflow_by as usize, + } + } else { + Similarities { + similarities: all_similarities, + overflow_by: 0, + } + } + } +} + +impl PartialEq for Language { + fn eq(&self, other: &String) -> bool { + format!("{self:?}").to_lowercase() == other.to_lowercase() + } +} + +/// 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: @@ -379,64 +440,3 @@ pub enum Language { #[strum(serialize = "Zulu", serialize = "zu")] ZU, } - -/// This struct represents a list of similar languages to the provided one. -pub struct Similarities { - /// Indicates how many languages are not included in the list. - overflow_by: usize, - /// List of similar languages. - similarities: Vec, -} - -impl Similarities { - pub fn overflow_by(&self) -> usize { - self.overflow_by - } - - pub fn similarities(&self) -> &[T] { - &self.similarities - } -} - -impl Language { - /// This method returns a list of similar languages to the provided one. - pub fn get_similarities(lang: &str, max_amount: usize) -> Similarities { - let all_similarities = Self::iter() - .map(|variant| format!("{variant:#} ({variant:?})")) - .filter(|variant| variant.contains(lang)) - .collect::>(); - - let overflow_by = all_similarities.len() as i32 - max_amount as i32; - - if overflow_by > 0 { - Similarities { - similarities: all_similarities.into_iter().take(max_amount).collect(), - overflow_by: overflow_by as usize, - } - } else { - Similarities { - similarities: all_similarities, - overflow_by: 0, - } - } - } -} - -impl PartialEq for Language { - fn eq(&self, other: &String) -> bool { - format!("{self:?}").to_lowercase() == other.to_lowercase() - } -} - -/// 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 }) - } -} diff --git a/translatable_shared/src/translations/collection.rs b/translatable_shared/src/translations/collection.rs index 0daaa4e..3b07abf 100644 --- a/translatable_shared/src/translations/collection.rs +++ b/translatable_shared/src/translations/collection.rs @@ -31,7 +31,7 @@ impl TranslationNodeCollection { /// in that specific file. #[allow(unused)] pub fn get_node(&self, path: &str) -> Option<&TranslationNode> { - self.0.get(&path.to_string()) + self.0.get(path) } /// This method is used to load a specific translation From 9bfb070e4e798c394ec3c7b123555d0cae16ae17 Mon Sep 17 00:00:00 2001 From: stifskere Date: Wed, 16 Apr 2025 05:22:39 +0200 Subject: [PATCH 090/228] fix: indentation on macro attribute invocation --- translatable_proc/src/data/translations.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/translatable_proc/src/data/translations.rs b/translatable_proc/src/data/translations.rs index 5e08697..e1ef263 100644 --- a/translatable_proc/src/data/translations.rs +++ b/translatable_proc/src/data/translations.rs @@ -44,7 +44,7 @@ pub enum TranslationDataError { reason = _0.message(), span = _0 .span() - .map(|range| format!("on {}:{}", range.start, range.end)) + .map(|range| format!("on {}:{}", range.start, range.end)) .unwrap_or_else(String::new) )] ParseToml(TomlError, String), From 711016378ead466be2f8bf61b632ae9fcf311ac4 Mon Sep 17 00:00:00 2001 From: stifskere Date: Wed, 16 Apr 2025 08:33:34 +0200 Subject: [PATCH 091/228] feat: move template validation to compile time and changed `cargo fmt` to wrap lines --- Cargo.lock | 7 -- rustfmt.toml | 1 + translatable/src/error.rs | 40 ++++++-- translatable/tests/test.rs | 2 +- translatable_proc/src/data/config.rs | 23 ++++- translatable_proc/src/data/translations.rs | 11 ++- .../src/macro_generation/translation.rs | 50 +++++----- .../src/macro_input/translation.rs | 23 ++++- translatable_shared/Cargo.toml | 1 - translatable_shared/src/macros/collections.rs | 18 ++-- translatable_shared/src/misc/language.rs | 5 +- translatable_shared/src/misc/templating.rs | 91 +++++++++---------- .../src/translations/collection.rs | 12 ++- translatable_shared/src/translations/node.rs | 28 +++--- 14 files changed, 186 insertions(+), 126 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5c6210e..776f488 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -255,7 +255,6 @@ dependencies = [ "syn", "thiserror", "toml", - "unicode-xid", ] [[package]] @@ -279,12 +278,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "winapi-util" version = "0.1.9" diff --git a/rustfmt.toml b/rustfmt.toml index 4951559..66f96ac 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -25,6 +25,7 @@ format_strings = true match_arm_leading_pipes = "Preserve" match_block_trailing_comma = true trailing_comma = "Vertical" +chain_width = 0 # Miscellaneous condense_wildcard_suffixes = true diff --git a/translatable/src/error.rs b/translatable/src/error.rs index dc7b193..936203e 100644 --- a/translatable/src/error.rs +++ b/translatable/src/error.rs @@ -3,22 +3,50 @@ use translatable_shared::misc::language::Language; use translatable_shared::misc::templating::TemplateError; use translatable_shared::translations::node::TranslationNodeError; -/// This enum is used to debug runtime errors generated -/// by the macros in runtime, the error message can be obtained -/// using the `Display` trait of the enum itself. +/// 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. #[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. #[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 run time language + /// validity, check `Error::LanguageNotAvailable` + /// for that purpose. #[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 run time. #[error("The language '{0:?}' ('{0:#}') is not available for the path '{1}'")] LanguageNotAvailable(Language, String), - - #[error("An error has occurred while parsing templates: {0:#}")] - TemplateMissmatch(#[from] TemplateError), } impl RuntimeError { diff --git a/translatable/tests/test.rs b/translatable/tests/test.rs index 1284126..d843f96 100644 --- a/translatable/tests/test.rs +++ b/translatable/tests/test.rs @@ -7,7 +7,7 @@ const RESULT: &str = "Β‘Hola John Doe!"; #[test] fn both_static() { let result = translation!("es", static common::greeting, name = NAME, surname = SURNAME); - assert!(result.unwrap() == RESULT); + assert!(result == RESULT); } #[test] diff --git a/translatable_proc/src/data/config.rs b/translatable_proc/src/data/config.rs index 419c18b..d029a29 100644 --- a/translatable_proc/src/data/config.rs +++ b/translatable_proc/src/data/config.rs @@ -122,24 +122,37 @@ pub fn load_config() -> Result<&'static MacroConfig, ConfigError> { } // Load base configuration from TOML file - let toml_content = - read_to_string("./translatable.toml").unwrap_or_default().parse::
()?; + 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())) + .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())); + .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())) + value + .parse() + .map_err(|_| ConfigError::InvalidValue($key.into(), value.into())) } else { Ok($default) } diff --git a/translatable_proc/src/data/translations.rs b/translatable_proc/src/data/translations.rs index e1ef263..98a6ea9 100644 --- a/translatable_proc/src/data/translations.rs +++ b/translatable_proc/src/data/translations.rs @@ -74,9 +74,16 @@ fn walk_dir(path: &str) -> Result, TranslationDataError> { for entry in directory { let path = entry.path(); if path.is_dir() { - stack.push(path.to_str().ok_or(TranslationDataError::InvalidUnicode)?.to_string()); + stack.push( + path.to_str() + .ok_or(TranslationDataError::InvalidUnicode)? + .to_string(), + ); } else { - result.push(path.to_string_lossy().to_string()); + result.push( + path.to_string_lossy() + .to_string(), + ); } } } diff --git a/translatable_proc/src/macro_generation/translation.rs b/translatable_proc/src/macro_generation/translation.rs index a719887..34a8e08 100644 --- a/translatable_proc/src/macro_generation/translation.rs +++ b/translatable_proc/src/macro_generation/translation.rs @@ -2,7 +2,7 @@ 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 translatable_shared::macros::collections::{map_to_tokens, map_transform_to_tokens}; use translatable_shared::misc::language::Language; use crate::data::translations::load_translations; @@ -21,8 +21,10 @@ enum MacroCompileError { pub fn translation_macro(input: TranslationMacroArgs) -> TokenStream2 { let translations = handle_macro_result!(load_translations()); - let template_replacements = - input.replacements().iter().map(|(key, value)| quote! { #key = #value }); + 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() { @@ -32,27 +34,28 @@ pub fn translation_macro(input: TranslationMacroArgs) -> TokenStream2 { .find_path(path) .ok_or_else(|| MacroCompileError::PathNotFound(static_path_display.clone())); - let translation = - handle_macro_result!(translation_object).get(language).ok_or_else(|| { - MacroCompileError::LanguageNotAvailable( - language.clone(), - static_path_display.clone(), - ) - }); - - let translation = handle_macro_result!(translation).into_token_stream(); + 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! { - translatable::shared::replace_templates!( - #translation, - #(#template_replacements),* - ) + #translation + .replace_with(#template_replacements) }; } } let language = match input.language() { - InputType::Static(language) => language.clone().to_token_stream(), + InputType::Static(language) => language + .clone() + .to_token_stream(), InputType::Dynamic(language) => quote! { translatable::shared::misc::language::Language::from(#language) }, @@ -96,15 +99,10 @@ pub fn translation_macro(input: TranslationMacroArgs) -> TokenStream2 { #[doc(hidden)] let language = #language; - translatable::shared::replace_templates!( - { - #translation_object - .get(&language) - .ok_or_else(|| translatable::Error::LanguageNotAvailable(language, path.join("::")))? - .to_string() - }, - #(#template_replacements),* - )? + #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/translation.rs b/translatable_proc/src/macro_input/translation.rs index a96b5c4..14e7e36 100644 --- a/translatable_proc/src/macro_input/translation.rs +++ b/translatable_proc/src/macro_input/translation.rs @@ -80,7 +80,10 @@ 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::() { + match literal + .value() + .parse::() + { Ok(language) => InputType::Static(language), Err(_) => Err(TranslationMacroArgsError::InvalidIsoLiteral(literal.value()) @@ -100,7 +103,9 @@ impl Parse for TranslationMacroArgs { .segments .into_iter() .map(|segment| match segment.arguments { - PathArguments::None => Ok(segment.ident.to_string()), + PathArguments::None => Ok(segment + .ident + .to_string()), other => Err(TranslationMacroArgsError::InvalidPathContainsGenerics .into_syn_error(other)), @@ -110,7 +115,11 @@ impl Parse for TranslationMacroArgs { InputType::Static(language_arg) }, - Err(_) => InputType::Dynamic(input.parse::()?.to_token_stream()), + Err(_) => InputType::Dynamic( + input + .parse::()? + .to_token_stream(), + ), }; let mut replacements = HashMap::new(); @@ -119,9 +128,13 @@ impl Parse for TranslationMacroArgs { input.parse::()?; let key = input.parse::()?; let value = match input.parse::() { - Ok(_) => input.parse::()?.to_token_stream(), + Ok(_) => input + .parse::()? + .to_token_stream(), - Err(_) => key.clone().into_token_stream(), + Err(_) => key + .clone() + .into_token_stream(), }; replacements.insert(key, value); diff --git a/translatable_shared/Cargo.toml b/translatable_shared/Cargo.toml index 62e4efb..c8133fb 100644 --- a/translatable_shared/Cargo.toml +++ b/translatable_shared/Cargo.toml @@ -15,4 +15,3 @@ strum = { version = "0.27.1", features = ["derive", "strum_macros"] } syn = { version = "2.0.100", features = ["full"] } thiserror = "2.0.12" toml = "0.8.20" -unicode-xid = "0.2.6" diff --git a/translatable_shared/src/macros/collections.rs b/translatable_shared/src/macros/collections.rs index 6ef93d7..6d9016e 100644 --- a/translatable_shared/src/macros/collections.rs +++ b/translatable_shared/src/macros/collections.rs @@ -5,12 +5,14 @@ use quote::{ToTokens, quote}; #[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(); + let map = map + .iter() + .map(|(key, value)| { + let key = key.into_token_stream(); + let value = value.into_token_stream(); - quote! { (#key, #value) } - }); + quote! { (#key, #value) } + }); quote! { vec![#(#map),*] @@ -22,11 +24,11 @@ pub fn map_to_tokens(map: &HashMap) -> TokenStre #[inline] pub fn map_transform_to_tokens(map: &HashMap, predicate: F) -> TokenStream2 where - K: ToTokens, - V: ToTokens, F: Fn(&K, &V) -> TokenStream2, { - let processed = map.iter().map(|(key, value)| predicate(key, value)); + let processed = map + .iter() + .map(|(key, value)| predicate(key, value)); quote! { vec![#(#processed),*] diff --git a/translatable_shared/src/misc/language.rs b/translatable_shared/src/misc/language.rs index d1b58b9..a314987 100644 --- a/translatable_shared/src/misc/language.rs +++ b/translatable_shared/src/misc/language.rs @@ -33,7 +33,10 @@ impl Language { if overflow_by > 0 { Similarities { - similarities: all_similarities.into_iter().take(max_amount).collect(), + similarities: all_similarities + .into_iter() + .take(max_amount) + .collect(), overflow_by: overflow_by as usize, } } else { diff --git a/translatable_shared/src/misc/templating.rs b/translatable_shared/src/misc/templating.rs index 0b41d3d..f0324a3 100644 --- a/translatable_shared/src/misc/templating.rs +++ b/translatable_shared/src/misc/templating.rs @@ -2,9 +2,12 @@ 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; -use unicode_xid::UnicodeXID; + +use crate::macros::collections::map_transform_to_tokens; #[derive(Error, Debug)] pub enum TemplateError { @@ -14,13 +17,6 @@ pub enum TemplateError { #[error("Found template with key '{0}' which is an invalid identifier")] InvalidIdent(String), - - // Compile errors - #[error("Found template with a non compliant XID key, found invalid start character '{0}'")] - InvalidxIdStart(char), - - #[error("Found template with a non compliant XID key, found invalid rest character '{0}'")] - InvalidxIdRest(char), } pub struct FormatString { @@ -29,24 +25,35 @@ pub struct FormatString { } impl FormatString { - pub fn replace_with(mut self, values: HashMap) -> String { - let mut replacements = self.spans.into_iter().collect::)>>(); - replacements.sort_by_key(|(_key, range)| range.start); + pub fn from_data(original: &str, spans: HashMap>) -> Self { + Self { original: original.to_string(), spans } + } + + pub fn replace_with(&self, values: HashMap) -> String { + let mut original = self + .original + .clone(); + let mut spans = self + .spans + .iter() + .map(|(key, value)| (key.clone(), value.clone())) + .collect::)>>(); + spans.sort_by_key(|(_key, range)| range.start); let mut offset = 0isize; - for (key, range) in replacements { + 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; - self.original.replace_range(start..end, value); + original.replace_range(start..end, value); offset += value.len() as isize - (range.end - range.start) as isize; } } - self.original + original } } @@ -57,11 +64,17 @@ impl FromStr for FormatString { let original = s.to_string(); let mut spans = HashMap::new(); - let char_to_byte = s.char_indices().map(|(i, _)| i).collect::>(); + 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() { + 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. @@ -100,40 +113,22 @@ impl FromStr for FormatString { } } -#[macro_export] -macro_rules! replace_templates { - ($orig:expr, $($key:ident = $value:expr),* $(,)?) => {{ - $orig.parse::<$crate::misc::templating::FormatString>() - .map(|parsed| parsed - .replace_with( - vec![$((stringify!($key).to_string(), $value.to_string())),*] - .into_iter() - .collect::>() - ) - ) - }} -} - -pub fn validate_format_string(original: &str) -> Result<(), TemplateError> { - let mut last_bracket_idx = 0usize; - - for (i, c) in original.chars().enumerate() { - match (c, last_bracket_idx) { - ('{', lbi) if lbi != i.saturating_sub(1) => last_bracket_idx = i, - ('{', lbi) if lbi == i.saturating_sub(1) => last_bracket_idx = 0, - ('}', lbi) if lbi != 0 => last_bracket_idx = 0, +impl ToTokens for FormatString { + fn to_tokens(&self, tokens: &mut TokenStream2) { + let original = &self.original; - (c, lbi) if i > 0 && lbi == i - 1 && !c.is_xid_start() => { - return Err(TemplateError::InvalidxIdStart(c)); - }, + let span_map = map_transform_to_tokens(&self.spans, |key, range| { + let range_start = range.start; + let range_end = range.end; - (c, lbi) if lbi != 0 && !c.is_xid_continue() => { - return Err(TemplateError::InvalidxIdRest(c)); - }, + quote! { (#key.to_string(), #range_start..#range_end) } + }); - _ => {}, - } + tokens.append_all(quote! { + translatable::shared::misc::templating::FormatString::from_data( + #original, + #span_map + ) + }); } - - Ok(()) } diff --git a/translatable_shared/src/translations/collection.rs b/translatable_shared/src/translations/collection.rs index 3b07abf..ea372e8 100644 --- a/translatable_shared/src/translations/collection.rs +++ b/translatable_shared/src/translations/collection.rs @@ -31,7 +31,8 @@ impl TranslationNodeCollection { /// in that specific file. #[allow(unused)] pub fn get_node(&self, path: &str) -> Option<&TranslationNode> { - self.0.get(path) + self.0 + .get(path) } /// This method is used to load a specific translation @@ -47,7 +48,9 @@ impl TranslationNodeCollection { /// 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)) + self.0 + .values() + .find_map(|node| node.find_path(path)) } } @@ -55,7 +58,10 @@ impl TranslationNodeCollection { /// in a `TranslationNodeCollection`. impl FromIterator<(String, TranslationNode)> for TranslationNodeCollection { fn from_iter>(iter: T) -> Self { - Self(iter.into_iter().collect()) + Self( + iter.into_iter() + .collect(), + ) } } diff --git a/translatable_shared/src/translations/node.rs b/translatable_shared/src/translations/node.rs index 00ab4e6..7d01d06 100644 --- a/translatable_shared/src/translations/node.rs +++ b/translatable_shared/src/translations/node.rs @@ -6,9 +6,9 @@ use strum::ParseError; use thiserror::Error; use toml::{Table, Value}; -use crate::macros::collections::map_transform_to_tokens; +use crate::macros::collections::{map_to_tokens, map_transform_to_tokens}; use crate::misc::language::Language; -use crate::misc::templating::{TemplateError, validate_format_string}; +use crate::misc::templating::{FormatString, TemplateError}; /// Errors occurring during TOML-to-translation structure transformation #[derive(Error, Debug)] @@ -31,11 +31,10 @@ pub enum TranslationNodeError { } pub type TranslationNesting = HashMap; -pub type TranslationObject = HashMap; +pub type TranslationObject = HashMap; /// Represents nested translation structure, /// as it is on the translation files. -#[derive(Clone)] pub enum TranslationNode { /// Nested namespace containing other translation objects Nesting(TranslationNesting), @@ -52,14 +51,21 @@ impl TranslationNode { /// # Returns /// 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::>(); + 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()) + nested + .get(first)? + .find_path(&rest.to_vec()) }, - Self::Translation(translation) => path.is_empty().then_some(translation), + Self::Translation(translation) => path + .is_empty() + .then_some(translation), } } } @@ -86,10 +92,7 @@ impl ToTokens for TranslationNode { }, TranslationNode::Translation(translation) => { - let map = map_transform_to_tokens( - translation, - |key, value| quote! { (#key, #value.to_string()) }, - ); + let map = map_to_tokens(translation); tokens.append_all(quote! { translatable::shared::translations::node::TranslationNode::Translation( @@ -118,8 +121,7 @@ impl TryFrom
for TranslationNode { match result { Self::Translation(translation) => { - validate_format_string(&translation_value)?; - translation.insert(key.parse()?, translation_value); + translation.insert(key.parse()?, translation_value.parse()?); }, Self::Nesting(_) => return Err(TranslationNodeError::InvalidNesting), From 0c4244c2e17f5e153e24b799363fbe3a761b4ae4 Mon Sep 17 00:00:00 2001 From: stifskere Date: Wed, 16 Apr 2025 14:56:50 +0200 Subject: [PATCH 092/228] docs: translatable/src/error.rs --- translatable/src/error.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/translatable/src/error.rs b/translatable/src/error.rs index 936203e..52923b4 100644 --- a/translatable/src/error.rs +++ b/translatable/src/error.rs @@ -50,17 +50,16 @@ pub enum RuntimeError { } impl RuntimeError { - /// This method makes use of the `Display` implemeted in - /// `Error` to display the formatted cause String of - /// the specific error. + /// Runtime error display helper. /// - /// This method is marked as `cold`, because in the application - /// there should be the least amount of errors possible, - /// when displaying the error, please do in a lazy - /// error handling method such as `ok_or_else` or `inspect_err`. + /// 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 - /// The cause heap allocated String. + /// A heap allocated string containing + /// the cause of the error. #[cold] #[inline] pub fn cause(&self) -> String { From a9f6ee3939622e627b1a13240236a26b5e124bf2 Mon Sep 17 00:00:00 2001 From: stifskere Date: Wed, 16 Apr 2025 15:15:29 +0200 Subject: [PATCH 093/228] docs: added module description to error.rs --- translatable/src/error.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/translatable/src/error.rs b/translatable/src/error.rs index 52923b4..a2ccd2e 100644 --- a/translatable/src/error.rs +++ b/translatable/src/error.rs @@ -1,6 +1,12 @@ +//! 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::misc::templating::TemplateError; use translatable_shared::translations::node::TranslationNodeError; /// Macro runtime error handling. @@ -30,7 +36,7 @@ pub enum RuntimeError { /// The specified path may not be found /// in any of the translation files. /// - /// This is not related to run time language + /// This is not related to runtime language /// validity, check `Error::LanguageNotAvailable` /// for that purpose. #[error("The path '{0}' could not be found")] @@ -44,7 +50,7 @@ pub enum RuntimeError { /// 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 run time. + /// time. In that case we don't reach runtime. #[error("The language '{0:?}' ('{0:#}') is not available for the path '{1}'")] LanguageNotAvailable(Language, String), } From 1bbe64e295597f54b19700c516966d99728c1a72 Mon Sep 17 00:00:00 2001 From: stifskere Date: Wed, 16 Apr 2025 15:21:34 +0200 Subject: [PATCH 094/228] docs translatable/src/lib.rs --- translatable/src/lib.rs | 40 ++++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/translatable/src/lib.rs b/translatable/src/lib.rs index d828bee..5094e50 100644 --- a/translatable/src/lib.rs +++ b/translatable/src/lib.rs @@ -1,18 +1,34 @@ +//! # Translatable +//! +//! A robust internationalization solution for +//! Rust featuring compile-time validation, +//! ISO 639-1 compliance, and TOML-based +//! translation management. + mod error; -// A re-export to the runtime error -// enum for user availabilty and -// debugging. +/// Runtime error re-export. +/// +/// This `use` statement renames +/// the run time error as a common +/// error by rust practice and exports +/// it. pub use error::RuntimeError as Error; -/// A re-export from the Language enum -/// for users to dynamically parse -/// when using dynamic arguments. -pub use shared::misc::language::Language; -/// A re-export to the translation macro -/// exported in the proc_macro module. + +/// 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. pub use translatable_proc::translation; -/// A re-export of all the shared modules -/// as declared in the shared crate used -/// for macro generation. + +/// User-facing util re-exports. +/// +/// This `use` statement re-exports +/// all the shared module items that +/// are useful for the end-user. +pub use shared::misc::language::Language; + #[doc(hidden)] pub use translatable_shared as shared; From bf197ea6d92087bfec5ff96cf9b93f6c65cfe8e1 Mon Sep 17 00:00:00 2001 From: stifskere Date: Wed, 16 Apr 2025 15:55:33 +0200 Subject: [PATCH 095/228] docs: change method standards --- translatable/src/error.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/translatable/src/error.rs b/translatable/src/error.rs index a2ccd2e..2d0db54 100644 --- a/translatable/src/error.rs +++ b/translatable/src/error.rs @@ -63,7 +63,7 @@ impl RuntimeError { /// monads such as `ok_or_else` or any /// other `or_else` method. /// - /// # Returns + /// **Returns** /// A heap allocated string containing /// the cause of the error. #[cold] From cb626e639ea2b0e691f7ab438388af56c47e120a Mon Sep 17 00:00:00 2001 From: stifskere Date: Wed, 16 Apr 2025 19:15:14 +0200 Subject: [PATCH 096/228] docs: change enum documentation format --- translatable/src/error.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/translatable/src/error.rs b/translatable/src/error.rs index 2d0db54..e5abaf9 100644 --- a/translatable/src/error.rs +++ b/translatable/src/error.rs @@ -27,7 +27,12 @@ pub enum RuntimeError { /// template validation... /// /// `Display` directly forwards the inner - /// error `Display` value. + /// error `Display` value. The enum implements + /// `From` to wrap the + /// original error. + /// + /// **Parameters** + /// * `0` - The TranslationNodeError derivation. #[error("{0:#}")] TranslationNode(#[from] TranslationNodeError), @@ -39,6 +44,10 @@ pub enum RuntimeError { /// This is not related to runtime language /// validity, check `Error::LanguageNotAvailable` /// for that purpose. + /// + /// **Parameters** + /// * `0` - The path that could not be found + /// appended with it's separator. #[error("The path '{0}' could not be found")] PathNotFound(String), @@ -51,6 +60,11 @@ pub enum RuntimeError { /// 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), } From 92f7addb8e56e9d628f930d539c43ee5595ea7d4 Mon Sep 17 00:00:00 2001 From: stifskere Date: Wed, 16 Apr 2025 19:52:39 +0200 Subject: [PATCH 097/228] docs: translatable_proc/src/data/config.rs --- translatable_proc/src/data/config.rs | 161 ++++++++++++++++++++------- 1 file changed, 121 insertions(+), 40 deletions(-) diff --git a/translatable_proc/src/data/config.rs b/translatable_proc/src/data/config.rs index d029a29..d917944 100644 --- a/translatable_proc/src/data/config.rs +++ b/translatable_proc/src/data/config.rs @@ -1,7 +1,8 @@ -//! Configuration loading and handling for translatable content +//! User configuration module. //! -//! This module provides functionality to load and manage configuration -//! settings for localization/translation workflows from a TOML file. +//! 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; @@ -13,28 +14,78 @@ use thiserror::Error; use toml::Table; use toml::de::Error as TomlError; -/// Errors that can occur during configuration loading +/// Configuration error enum. +/// +/// Used for compiletime 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 occurred while reading configuration file + /// 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 wrap + /// the error. + /// + /// **Parameters** + /// * `0` - The IO error derivation. #[error("IO error reading configuration: {0:#}")] Io(#[from] IoError), - /// TOML parsing error with location information + /// 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 wrap + /// the error. + /// + /// **Parameters** + /// * `0` - The TOML deserialization error derivation. #[error( "TOML parse error '{}'{}", .0.message(), - .0.span().map(|l| format!(" in ./translatable.toml:{}:{}", l.start, l.end)) + .0.span() + .map(|l| format!(" in ./translatable.toml:{}:{}", l.start, l.end)) .unwrap_or_else(|| "".into()) )] ParseToml(#[from] TomlError), - /// Invalid environment variable value for configuration options + /// 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), } -/// File search order strategy +/// 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) @@ -45,7 +96,13 @@ pub enum SeekMode { Unalphabetical, } -/// Translation conflict resolution strategy +/// 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) @@ -56,9 +113,16 @@ pub enum TranslationOverlap { Ignore, } -/// Main configuration structure for translation system +/// 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 directory containing translation files + /// Path to the directory containing translation files. + /// + /// Specifies the base location where the system will search for + /// translation files. /// /// # Example /// ```toml @@ -66,62 +130,76 @@ pub struct MacroConfig { /// ``` path: String, - /// File processing order strategy + /// File processing order strategy. /// - /// Default: alphabetical file processing + /// Defines the order in which translation files are processed. + /// Default: alphabetical order. seek_mode: SeekMode, - /// Translation conflict resolution strategy + /// Translation conflict resolution strategy. /// - /// Determines behavior when multiple files contain the same translation - /// path + /// Determines the behavior when multiple files contain the same + /// translation key. overlap: TranslationOverlap, } impl MacroConfig { - /// Get reference to configured locales path + /// 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 current seek mode strategy + /// 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 current overlap resolution strategy + /// 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 +/// 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 configuration from file or use defaults +/// 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. /// -/// # Implementation Notes -/// - Uses `OnceLock` for thread-safe singleton initialization -/// - Missing config file is not considered an error -/// - Config file must be named `translatable.toml` in root directory -/// - Environment variables take precedence over TOML configuration -/// - Supported environment variables: -/// - `TRANSLATABLE_LOCALES_PATH`: Overrides translation directory path -/// - `TRANSLATABLE_SEEK_MODE`: Sets file processing order ("alphabetical" or -/// "unalphabetical") -/// - `TRANSLATABLE_OVERLAP`: Sets conflict strategy ("overwrite" or "ignore") +/// Disclaimers: +/// - Must be called before any translation logic that depends on configuration. +/// - If `translatable.toml` is malformed or contains invalid values, the function +/// will return an error. /// -/// # Panics -/// Will not panic but returns ConfigError for: -/// - Malformed TOML syntax -/// - Filesystem permission issues -/// - Invalid environment variable values +/// **Returns** +/// `Ok(&MacroConfig)` β€” if the configuration is successfully loaded or already cached. +/// `Err(ConfigError)` β€” if loading or parsing fails. pub fn load_config() -> Result<&'static MacroConfig, ConfigError> { if let Some(config) = TRANSLATABLE_CONFIG.get() { return Ok(config); } - // Load base configuration from TOML file let toml_content = read_to_string("./translatable.toml") .unwrap_or_default() .parse::
()?; @@ -160,7 +238,11 @@ pub fn load_config() -> Result<&'static MacroConfig, ConfigError> { } let config = MacroConfig { - path: config_value!("TRANSLATABLE_LOCALES_PATH", "path", "./translations"), + path: config_value!( + "TRANSLATABLE_LOCALES_PATH", + "path", + "./translations" + ), overlap: config_value!(parse( "TRANSLATABLE_OVERLAP", "overlap", @@ -173,6 +255,5 @@ pub fn load_config() -> Result<&'static MacroConfig, ConfigError> { ))?, }; - // Freeze configuration in global cache Ok(TRANSLATABLE_CONFIG.get_or_init(|| config)) } From 4e8196c8c3c8a323cb8f661010b1371ba369a90d Mon Sep 17 00:00:00 2001 From: stifskere Date: Wed, 16 Apr 2025 20:17:49 +0200 Subject: [PATCH 098/228] docs: translatable_proc/src/data/mod.rs --- translatable_proc/src/data/mod.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/translatable_proc/src/data/mod.rs b/translatable_proc/src/data/mod.rs index d40b299..dbb15d2 100644 --- a/translatable_proc/src/data/mod.rs +++ b/translatable_proc/src/data/mod.rs @@ -1,2 +1,13 @@ +//! 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; From 48eb0c9e32a4cb4c0c65b60ff2c0ff8a8009eba3 Mon Sep 17 00:00:00 2001 From: stifskere Date: Thu, 17 Apr 2025 03:12:07 +0200 Subject: [PATCH 099/228] docs: translatable_proc/src/data/translations.rs --- translatable/src/error.rs | 8 +- translatable/src/lib.rs | 15 +-- translatable_proc/src/data/config.rs | 34 +++-- translatable_proc/src/data/translations.rs | 141 ++++++++++++++++----- 4 files changed, 135 insertions(+), 63 deletions(-) diff --git a/translatable/src/error.rs b/translatable/src/error.rs index e5abaf9..a93f653 100644 --- a/translatable/src/error.rs +++ b/translatable/src/error.rs @@ -27,9 +27,11 @@ pub enum RuntimeError { /// template validation... /// /// `Display` directly forwards the inner - /// error `Display` value. The enum implements - /// `From` to wrap the - /// original error. + /// error `Display` value. + /// + /// The enum implements + /// `From` to allow + /// conversion from `TranslationNodeError`. /// /// **Parameters** /// * `0` - The TranslationNodeError derivation. diff --git a/translatable/src/lib.rs b/translatable/src/lib.rs index 5094e50..b1473ea 100644 --- a/translatable/src/lib.rs +++ b/translatable/src/lib.rs @@ -14,7 +14,12 @@ mod error; /// error by rust practice and exports /// it. pub use error::RuntimeError as Error; - +/// User-facing util re-exports. +/// +/// This `use` statement re-exports +/// all the shared module items that +/// are useful for the end-user. +pub use shared::misc::language::Language; /// Macro re-exports. /// /// This `use` statement re-exports @@ -22,13 +27,5 @@ pub use error::RuntimeError as Error; /// which only work if included from /// this module due to path generation. pub use translatable_proc::translation; - -/// User-facing util re-exports. -/// -/// This `use` statement re-exports -/// all the shared module items that -/// are useful for the end-user. -pub use shared::misc::language::Language; - #[doc(hidden)] pub use translatable_shared as shared; diff --git a/translatable_proc/src/data/config.rs b/translatable_proc/src/data/config.rs index d917944..6b18416 100644 --- a/translatable_proc/src/data/config.rs +++ b/translatable_proc/src/data/config.rs @@ -16,7 +16,7 @@ use toml::de::Error as TomlError; /// Configuration error enum. /// -/// Used for compiletime configuration +/// Used for compile time configuration /// errors, such as errors while opening /// files or parsing a file format. /// @@ -30,9 +30,10 @@ pub enum ConfigError { /// with the file system. /// /// `Display` forwards the inner error `Display` - /// value with some prefix text. The enum - /// implements `From` to wrap - /// the error. + /// value with some prefix text. + /// + /// The enum implements `From` to + /// allow conversion from `std::io::Error`. /// /// **Parameters** /// * `0` - The IO error derivation. @@ -46,9 +47,10 @@ pub enum ConfigError { /// /// The error is formatted displaying /// the file name hardcoded as `./translatable.toml` - /// and appended with the line and character. The - /// enum implements `From` to wrap - /// the error. + /// 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. @@ -187,14 +189,12 @@ static TRANSLATABLE_CONFIG: OnceLock = OnceLock::new(); /// The configuration is cached after the first successful load, and reused on /// subsequent calls. /// -/// Disclaimers: -/// - Must be called before any translation logic that depends on configuration. -/// - If `translatable.toml` is malformed or contains invalid values, the function -/// will return an error. -/// /// **Returns** -/// `Ok(&MacroConfig)` β€” if the configuration is successfully loaded or already cached. -/// `Err(ConfigError)` β€” if loading or parsing fails. +/// 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. pub fn load_config() -> Result<&'static MacroConfig, ConfigError> { if let Some(config) = TRANSLATABLE_CONFIG.get() { return Ok(config); @@ -238,11 +238,7 @@ pub fn load_config() -> Result<&'static MacroConfig, ConfigError> { } let config = MacroConfig { - path: config_value!( - "TRANSLATABLE_LOCALES_PATH", - "path", - "./translations" - ), + path: config_value!("TRANSLATABLE_LOCALES_PATH", "path", "./translations"), overlap: config_value!(parse( "TRANSLATABLE_OVERLAP", "overlap", diff --git a/translatable_proc/src/data/translations.rs b/translatable_proc/src/data/translations.rs index 98a6ea9..deb12ae 100644 --- a/translatable_proc/src/data/translations.rs +++ b/translatable_proc/src/data/translations.rs @@ -1,3 +1,13 @@ +//! 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; @@ -10,35 +20,69 @@ use translatable_shared::translations::node::{TranslationNode, TranslationNodeEr use super::config::{ConfigError, SeekMode, TranslationOverlap, load_config}; -/// Contains error definitions for what may go wrong while -/// loading a translation. +/// 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 { - /// Represents a generic IO error, if a file couldn't - /// be opened, a path could not be found... This error - /// will be inferred from an `std::io::Error`. + /// 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 `IoError` representation + /// prefixed with additional context. + /// + /// The enum implements `From` to allow + /// automatic conversion from `IoError`. + /// + /// **Parameters** + /// * `0` β€” The underlying I/O error. #[error("There was a problem with an IO operation: {0:#}")] - SystemIo(#[from] IoError), + Io(#[from] IoError), - /// Used to convert any configuration loading error - /// into a `TranslationDataError`, error messages are - /// handled by the `ConfigError` itself. + /// 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. #[error("{0:#}")] LoadConfig(#[from] ConfigError), - /// Specific conversion error for when a path can't be converted - /// to a manipulable string because it contains invalid - /// unicode characters. + /// 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, - /// Represents a TOML deserialization error, this happens while - /// loading files and converting their content to TOML. + /// 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. /// - /// # Arguments - /// * `.0` - The RAW toml::de::Error returned by the deserialization - /// function. - /// * `.1` - The path where the file was originally found. + /// **Parameters** + /// * `0` β€” The `TomlError` 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(), @@ -49,25 +93,47 @@ pub enum TranslationDataError { )] 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 +/// 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 directory to find all translation files +/// 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 +/// **Arguments** +/// * `path` β€” Root directory to scan for translation files. /// -/// # Returns -/// Vec of file paths or TranslationError +/// **Returns** +/// A `Result` containing either: +/// * `Ok(Vec)` β€” A flat list of discovered file paths. +/// * `Err(TranslationDataError)` β€” If traversal fails at any point. fn walk_dir(path: &str) -> Result, TranslationDataError> { let mut stack = vec![path.to_string()]; let mut result = Vec::new(); - // Use iterative approach to avoid recursion depth limits while let Some(current_path) = stack.pop() { let directory = read_dir(¤t_path)?.collect::, _>>()?; @@ -91,15 +157,26 @@ fn walk_dir(path: &str) -> Result, TranslationDataError> { Ok(result) } -/// Loads and caches translations from configured directory +/// 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. /// -/// # Returns -/// Reference to cached translations or TranslationError +/// This function will return a reference to the cached translations +/// on every subsequent call. /// -/// # Implementation Details -/// - Uses OnceLock for thread-safe initialization -/// - Applies sorting based on configuration -/// - Handles file parsing and validation +/// **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. pub fn load_translations() -> Result<&'static TranslationNodeCollection, TranslationDataError> { if let Some(translations) = TRANSLATIONS.get() { return Ok(translations); From a51b2b7da6393ab8044bb46a1d722a339e4ad90a Mon Sep 17 00:00:00 2001 From: stifskere Date: Thu, 17 Apr 2025 03:16:03 +0200 Subject: [PATCH 100/228] docs: translatable_proc/src/macro_generation/mod.rs --- translatable_proc/src/macro_generation/mod.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/translatable_proc/src/macro_generation/mod.rs b/translatable_proc/src/macro_generation/mod.rs index f5648e8..80119a8 100644 --- a/translatable_proc/src/macro_generation/mod.rs +++ b/translatable_proc/src/macro_generation/mod.rs @@ -1 +1,9 @@ +//! 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. + pub mod translation; From 177fca857a1ce6b55f367562fe069a7336614952 Mon Sep 17 00:00:00 2001 From: stifskere Date: Thu, 17 Apr 2025 04:28:33 +0200 Subject: [PATCH 101/228] docs: translatable_proc/src/macro_generation/translation.rs --- translatable/src/error.rs | 2 +- translatable_proc/src/data/config.rs | 2 +- .../src/macro_generation/translation.rs | 43 +++++++++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/translatable/src/error.rs b/translatable/src/error.rs index a93f653..31cdee2 100644 --- a/translatable/src/error.rs +++ b/translatable/src/error.rs @@ -12,7 +12,7 @@ use translatable_shared::translations::node::TranslationNodeError; /// Macro runtime error handling. /// /// Used in `translation!(...)` invocations for non -/// compile time validations and errors. +/// compile-time validations and errors. /// /// Use the `Display` implementation to obtain the /// error message, `self.cause()` is available as diff --git a/translatable_proc/src/data/config.rs b/translatable_proc/src/data/config.rs index 6b18416..dac9e8e 100644 --- a/translatable_proc/src/data/config.rs +++ b/translatable_proc/src/data/config.rs @@ -16,7 +16,7 @@ use toml::de::Error as TomlError; /// Configuration error enum. /// -/// Used for compile time configuration +/// Used for compile-time configuration /// errors, such as errors while opening /// files or parsing a file format. /// diff --git a/translatable_proc/src/macro_generation/translation.rs b/translatable_proc/src/macro_generation/translation.rs index 34a8e08..d562f1f 100644 --- a/translatable_proc/src/macro_generation/translation.rs +++ b/translatable_proc/src/macro_generation/translation.rs @@ -1,3 +1,9 @@ +//! `translation!()` macro output module. +//! +//! This module contains the required for +//! the generation of the `translation!()` macro tokens +//! with intrinsics from `macro_input::translation.rs`. + use proc_macro2::TokenStream as TokenStream2; use quote::{ToTokens, quote}; use thiserror::Error; @@ -9,15 +15,52 @@ use crate::data::translations::load_translations; use crate::macro_input::input_type::InputType; use crate::macro_input::translation::TranslationMacroArgs; +/// Macro compile-time translation resolution error. +/// +/// Represents errors that can occur while compiling translation macros. +/// 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 `translatable!()` macro. #[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. pub fn translation_macro(input: TranslationMacroArgs) -> TokenStream2 { let translations = handle_macro_result!(load_translations()); From b470361b40155a22d0568ccc6cac1363790c90fd Mon Sep 17 00:00:00 2001 From: stifskere Date: Thu, 17 Apr 2025 06:37:17 +0200 Subject: [PATCH 102/228] docs: add type links to all documentation --- translatable/src/error.rs | 44 ++++++++++++------- translatable/src/lib.rs | 19 +++++--- translatable_proc/src/data/config.rs | 34 ++++++++------ translatable_proc/src/data/mod.rs | 2 +- translatable_proc/src/data/translations.rs | 42 +++++++++++------- translatable_proc/src/macro_generation/mod.rs | 4 +- .../src/macro_generation/translation.rs | 3 +- .../src/macro_input/input_type.rs | 5 +++ 8 files changed, 100 insertions(+), 53 deletions(-) diff --git a/translatable/src/error.rs b/translatable/src/error.rs index 31cdee2..3552de3 100644 --- a/translatable/src/error.rs +++ b/translatable/src/error.rs @@ -11,30 +11,38 @@ use translatable_shared::translations::node::TranslationNodeError; /// Macro runtime error handling. /// -/// Used in `translation!(...)` invocations for non +/// 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 +/// 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 + /// [`TranslationNode`] construction + /// failure, usually nesting missmatch, invalid /// template validation... /// - /// `Display` directly forwards the inner - /// error `Display` value. + /// [`Display`] directly forwards the inner + /// error [`Display`] value. /// /// The enum implements - /// `From` to allow - /// conversion from `TranslationNodeError`. + /// [`From`] to allow + /// conversion from + /// [`TranslationNodeError`]. /// /// **Parameters** - /// * `0` - The TranslationNodeError derivation. + /// * `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), @@ -44,12 +52,14 @@ pub enum RuntimeError { /// in any of the translation files. /// /// This is not related to runtime language - /// validity, check `Error::LanguageNotAvailable` + /// 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), @@ -59,8 +69,8 @@ pub enum RuntimeError { /// 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 + /// 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** @@ -74,14 +84,16 @@ pub enum RuntimeError { impl RuntimeError { /// Runtime error display helper. /// - /// This method is marked as `cold` + /// This method is marked as `#[cold]` /// so it should be called lazily with - /// monads such as `ok_or_else` or any + /// monads such as [`ok_or_else`] or any /// other `or_else` method. /// /// **Returns** - /// A heap allocated string containing + /// 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 { diff --git a/translatable/src/lib.rs b/translatable/src/lib.rs index b1473ea..845b19a 100644 --- a/translatable/src/lib.rs +++ b/translatable/src/lib.rs @@ -13,19 +13,26 @@ mod error; /// the run time error as a common /// error by rust practice and exports /// it. +#[rustfmt::skip] pub use error::RuntimeError as Error; -/// User-facing util re-exports. -/// -/// This `use` statement re-exports -/// all the shared module items that -/// are useful for the end-user. -pub use shared::misc::language::Language; + /// 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; + +/// User-facing util re-exports. +/// +/// This `use` statement re-exports +/// all the shared module items that +/// are useful for the end-user. +#[rustfmt::skip] +pub use shared::misc::language::Language; + #[doc(hidden)] +#[rustfmt::skip] pub use translatable_shared as shared; diff --git a/translatable_proc/src/data/config.rs b/translatable_proc/src/data/config.rs index dac9e8e..95515be 100644 --- a/translatable_proc/src/data/config.rs +++ b/translatable_proc/src/data/config.rs @@ -29,14 +29,17 @@ pub enum ConfigError { /// Usually errors while interacting /// with the file system. /// - /// `Display` forwards the inner error `Display` + /// [`Display`] forwards the inner error [`Display`] /// value with some prefix text. /// - /// The enum implements `From` to - /// allow conversion from `std::io::Error`. + /// 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), @@ -49,11 +52,13 @@ pub enum ConfigError { /// 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` + /// 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(), @@ -173,15 +178,15 @@ impl MacroConfig { /// 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. +/// 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. +/// 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. @@ -191,10 +196,13 @@ static TRANSLATABLE_CONFIG: OnceLock = OnceLock::new(); /// /// **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 +/// * [`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); diff --git a/translatable_proc/src/data/mod.rs b/translatable_proc/src/data/mod.rs index dbb15d2..af291da 100644 --- a/translatable_proc/src/data/mod.rs +++ b/translatable_proc/src/data/mod.rs @@ -5,7 +5,7 @@ //! related configuration. //! //! The only thing that should possibly -//! be used outside is the `translations` +//! be used outside is the [`translations`] //! module, as the config is mostly //! to read the translations from the files. diff --git a/translatable_proc/src/data/translations.rs b/translatable_proc/src/data/translations.rs index deb12ae..21bb9bf 100644 --- a/translatable_proc/src/data/translations.rs +++ b/translatable_proc/src/data/translations.rs @@ -35,14 +35,17 @@ pub enum TranslationDataError { /// Raised when an I/O operation fails during translation /// retrieval, typically caused by filesystem-level issues. /// - /// `Display` will forward the inner `IoError` representation - /// prefixed with additional context. + /// [`Display`] will forward the inner [`std::io::Error`] + /// representation prefixed with additional context. /// - /// The enum implements `From` to allow + /// 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("There was a problem with an IO operation: {0:#}")] Io(#[from] IoError), @@ -52,13 +55,15 @@ pub enum TranslationDataError { /// successfully, typically due to invalid values or missing /// configuration data. /// - /// `Display` will forward the inner `ConfigError` message. + /// [`Display`] will forward the inner [`ConfigError`] message. /// - /// The enum implements `From` to allow automatic + /// 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), @@ -81,7 +86,8 @@ pub enum TranslationDataError { /// the location within the file (if available), and the file path. /// /// **Parameters** - /// * `0` β€” The `TomlError` carrying the underlying deserialization error. + /// * `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}", @@ -99,7 +105,7 @@ pub enum TranslationDataError { /// a translation node, typically due to invalid formatting /// or missing expected data. /// - /// The enum implements `From` for + /// The enum implements [`From`] for /// seamless conversion. /// /// **Parameters** @@ -111,14 +117,14 @@ pub enum TranslationDataError { /// 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 +/// 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. +/// Paths are returned as [`String`] values, ready for processing. /// /// Any filesystem errors, invalid paths, or read failures are reported /// via `TranslationDataError`. @@ -128,8 +134,11 @@ static TRANSLATIONS: OnceLock = OnceLock::new(); /// /// **Returns** /// A `Result` containing either: -/// * `Ok(Vec)` β€” A flat list of discovered file paths. -/// * `Err(TranslationDataError)` β€” If traversal fails at any point. +/// * [`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(); @@ -166,17 +175,20 @@ fn walk_dir(path: &str) -> Result, TranslationDataError> { /// - 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 +/// 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 +/// 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); diff --git a/translatable_proc/src/macro_generation/mod.rs b/translatable_proc/src/macro_generation/mod.rs index 80119a8..583ff00 100644 --- a/translatable_proc/src/macro_generation/mod.rs +++ b/translatable_proc/src/macro_generation/mod.rs @@ -4,6 +4,8 @@ //! 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. +//! from the [`macro_input`] module. +//! +//! [`macro_input`]: crate::macro_input pub mod translation; diff --git a/translatable_proc/src/macro_generation/translation.rs b/translatable_proc/src/macro_generation/translation.rs index d562f1f..97638fa 100644 --- a/translatable_proc/src/macro_generation/translation.rs +++ b/translatable_proc/src/macro_generation/translation.rs @@ -32,7 +32,8 @@ enum MacroCompileError { #[error("The path '{0}' could not be found")] PathNotFound(String), - /// The requested language is not available for the provided translation path. + /// The requested language is not available for the provided translation + /// path. /// /// **Parameters** /// * `0` β€” The requested `Language`. diff --git a/translatable_proc/src/macro_input/input_type.rs b/translatable_proc/src/macro_input/input_type.rs index 455dc8b..44b63df 100644 --- a/translatable_proc/src/macro_input/input_type.rs +++ b/translatable_proc/src/macro_input/input_type.rs @@ -1,3 +1,8 @@ +//! 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; /// This enum abstracts (in the programming sense) From 09e871ce73afdba57f974f9a81a0a35eb1561708 Mon Sep 17 00:00:00 2001 From: stifskere Date: Thu, 17 Apr 2025 19:05:26 +0200 Subject: [PATCH 103/228] docs: translatable_proc/src/macro_input/input_type.rs --- .../src/macro_input/input_type.rs | 55 +++++++++++++++---- 1 file changed, 43 insertions(+), 12 deletions(-) diff --git a/translatable_proc/src/macro_input/input_type.rs b/translatable_proc/src/macro_input/input_type.rs index 44b63df..2754ac2 100644 --- a/translatable_proc/src/macro_input/input_type.rs +++ b/translatable_proc/src/macro_input/input_type.rs @@ -1,29 +1,60 @@ //! 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. +//! 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; -/// This enum abstracts (in the programming sense) -/// the logic on separating between what's considered -/// dynamic and static while parsing the abstract -/// (in the conceptual sense) macro input. +/// 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), } -impl> InputType { - /// This method allows converting the - /// enum value whether it's conceptually - /// dynamic or static into its dynamic - /// represented as a `TokenStream` +/// [`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.into(), + Self::Static(value) => value.to_token_stream(), Self::Dynamic(value) => value, } } From 678431191591eda70eb21e348b8b526a8282a654 Mon Sep 17 00:00:00 2001 From: stifskere Date: Thu, 17 Apr 2025 19:10:37 +0200 Subject: [PATCH 104/228] docs: translatable_proc/src/macro_input/mod.rs --- translatable_proc/src/macro_generation/mod.rs | 2 ++ translatable_proc/src/macro_input/mod.rs | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/translatable_proc/src/macro_generation/mod.rs b/translatable_proc/src/macro_generation/mod.rs index 583ff00..a3a3ef1 100644 --- a/translatable_proc/src/macro_generation/mod.rs +++ b/translatable_proc/src/macro_generation/mod.rs @@ -6,6 +6,8 @@ //! modules may be issued with intrinsics //! from the [`macro_input`] module. //! +//! Each module represents a single macro. +//! //! [`macro_input`]: crate::macro_input pub mod translation; diff --git a/translatable_proc/src/macro_input/mod.rs b/translatable_proc/src/macro_input/mod.rs index 1725820..5ae5e87 100644 --- a/translatable_proc/src/macro_input/mod.rs +++ b/translatable_proc/src/macro_input/mod.rs @@ -1,2 +1,14 @@ +//! 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 input_type; pub mod translation; From d853ef9212236e5f6a01201755d0dcb03aa5b494 Mon Sep 17 00:00:00 2001 From: stifskere Date: Thu, 17 Apr 2025 22:19:43 +0200 Subject: [PATCH 105/228] docs: translatable_proc/src/macro_input/translation.rs --- translatable/tests/test.rs | 1 + .../src/macro_generation/translation.rs | 10 ++- .../src/macro_input/translation.rs | 64 +++++++++++++------ translatable_shared/src/macros/errors.rs | 13 ++-- 4 files changed, 60 insertions(+), 28 deletions(-) diff --git a/translatable/tests/test.rs b/translatable/tests/test.rs index d843f96..21af7e7 100644 --- a/translatable/tests/test.rs +++ b/translatable/tests/test.rs @@ -20,6 +20,7 @@ fn language_static_path_dynamic() { 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); } diff --git a/translatable_proc/src/macro_generation/translation.rs b/translatable_proc/src/macro_generation/translation.rs index 97638fa..33931ce 100644 --- a/translatable_proc/src/macro_generation/translation.rs +++ b/translatable_proc/src/macro_generation/translation.rs @@ -1,8 +1,10 @@ -//! `translation!()` macro output module. +//! [`translation!()`] macro output module. //! //! This module contains the required for //! the generation of the `translation!()` macro tokens //! with intrinsics from `macro_input::translation.rs`. +//! +//! [`translation!()`]: crate::translation use proc_macro2::TokenStream as TokenStream2; use quote::{ToTokens, quote}; @@ -17,12 +19,14 @@ use crate::macro_input::translation::TranslationMacroArgs; /// Macro compile-time translation resolution error. /// -/// Represents errors that can occur while compiling translation macros. +/// Represents errors that can occur while compiling the [`translation!()`]. /// 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 `translatable!()` macro. +/// for immediate feedback while invoking the [`translation!()`] macro. +/// +/// [`translation!()`]: crate::translation #[derive(Error, Debug)] enum MacroCompileError { /// The requested translation path could not be found. diff --git a/translatable_proc/src/macro_input/translation.rs b/translatable_proc/src/macro_input/translation.rs index 14e7e36..f1c1555 100644 --- a/translatable_proc/src/macro_input/translation.rs +++ b/translatable_proc/src/macro_input/translation.rs @@ -1,3 +1,11 @@ +//! [`translation!()`] output generation 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; @@ -5,7 +13,6 @@ use quote::ToTokens; use syn::parse::{Parse, ParseStream}; use syn::token::Static; use syn::{ - Error as SynError, Expr, ExprLit, Ident, @@ -17,15 +24,15 @@ use syn::{ }; use thiserror::Error; use translatable_shared::misc::language::Language; +use translatable_shared::macros::errors::IntoCompileError; use super::input_type::InputType; -/// Used to represent errors on parsing a `TranslationMacroArgs` -/// using `parse_macro_input!`. +/// Parse error for [`TranslationMacroArgs`]. /// -/// The enum implements a helper function to convert itself -/// to a `syn` spanned error, so this enum isn't directly -/// exposed as the `syn::Error` instance takes place. +/// 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. #[derive(Error, Debug)] enum TranslationMacroArgsError { /// An error while parsing a compile-time String value @@ -34,15 +41,23 @@ enum TranslationMacroArgsError { InvalidIsoLiteral(String), /// Extra tokens were found while parsing a static path for - /// the `translation!` macro, specifically generic arguments. + /// the [`translation!()`] macro, specifically generic arguments. + /// + /// [`translation!()`]: crate::translation #[error("This translation path contains generic arguments, and cannot be parsed")] InvalidPathContainsGenerics, } -/// The `TranslationMacroArgs` struct is used to represent -/// the `translation!` macro parsed arguments, it's sole -/// purpose is to be used with `parse_macro_input!` with the -/// `Parse` implementation the structure has. +/// [`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 @@ -56,7 +71,7 @@ pub struct TranslationMacroArgs { /// 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 - /// `TokenStream`. + /// [`TokenStream2`]. path: InputType>, /// Stores the replacement arguments for the translation @@ -67,15 +82,10 @@ pub struct TranslationMacroArgs { replacements: HashMap, } -impl TranslationMacroArgsError { - pub fn into_syn_error(self, span: T) -> SynError { - SynError::new_spanned(span, self.to_string()) - } -} - -/// The implementation is used to achieve the -/// sole purpose this structure has, which is being -/// used in a `parse_macro_input!` call. +/// [`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::()? { @@ -150,18 +160,30 @@ impl Parse for TranslationMacroArgs { } 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 { diff --git a/translatable_shared/src/macros/errors.rs b/translatable_shared/src/macros/errors.rs index 42842fb..b3c4445 100644 --- a/translatable_shared/src/macros/errors.rs +++ b/translatable_shared/src/macros/errors.rs @@ -1,23 +1,28 @@ use std::fmt::Display; use proc_macro2::TokenStream as TokenStream2; -use quote::quote; +use quote::{quote, ToTokens}; +use syn::Error as SynError; /// Implements a helper function to convert /// anything that implements Display into /// a generated `compile_error!` in macros. pub trait IntoCompileError where - Self: Display, + Self: Display + Sized, { /// Transforms the value into a string /// and wraps `compile_error!` into it /// for it to be returned when an error /// happens - fn into_compile_error(&self) -> TokenStream2 { + fn to_compile_error(&self) -> TokenStream2 { let message = self.to_string(); quote! { std::compile_error!(#message) } } + + fn into_syn_error(self, span: T) -> SynError { + SynError::new_spanned(span, self.to_string()) + } } impl IntoCompileError for T {} @@ -29,7 +34,7 @@ macro_rules! handle_macro_result { match $val { std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => return error.into_compile_error(), + std::result::Result::Err(error) => return error.to_compile_error(), } }}; } From b4600daf080c6b5b9bc83f9a6e06461f61c33ac9 Mon Sep 17 00:00:00 2001 From: stifskere Date: Thu, 17 Apr 2025 22:46:36 +0200 Subject: [PATCH 106/228] docs: translatable_proc/src/lib.rs --- translatable_proc/src/lib.rs | 58 ++++++++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/translatable_proc/src/lib.rs b/translatable_proc/src/lib.rs index 0bd8c66..82dec19 100644 --- a/translatable_proc/src/lib.rs +++ b/translatable_proc/src/lib.rs @@ -1,11 +1,11 @@ -//! Internationalization library providing compile-time and runtime translation -//! facilities +//! Macro declarations for the `translatable` crate. //! -//! # Features -//! - TOML-based translation files -//! - ISO 639-1 language validation -//! - Configurable loading strategies -//! - Procedural macro for compile-time checking +//! 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. use proc_macro::TokenStream; use syn::parse_macro_input; @@ -17,16 +17,44 @@ mod data; mod macro_generation; mod macro_input; -/// Procedural macro for compile-time translation validation +/// **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`. /// -/// # Usage -/// ```ignore -/// translation!("en", static some::path) -/// ``` +/// Replacement parameters are not validated, if a parameter exists it will be replaced +/// otherwise it won't. /// -/// # Parameters -/// - Language code/literal -/// - Translation path (supports static analysis) +/// **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() From 3033a785bc1c383409f94350daab5a1472fc1205 Mon Sep 17 00:00:00 2001 From: stifskere Date: Fri, 18 Apr 2025 00:54:55 +0200 Subject: [PATCH 107/228] docs: translatable_shared/src/macros/collections.rs --- translatable_proc/src/lib.rs | 16 +++++--- .../src/macro_input/translation.rs | 13 +----- translatable_shared/src/macros/collections.rs | 41 +++++++++++++++++++ translatable_shared/src/macros/errors.rs | 2 +- 4 files changed, 54 insertions(+), 18 deletions(-) diff --git a/translatable_proc/src/lib.rs b/translatable_proc/src/lib.rs index 82dec19..74d7267 100644 --- a/translatable_proc/src/lib.rs +++ b/translatable_proc/src/lib.rs @@ -26,9 +26,11 @@ mod macro_input; /// **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` +/// * `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. +/// * `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. @@ -37,8 +39,10 @@ mod macro_input; /// - 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 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. /// @@ -48,8 +52,8 @@ mod macro_input; /// 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. +/// Replacement parameters are not validated, if a parameter exists it will be +/// replaced otherwise it won't. /// /// **Returns** /// A `Result` containing either: diff --git a/translatable_proc/src/macro_input/translation.rs b/translatable_proc/src/macro_input/translation.rs index f1c1555..f62537f 100644 --- a/translatable_proc/src/macro_input/translation.rs +++ b/translatable_proc/src/macro_input/translation.rs @@ -12,19 +12,10 @@ use proc_macro2::TokenStream as TokenStream2; use quote::ToTokens; use syn::parse::{Parse, ParseStream}; use syn::token::Static; -use syn::{ - Expr, - ExprLit, - Ident, - Lit, - Path, - PathArguments, - Result as SynResult, - Token, -}; +use syn::{Expr, ExprLit, Ident, Lit, Path, PathArguments, Result as SynResult, Token}; use thiserror::Error; -use translatable_shared::misc::language::Language; use translatable_shared::macros::errors::IntoCompileError; +use translatable_shared::misc::language::Language; use super::input_type::InputType; diff --git a/translatable_shared/src/macros/collections.rs b/translatable_shared/src/macros/collections.rs index 6d9016e..c1b09b3 100644 --- a/translatable_shared/src/macros/collections.rs +++ b/translatable_shared/src/macros/collections.rs @@ -1,8 +1,28 @@ +//! 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 @@ -21,6 +41,27 @@ pub fn map_to_tokens(map: &HashMap) -> TokenStre } } +/// [`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 diff --git a/translatable_shared/src/macros/errors.rs b/translatable_shared/src/macros/errors.rs index b3c4445..d7c987b 100644 --- a/translatable_shared/src/macros/errors.rs +++ b/translatable_shared/src/macros/errors.rs @@ -1,7 +1,7 @@ use std::fmt::Display; use proc_macro2::TokenStream as TokenStream2; -use quote::{quote, ToTokens}; +use quote::{ToTokens, quote}; use syn::Error as SynError; /// Implements a helper function to convert From c1c0d1b5ef2027b1848c06003ac73b121de79652 Mon Sep 17 00:00:00 2001 From: stifskere Date: Fri, 18 Apr 2025 01:40:52 +0200 Subject: [PATCH 108/228] docs: translatable_shared/src/macros/errors.rs --- .../src/macro_input/translation.rs | 4 +- translatable_shared/src/macros/errors.rs | 50 ++++++++++++++++--- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/translatable_proc/src/macro_input/translation.rs b/translatable_proc/src/macro_input/translation.rs index f62537f..bd072c0 100644 --- a/translatable_proc/src/macro_input/translation.rs +++ b/translatable_proc/src/macro_input/translation.rs @@ -88,7 +88,7 @@ impl Parse for TranslationMacroArgs { Ok(language) => InputType::Static(language), Err(_) => Err(TranslationMacroArgsError::InvalidIsoLiteral(literal.value()) - .into_syn_error(literal))?, + .to_syn_error(literal))?, } }, @@ -109,7 +109,7 @@ impl Parse for TranslationMacroArgs { .to_string()), other => Err(TranslationMacroArgsError::InvalidPathContainsGenerics - .into_syn_error(other)), + .to_syn_error(other)), }) .collect::, _>>()?; diff --git a/translatable_shared/src/macros/errors.rs b/translatable_shared/src/macros/errors.rs index d7c987b..0b88eee 100644 --- a/translatable_shared/src/macros/errors.rs +++ b/translatable_shared/src/macros/errors.rs @@ -1,32 +1,70 @@ +//! 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; -/// Implements a helper function to convert -/// anything that implements Display into -/// a generated `compile_error!` in macros. +/// 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 + /// and wraps [`compile_error!`] into it /// for it to be returned when an error - /// happens + /// happens. + /// + /// **Returns** + /// A [`compile_error!`] wrapped `&str`. fn to_compile_error(&self) -> TokenStream2 { let message = self.to_string(); quote! { std::compile_error!(#message) } } - fn into_syn_error(self, span: T) -> SynError { + /// 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`. + 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. +/// +/// [`to_compile_error`]: IntoCompileError::to_compile_error #[macro_export] macro_rules! handle_macro_result { ($val:expr) => {{ From de6cc5695ea194deca8f6d2ccc860ae0e1f1d0d1 Mon Sep 17 00:00:00 2001 From: stifskere Date: Fri, 18 Apr 2025 01:43:12 +0200 Subject: [PATCH 109/228] docs: translatable_shared/src/macros/mod.rs --- translatable_shared/src/macros/mod.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/translatable_shared/src/macros/mod.rs b/translatable_shared/src/macros/mod.rs index e1d0e77..885c4fc 100644 --- a/translatable_shared/src/macros/mod.rs +++ b/translatable_shared/src/macros/mod.rs @@ -1,2 +1,13 @@ +//! 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; From d54a26cf64d2a8e2eebf90d65a6a4e58794ee167 Mon Sep 17 00:00:00 2001 From: stifskere Date: Fri, 18 Apr 2025 02:36:54 +0200 Subject: [PATCH 110/228] feat: removed unused implementations in language and documented it's module --- translatable_shared/src/macros/errors.rs | 2 + translatable_shared/src/misc/language.rs | 59 +++--------------------- 2 files changed, 9 insertions(+), 52 deletions(-) diff --git a/translatable_shared/src/macros/errors.rs b/translatable_shared/src/macros/errors.rs index 0b88eee..dc63564 100644 --- a/translatable_shared/src/macros/errors.rs +++ b/translatable_shared/src/macros/errors.rs @@ -29,6 +29,7 @@ where /// /// **Returns** /// A [`compile_error!`] wrapped `&str`. + #[cold] fn to_compile_error(&self) -> TokenStream2 { let message = self.to_string(); quote! { std::compile_error!(#message) } @@ -45,6 +46,7 @@ where /// /// **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()) } diff --git a/translatable_shared/src/misc/language.rs b/translatable_shared/src/misc/language.rs index a314987..b61ecc5 100644 --- a/translatable_shared/src/misc/language.rs +++ b/translatable_shared/src/misc/language.rs @@ -1,59 +1,14 @@ +//! [`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, IntoEnumIterator}; +use strum::{Display, EnumIter, EnumString}; use syn::Ident; -/// This struct represents a list of similar languages to the provided one. -pub struct Similarities { - /// Indicates how many languages are not included in the list. - overflow_by: usize, - /// List of similar languages. - similarities: Vec, -} - -impl Similarities { - pub fn overflow_by(&self) -> usize { - self.overflow_by - } - - pub fn similarities(&self) -> &[T] { - &self.similarities - } -} - -impl Language { - /// This method returns a list of similar languages to the provided one. - pub fn get_similarities(lang: &str, max_amount: usize) -> Similarities { - let all_similarities = Self::iter() - .map(|variant| format!("{variant:#} ({variant:?})")) - .filter(|variant| variant.contains(lang)) - .collect::>(); - - let overflow_by = all_similarities.len() as i32 - max_amount as i32; - - if overflow_by > 0 { - Similarities { - similarities: all_similarities - .into_iter() - .take(max_amount) - .collect(), - overflow_by: overflow_by as usize, - } - } else { - Similarities { - similarities: all_similarities, - overflow_by: 0, - } - } - } -} - -impl PartialEq for Language { - fn eq(&self, other: &String) -> bool { - format!("{self:?}").to_lowercase() == other.to_lowercase() - } -} - /// This implementation converts the tagged union /// to an equivalent call from the runtime context. /// From 79a4614a085442520fa4c687332379195726f445 Mon Sep 17 00:00:00 2001 From: stifskere Date: Fri, 18 Apr 2025 02:48:13 +0200 Subject: [PATCH 111/228] docs: translatable_shared/src/misc/mod.rs --- translatable_shared/src/misc/mod.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/translatable_shared/src/misc/mod.rs b/translatable_shared/src/misc/mod.rs index d7724b6..778f7c6 100644 --- a/translatable_shared/src/misc/mod.rs +++ b/translatable_shared/src/misc/mod.rs @@ -1,2 +1,8 @@ +//! 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; From fbd65cb65818c8389070f37d3ccb6e7e2ee73cde Mon Sep 17 00:00:00 2001 From: stifskere Date: Fri, 18 Apr 2025 03:51:01 +0200 Subject: [PATCH 112/228] chore: sync --- translatable_shared/src/misc/templating.rs | 48 +++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/translatable_shared/src/misc/templating.rs b/translatable_shared/src/misc/templating.rs index f0324a3..134c23b 100644 --- a/translatable_shared/src/misc/templating.rs +++ b/translatable_shared/src/misc/templating.rs @@ -1,3 +1,10 @@ +//! 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; @@ -9,18 +16,57 @@ use thiserror::Error; use crate::macros::collections::map_transform_to_tokens; +/// 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 { - // Runtime errors + /// 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 keping the alignment with `self.spans`. original: String, + + /// Template spans. + /// + /// This hashmap contains the ranges + /// of the templates found in the + /// original string, for the sake + /// of replacing them in a copy + /// of the original string. spans: HashMap>, } From f3f1930149e37910abe3e393d172fc5d26c1cbfe Mon Sep 17 00:00:00 2001 From: stifskere Date: Fri, 18 Apr 2025 04:04:17 +0200 Subject: [PATCH 113/228] fix: templates only supported one of each key due to HashMap limits --- translatable/tests/test.rs | 2 +- translatable_shared/src/misc/templating.rs | 32 +++++++++++----------- translations/test.toml | 4 +-- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/translatable/tests/test.rs b/translatable/tests/test.rs index 21af7e7..60e7785 100644 --- a/translatable/tests/test.rs +++ b/translatable/tests/test.rs @@ -2,7 +2,7 @@ use translatable::{Language, translation}; const NAME: &str = "John"; const SURNAME: &str = "Doe"; -const RESULT: &str = "Β‘Hola John Doe!"; +const RESULT: &str = "Β‘Hola John Doe! Mi nombre es John Doe {{hola}}"; #[test] fn both_static() { diff --git a/translatable_shared/src/misc/templating.rs b/translatable_shared/src/misc/templating.rs index 134c23b..efe01c2 100644 --- a/translatable_shared/src/misc/templating.rs +++ b/translatable_shared/src/misc/templating.rs @@ -67,11 +67,11 @@ pub struct FormatString { /// original string, for the sake /// of replacing them in a copy /// of the original string. - spans: HashMap>, + spans: Vec<(String, Range)>, } impl FormatString { - pub fn from_data(original: &str, spans: HashMap>) -> Self { + pub fn from_data(original: &str, spans: Vec<(String, Range)>) -> Self { Self { original: original.to_string(), spans } } @@ -79,11 +79,8 @@ impl FormatString { let mut original = self .original .clone(); - let mut spans = self - .spans - .iter() - .map(|(key, value)| (key.clone(), value.clone())) - .collect::)>>(); + + let mut spans = self.spans.clone(); spans.sort_by_key(|(_key, range)| range.start); let mut offset = 0isize; @@ -108,7 +105,7 @@ impl FromStr for FormatString { fn from_str(s: &str) -> Result { let original = s.to_string(); - let mut spans = HashMap::new(); + let mut spans = Vec::new(); let char_to_byte = s .char_indices() @@ -134,12 +131,12 @@ impl FromStr for FormatString { ('}', Some(open_idx)) => { let key = current_tmpl_key.clone(); - spans.insert( + spans.push(( parse_str::(&key) .map_err(|_| TemplateError::InvalidIdent(key))? .to_string(), char_to_byte[open_idx]..char_to_byte[char_idx + 1], // inclusive - ); + )); last_bracket_idx = None; current_tmpl_key.clear(); @@ -163,17 +160,20 @@ impl ToTokens for FormatString { fn to_tokens(&self, tokens: &mut TokenStream2) { let original = &self.original; - let span_map = map_transform_to_tokens(&self.spans, |key, range| { - let range_start = range.start; - let range_end = range.end; + let span_map = self + .spans + .iter() + .map(|(key, range)| { + let start = range.start; + let end = range.end; - quote! { (#key.to_string(), #range_start..#range_end) } - }); + quote! { (#key.to_string(), #start..#end) } + }); tokens.append_all(quote! { translatable::shared::misc::templating::FormatString::from_data( #original, - #span_map + vec![#(#span_map),*] ) }); } diff --git a/translations/test.toml b/translations/test.toml index 1d45fc1..9ac8197 100644 --- a/translations/test.toml +++ b/translations/test.toml @@ -4,5 +4,5 @@ en = "Welcome to our app!" es = "Β‘Bienvenido a nuestra aplicaciΓ³n!" [common.greeting] -en = "Hello {name} {surname}!" -es = "Β‘Hola {name} {surname}!" +en = "Hello {name} {surname}! My name is {name} {surname} {{hello}}" +es = "Β‘Hola {name} {surname}! Mi nombre es {name} {surname} {{hola}}" From 34f02360ef30b9ede07165653ca4e1f2cedb53cd Mon Sep 17 00:00:00 2001 From: stifskere Date: Fri, 18 Apr 2025 18:28:41 +0200 Subject: [PATCH 114/228] docs: translatable_shared/src/misc/templating.rs --- translatable_proc/src/data/config.rs | 9 ++-- translatable_shared/src/misc/templating.rs | 56 +++++++++++++++++++--- 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/translatable_proc/src/data/config.rs b/translatable_proc/src/data/config.rs index 95515be..479d891 100644 --- a/translatable_proc/src/data/config.rs +++ b/translatable_proc/src/data/config.rs @@ -153,7 +153,8 @@ pub struct MacroConfig { impl MacroConfig { /// Get reference to the configured locales path. /// - /// Returns the path to the directory where translation files are expected + /// **Returns** + /// The path to the directory where translation files are expected /// to be located. pub fn path(&self) -> &str { &self.path @@ -161,7 +162,8 @@ impl MacroConfig { /// Get the current seek mode strategy. /// - /// Returns the configured strategy used to determine the order in which + /// **Returns** + /// The configured strategy used to determine the order in which /// translation files are processed. pub fn seek_mode(&self) -> SeekMode { self.seek_mode @@ -169,7 +171,8 @@ impl MacroConfig { /// Get the current overlap resolution strategy. /// - /// Returns the configured strategy for resolving translation conflicts + /// **Returns** + /// The configured strategy for resolving translation conflicts /// when multiple files define the same key. pub fn overlap(&self) -> TranslationOverlap { self.overlap diff --git a/translatable_shared/src/misc/templating.rs b/translatable_shared/src/misc/templating.rs index efe01c2..eb381f4 100644 --- a/translatable_shared/src/misc/templating.rs +++ b/translatable_shared/src/misc/templating.rs @@ -14,8 +14,6 @@ use quote::{ToTokens, TokenStreamExt, quote}; use syn::{Ident, parse_str}; use thiserror::Error; -use crate::macros::collections::map_transform_to_tokens; - /// Template parsing errors. /// /// This error is used within [`FormatString`] @@ -62,19 +60,50 @@ pub struct FormatString { /// Template spans. /// - /// This hashmap contains the ranges - /// of the templates found in the - /// original string, for the sake - /// of replacing them in a copy - /// of the original string. + /// 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 it's fields + /// private an inmutable. + /// + /// 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 @@ -100,6 +129,13 @@ impl FormatString { } } +/// 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; @@ -156,6 +192,12 @@ impl FromStr for FormatString { } } +/// 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; From a76577f480b1633dbe2a439a5e3e5348531ae81a Mon Sep 17 00:00:00 2001 From: stifskere Date: Sat, 19 Apr 2025 00:25:50 +0200 Subject: [PATCH 115/228] docs: translatable_shared/src/translations/collection.rs --- .../src/translations/collection.rs | 54 +++++++++++++++---- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/translatable_shared/src/translations/collection.rs b/translatable_shared/src/translations/collection.rs index ea372e8..b6e7549 100644 --- a/translatable_shared/src/translations/collection.rs +++ b/translatable_shared/src/translations/collection.rs @@ -1,3 +1,9 @@ +//! 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; @@ -6,19 +12,37 @@ use quote::{ToTokens, TokenStreamExt, quote}; use super::node::{TranslationNode, TranslationObject}; use crate::macros::collections::map_transform_to_tokens; -/// Wraps a collection of translation nodes, these translation nodes -/// are usually directly loaded files, and the keys to access them -/// independently are the complete system path. The collection -/// permits searching for translations by iterating all the files -/// in the specified configuration order, so most likely you don't -/// need to seek for a translation independently. +/// 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. @@ -35,6 +59,8 @@ impl TranslationNodeCollection { .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 @@ -54,17 +80,27 @@ impl TranslationNodeCollection { } } -/// Abstraction to easily collect a `HashMap` and wrap it -/// in a `TranslationNodeCollection`. +/// 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() + 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 = From ced1d3af818cc54b25f6efde7c4a1c0ff20c4d1b Mon Sep 17 00:00:00 2001 From: stifskere Date: Sat, 19 Apr 2025 00:28:31 +0200 Subject: [PATCH 116/228] docs: translatable_shared/src/translations/mod.rs --- translatable_shared/src/translations/mod.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/translatable_shared/src/translations/mod.rs b/translatable_shared/src/translations/mod.rs index 38ce8ff..a7e81a7 100644 --- a/translatable_shared/src/translations/mod.rs +++ b/translatable_shared/src/translations/mod.rs @@ -1,2 +1,10 @@ +//! 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; From 2b8d0b0c46e3d33c1570f83778e4fbfc05a59f67 Mon Sep 17 00:00:00 2001 From: stifskere Date: Sat, 19 Apr 2025 01:06:14 +0200 Subject: [PATCH 117/228] docs: translatable_shared/src/translations/node.rs --- translatable_shared/src/translations/node.rs | 102 ++++++++++++++----- 1 file changed, 78 insertions(+), 24 deletions(-) diff --git a/translatable_shared/src/translations/node.rs b/translatable_shared/src/translations/node.rs index 7d01d06..38ad988 100644 --- a/translatable_shared/src/translations/node.rs +++ b/translatable_shared/src/translations/node.rs @@ -1,3 +1,10 @@ +//! 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; @@ -10,46 +17,90 @@ use crate::macros::collections::{map_to_tokens, map_transform_to_tokens}; use crate::misc::language::Language; use crate::misc::templating::{FormatString, TemplateError}; -/// Errors occurring during TOML-to-translation structure transformation +/// [`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 { - /// Mixed content found in nesting node (strings and objects cannot coexist) + // We need to possibly solve the ambiguity between + // InvalidNesting and InvalidValue. + + /// Invalid object type error. + /// + /// This error signals that the nesting rules were + /// broken, thus the parsing cannot continue. #[error("A nesting can contain either strings or other nestings, but not both.")] InvalidNesting, - /// Template syntax error with unbalanced braces - #[error("Tempalte validation failed: {0:#}")] - UnclosedTemplate(#[from] TemplateError), + /// 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 type encountered in translation structure + /// Invalid value found inside a nesting. + /// + /// This error signals that an invalid value was found + /// inside a nesting. #[error("Only strings and objects are allowed for nested objects.")] InvalidValue, - /// Failed to parse language code from translation key + /// 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), } +/// 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; -/// Represents nested translation structure, -/// as it is on the translation files. +/// 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 { - /// Nested namespace containing other translation objects + /// Branch containing a [`TranslationNesting`]. + /// + /// Read the [`TranslationNesting`] documentation for + /// more information. Nesting(TranslationNesting), - /// Leaf node containing actual translations per language + + /// Branch containing a [`TranslationObject`]. + /// + /// Read the [`TranslationObject`] documentation for + /// more information. Translation(TranslationObject), } impl TranslationNode { - /// Resolves a translation path through the nesting hierarchy + /// Resolves a translation path through the nesting hierarchy. /// - /// # Arguments - /// * `path` - Slice of path segments to resolve + /// **Arguments** + /// * `path` - Slice of path segments to resolve. /// - /// # Returns - /// Reference to translations if path exists and points to leaf node + /// **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() @@ -70,11 +121,13 @@ impl TranslationNode { } } -/// This implementation converts the tagged union -/// to an equivalent call from the runtime context. +/// Compile-time to runtime conversion implementation. /// -/// This is exclusively meant to be used from the -/// macro generation context. +/// 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 { @@ -104,19 +157,20 @@ impl ToTokens for TranslationNode { } } -/// This implementation converts a `toml::Table` into a manageable -/// NestingType. +/// TOML table parsing. +/// +/// This implementation parses a TOML table object +/// into a [`TranslationNode`] for validation and +/// seeking the translations acording to the rules. impl TryFrom
for TranslationNode { type Error = TranslationNodeError; - /// Converts TOML table to validated translation structure fn try_from(value: Table) -> Result { let mut result = None; for (key, value) in value { match value { Value::String(translation_value) => { - // Initialize result if first entry let result = result.get_or_insert_with(|| Self::Translation(HashMap::new())); match result { From 0a719c9744acd688006c0fe415f8144177eaa56d Mon Sep 17 00:00:00 2001 From: stifskere Date: Sat, 19 Apr 2025 01:08:03 +0200 Subject: [PATCH 118/228] docs: translatable_shared/src/lib.rs --- translatable_shared/src/lib.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/translatable_shared/src/lib.rs b/translatable_shared/src/lib.rs index da1fd09..9e3a099 100644 --- a/translatable_shared/src/lib.rs +++ b/translatable_shared/src/lib.rs @@ -1,3 +1,13 @@ +//! Shared util declarations for `translatable` and `translatable_proc` +//! +//! 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. + pub mod macros; pub mod misc; pub mod translations; From a16122e6fc3f8c32b259f767e321cfabaaa32d65 Mon Sep 17 00:00:00 2001 From: Esteve Autet Alexe Date: Sat, 19 Apr 2025 02:43:19 +0200 Subject: [PATCH 119/228] docs: update readme to reflect changes Signed-off-by: Esteve Autet Alexe --- README.md | 58 +++++++++++++++++++++++++++---------------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 4a98821..fd5b9c0 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Translatable πŸŒπŸ—£οΈπŸ’¬πŸŒ +![translatable-readme](https://github.com/user-attachments/assets/4994514f-bbcc-48ea-a086-32e684adcd3a) [![Crates.io](https://img.shields.io/crates/v/translatable)](https://crates.io/crates/translatable) [![Docs.rs](https://docs.rs/translatable/badge.svg)](https://docs.rs/translatable) @@ -18,20 +18,19 @@ Our goal is not to be *blazingly fast* but to provide the most user-friendly exp ## Features πŸš€ -- **ISO 639-1 Standard**: Full support for 180+ language codes/names -- **Compile-Time Safety**: Macro-based translation validation -- **TOML Structure**: Hierarchical translation files with nesting -- **Smart Error Messages**: Context-aware suggestions -- **Template Validation**: Balanced bracket checking -- **Flexible Loading**: Configurable file processing order -- **Conflict Resolution**: Choose between first or last match priority +- **ISO 639-1 Standard**: Full support for 180+ language codes/names. +- **Adaptative optimizations**: Optimizations generated depending on call dynamism. +- **Translation templating**: Make replacements with templates on your translations out of the box. +- **Compile-Time validation**: Error reporting with *rust-analyzer* for static parameters. +- **Custom file structure**: Translatable uses a walkdir implementation. Configure your translations folder. +- **Conflict resolution**: Define translation processing rules with a `translatable.toml` file in the root directory. ## Installation πŸ“¦ -Run the following command in your project directory: +Add the following to your `Cargo.toml` under the `dependencies` section -```sh -cargo add translatable +```toml +translatable = "1.0.0" ``` ## Usage πŸ› οΈ @@ -58,9 +57,12 @@ all the files inside the path must be TOML files and sub folders, a `walk_dir` a to load all the translations inside that folder. The translation files have three rules -- Objects (including top level) can only contain objects and strings -- If an object contains another object, it can only contain other objects (known as nested object) -- If an object contains a string, it can only contain other strings (known as translation object) +- Objects can only contain objects and. Top level can only contain objects. +- If an object contains another object, it can only contain other objects (known as nested object). +- If an object contains a string, it can only contain other strings (known as translation object). + +Translation strings can contain templates, you may add sets of braces to the string with a key inside +and replace them while loading the translations with the macro. ### Loading translations @@ -71,11 +73,10 @@ To load translations you make use of the `translatable::translation` macro, that parameters to be passed. The first parameter consists of the language which can be passed dynamically as a variable or an expression -that resolves to an `impl Into`, or statically as a `&'static str` literal. Not mattering the way -it's passed, the translation must comply with the `ISO 639-1` standard. +that resolves to a `Translatable::Language`, or statically as a `&'static str` literal. For static values, the translation must comply with the `ISO 639-1` standard, as it is parsed to a `Translatable::Language` in compile time. The second parameter consists of the path, which can be passed dynamically as a variable or an expression -that resolves to an `impl Into` with the format `path.to.translation`, or statically with the following +that resolves to a `Vec` containing each path section, or statically with the following syntax `static path::to::translation`. The rest of parameters are `meta-variable patterns` also known as `key = value` parameters or key-value pairs, @@ -83,7 +84,7 @@ these are processed as replaces, *or format if the call is all-static*. When a t the name of a key inside it gets replaced for whatever is the `Display` implementation of the value. This meaning that the value must always implement `Display`. Otherwise, if you want to have a `{}` inside your translation, you can escape it the same way `format!` does, by using `{{}}`. Just like object construction works in rust, if -you have a parameter like `x = x`, you can shorten it to `x`. +you have a parameter like `x = x`, you can shorten it to `x`. The keys inside braces are XID validated. Depending on whether the parameters are static or dynamic the macro will act different, differing whether the checks are compile-time or run-time, the following table is a macro behavior matrix. @@ -95,12 +96,11 @@ the checks are compile-time or run-time, the following table is a macro behavior | `static language` + `dynamic path` | Language validity | `Result` (heap) | | `dynamic language` + `static path` (commonly used) | Path existence | `Result` (heap) | -- For the error handling, if you want to integrate this with `thiserror` you can use a `#[from] translatable::TranslationError`, -as a nested error, all the errors implement display, for optimization purposes there are not the same amount of errors with -dynamic parameters than there are with static parameters. +- For the error handling, if you want to integrate this with `thiserror` you can use a `#[from] translatable::Error`, +as a nested error, all the errors implement display. -- The runtime errors implement a `cause()` method that returns a heap allocated `String` with the error reason, essentially -the error display. +- The runtime errors implement a `cause()` method that returns a heap allocated `String` with the error reason, essentially the error display. That method is marked with `#[cold]`, use it in paths that don't evaluate all the time, +prefer using `or_else` than `or` which are lazy loaded methods. ## Example implementation πŸ“‚ @@ -143,13 +143,13 @@ extern crate translatable; use translatable::translation; fn main() { - let dynamic_lang = "es"; - let dynamic_path = "common.greeting" + let dynamic_lang = header.parse::(); + let dynamic_path = vec!["common", "greeting"]; - assert!(translation!("es", static common::greeting) == "Β‘Hola john!", name = "john"); - assert!(translation!("es", dynamic_path).unwrap() == "Β‘Hola john!".into(), name = "john"); - assert!(translation!(dynamic_lang, static common::greeting).unwrap() == "Β‘Hola john!".into(), name = "john"); - assert!(translation!(dynamic_lang, dynamic_path).unwrap() == "Β‘Hola john!".into(), name = "john"); + assert!(translation!("es", static common::greeting, name = "john") == "Β‘Hola john!"); + assert!(translation!("es", dynamic_path, name = "john").unwrap() == "Β‘Hola john!".into()); + assert!(translation!(dynamic_lang, static common::greeting, name = "john").unwrap() == "Β‘Hola john!".into()); + assert!(translation!(dynamic_lang, dynamic_path, name = "john").unwrap() == "Β‘Hola john!".into()); } ``` From 96e42ab4f02196ccb482a500980ebbf1dc941d57 Mon Sep 17 00:00:00 2001 From: Esteve Autet Alexe Date: Sat, 19 Apr 2025 02:46:08 +0200 Subject: [PATCH 120/228] chore: readme table of contents update Signed-off-by: Esteve Autet Alexe --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fd5b9c0..507220f 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Our goal is not to be *blazingly fast* but to provide the most user-friendly exp - [Features](#features-) - [Installation](#installation-) -- [Usage](#usage-) +- [Usage](#usage-%EF%B8%8F) - [Example implementation](#example-implementation-) - [Licensing](#license-) From dc0f0647df608f1b8e8719badd605f6f5fe7273c Mon Sep 17 00:00:00 2001 From: Esteve Autet Alexe Date: Sat, 19 Apr 2025 02:48:04 +0200 Subject: [PATCH 121/228] fix: readme test Signed-off-by: Esteve Autet Alexe --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 507220f..d0ebf8b 100644 --- a/README.md +++ b/README.md @@ -139,8 +139,7 @@ Notice how there is a template, this template is being replaced by the `name = "john"` key value pair passed as third parameter. ```rust -extern crate translatable; -use translatable::translation; +use translatable::{translation, Language}; fn main() { let dynamic_lang = header.parse::(); From 700930efcfaec05f52ed6f55dffa053a28710849 Mon Sep 17 00:00:00 2001 From: stifskere Date: Sat, 19 Apr 2025 03:00:36 +0200 Subject: [PATCH 122/228] chore: rewrite readme --- CODE_OF_CONDUCT.md | 11 +++++------ CONTRIBUTING.md | 27 +++++++++++++++++++-------- GOVERNANCE.md | 8 +++++--- README-MACROS.md | 11 +++++++++-- README-SHARED.md | 14 ++++++++++++++ 5 files changed, 52 insertions(+), 19 deletions(-) create mode 100644 README-SHARED.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 673d613..b371ad6 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -55,8 +55,8 @@ further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at `esteve@memw.es` or `chiko@envs.net`. All -complaints will be reviewed and investigated and will result in a response that +reported by contacting the project team at [`moderation@flaky.es`](mailto:moderation@flaky.es). +All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. @@ -67,9 +67,8 @@ members of the project's leadership. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 1.4, available at [contributor covenant]. [homepage]: https://www.contributor-covenant.org - - +[contributor covenant]: https://www.contributor-covenant.org/version/1/4/code-of-conduct.html diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d1b2dbd..af6d91a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,30 +1,41 @@ # Contributing to Translatable -In Translatable, we welcome contributions from everyone, including bug reports, pull requests, and feedback. This document serves as guidance if you are considering submitting any of the above. +In Translatable, we welcome contributions from everyone, including bug reports, +pull requests, and feedback. This document serves as guidance if you are +considering submitting any of the above. ## Submitting Bug Reports and Feature Requests -To submit a bug report or feature request, you can open an issue in this repository: [`FlakySL/translatable.rs`](https://github.com/FlakySL/translatable.rs). +To submit a bug report or feature request, you can open an issue in this +repository: [`FlakySL/translatable.rs`](https://github.com/FlakySL/translatable.rs). -When reporting a bug or requesting help, please include sufficient details to allow others to reproduce the behavior you're encountering. For guidance on how to approach this, read about [How to Create a Minimal, Reproducible Example](https://stackoverflow.com/help/minimal-reproducible-example). +When reporting a bug or requesting help, please include sufficient details +to allow others to reproduce the behavior you're encountering. For guidance on +how to approach this, read about [How to Create a Minimal, Reproducible Example](https://stackoverflow.com/help/minimal-reproducible-example). When making a feature request, please clearly explain: + 1. The problem you want to solve 2. How Translatable could help address this problem 3. Any potential alternatives 4. Possible disadvantages of your proposal -Before submitting, please verify that no existing issue addresses your specific problem/request. If you want to elaborate on a problem or discuss it further, you can use our [Discord channel](https://discord.gg/AJWFyps23a) at Flaky. +Before submitting, please verify that no existing issue addresses your specific +problem/request. If you want to elaborate on a problem or discuss it further, +you can use our [Discord channel](https://discord.gg/AJWFyps23a) at Flaky. We recommend using the issue templates provided in this repository. ## Running Tests and Compiling the Project -This project uses [`make`](https://www.gnu.org/software/make/) for all common tasks: +This project uses [cargo](https://github.com/rust-lang/cargo) the rust package manager. -- Run `make test` to execute both integration and unit tests -- Use `make compile` to compile the project in each target directory +- Run tests using `cargo test`. +- Compile the project using `cargo build`. ## Code of Conduct -The Translatable community follows the [Rust Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct). For moderation issues or escalation, please contact Esteve at [esteve@memw.es](mailto:esteve@memw.es) rather than the Rust moderation team. +The Translatable community follows the[Rust Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct). +For moderation issues or escalation, please contact Esteve or Luis at +[moderation@flaky.es](mailto:moderation@flaky.es) rather than the Rust +moderation team. diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 33cc929..b1387fe 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -1,9 +1,11 @@ # Governance and Moderation -This project is mainly maintained by the authors listed in both `translatable/Cargo.toml` and `translatable_proc/Cargo.toml`. +This project is mainly maintained by the authors listed in +`translatable/Cargo.toml`, `translatable_proc/Cargo.toml` and +`translatable_shared/Cargo.toml`. -- Esteve Autet `esteve@memw.es` -- Chiko `chiko@envs.net` +- Esteve Autet `esteve.autet@flaky.es` +- Chiko `luis.degnan@flaky.es` There is no hierarchy established (yet) but this might be subject to change soon. For any inquiries you can contact any of the emails listed above. diff --git a/README-MACROS.md b/README-MACROS.md index dd9aa82..8981d82 100644 --- a/README-MACROS.md +++ b/README-MACROS.md @@ -1,7 +1,14 @@ # Translatable Macros -This crate exists solely to provide macros for the [Translatable](https://crates.io/crates/translatable) crate. Using this crate without the main Translatable crate is **not supported**, and any support requests or bug reports regarding standalone usage will be redirected to the [Translatable](https://crates.io/crates/translatable) crate. +This crate exists solely to provide macros for the [Translatable] +crate. Using this crate without the main Translatable crate is +**not supported**, and any support requests or bug reports regarding +standalone usage will be redirected to the +[Translatable] crate. ## Licensing -This crate shares the same licensing terms as [Translatable](https://crates.io/crates/translatable), as these crates are essentially part of the same ecosystem. +This crate shares the same licensing terms as [Translatable], +as these crates are essentially part of the same ecosystem. + +[translatable]: https://crates.io/crates/translatable diff --git a/README-SHARED.md b/README-SHARED.md new file mode 100644 index 0000000..334354a --- /dev/null +++ b/README-SHARED.md @@ -0,0 +1,14 @@ +# Translatable Shared + +This crate exists solely to provide utilities for the [Translatable] +crate. Using this crate without the main Translatable crate is +**not supported**, and any support requests or bug reports regarding +standalone usage will be redirected to the +[Translatable] crate. + +## Licensing + +This crate shares the same licensing terms as [Translatable], +as these crates are essentially part of the same ecosystem. + +[translatable]: https://crates.io/crates/translatable From 7792dde15b7f0f045941acf7b0a64423a43249b1 Mon Sep 17 00:00:00 2001 From: stifskere Date: Sat, 19 Apr 2025 03:03:21 +0200 Subject: [PATCH 123/228] chore: change repo metadata --- Cargo.lock | 6 +++--- translatable/Cargo.toml | 6 +++--- translatable_proc/Cargo.toml | 4 ++-- translatable_shared/Cargo.toml | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 776f488..4fc13ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -223,7 +223,7 @@ dependencies = [ [[package]] name = "translatable" -version = "0.1.0" +version = "1.0.0" dependencies = [ "strum", "thiserror", @@ -234,7 +234,7 @@ dependencies = [ [[package]] name = "translatable_proc" -version = "0.1.0" +version = "1.0.0" dependencies = [ "proc-macro2", "quote", @@ -247,7 +247,7 @@ dependencies = [ [[package]] name = "translatable_shared" -version = "0.1.0" +version = "1.0.0" dependencies = [ "proc-macro2", "quote", diff --git a/translatable/Cargo.toml b/translatable/Cargo.toml index f6614c1..4749433 100644 --- a/translatable/Cargo.toml +++ b/translatable/Cargo.toml @@ -4,7 +4,7 @@ description = "A robust internationalization solution for Rust featuring compile repository = "https://github.com/FlakySL/translatable.rs" license = "MIT OR Apache-2.0" readme = "../README.md" -version = "0.1.0" +version = "1.0.0" edition = "2024" authors = ["Esteve Autet ", "Chiko "] keywords = [ @@ -18,8 +18,8 @@ keywords = [ [dependencies] strum = { version = "0.27.1", features = ["derive"] } thiserror = "2.0.12" -translatable_proc = { path = "../translatable_proc" } -translatable_shared = { path = "../translatable_shared/" } +translatable_proc = { version = "1", path = "../translatable_proc" } +translatable_shared = { version = "1", path = "../translatable_shared/" } [dev-dependencies] trybuild = "1.0.104" diff --git a/translatable_proc/Cargo.toml b/translatable_proc/Cargo.toml index c5a4129..18e2347 100644 --- a/translatable_proc/Cargo.toml +++ b/translatable_proc/Cargo.toml @@ -4,7 +4,7 @@ description = "Proc macro crate for the translatable library." repository = "https://github.com/FlakySL/translatable.rs" license = "MIT OR Apache-2.0" readme = "../README-MACROS.md" -version = "0.1.0" +version = "1.0.0" edition = "2024" authors = ["Esteve Autet ", "Chiko "] @@ -18,4 +18,4 @@ strum = { version = "0.27.1", features = ["derive"] } syn = { version = "2.0.98", features = ["full"] } thiserror = "2.0.11" toml = "0.8.20" -translatable_shared = { path = "../translatable_shared/" } +translatable_shared = { version = "1", path = "../translatable_shared/" } diff --git a/translatable_shared/Cargo.toml b/translatable_shared/Cargo.toml index c8133fb..6b03ccc 100644 --- a/translatable_shared/Cargo.toml +++ b/translatable_shared/Cargo.toml @@ -3,8 +3,8 @@ name = "translatable_shared" description = "Shared dependencies crate for translatable." repository = "https://github.com/FlakySL/translatable.rs" license = "MIT OR Apache-2.0" -readme = "../README-MACROS.md" -version = "0.1.0" +readme = "../README-SHARED.md" +version = "1.0.0" edition = "2024" authors = ["Esteve Autet ", "Chiko "] From 62a93a585895abde285941a0b2317cd87349924d Mon Sep 17 00:00:00 2001 From: stifskere Date: Sat, 19 Apr 2025 03:37:58 +0200 Subject: [PATCH 124/228] chore: resolve ambiguity between error types --- translatable_shared/src/misc/templating.rs | 4 +- .../src/translations/collection.rs | 9 ++-- translatable_shared/src/translations/node.rs | 41 +++++++++++-------- 3 files changed, 30 insertions(+), 24 deletions(-) diff --git a/translatable_shared/src/misc/templating.rs b/translatable_shared/src/misc/templating.rs index eb381f4..3e863d9 100644 --- a/translatable_shared/src/misc/templating.rs +++ b/translatable_shared/src/misc/templating.rs @@ -109,7 +109,9 @@ impl FormatString { .original .clone(); - let mut spans = self.spans.clone(); + let mut spans = self + .spans + .clone(); spans.sort_by_key(|(_key, range)| range.start); let mut offset = 0isize; diff --git a/translatable_shared/src/translations/collection.rs b/translatable_shared/src/translations/collection.rs index b6e7549..f05b665 100644 --- a/translatable_shared/src/translations/collection.rs +++ b/translatable_shared/src/translations/collection.rs @@ -30,7 +30,7 @@ impl TranslationNodeCollection { /// /// 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 + /// you want to obtain all the translation files use /// /// **Arguments** /// * `collection` - An already populated collection for lookup. @@ -82,13 +82,12 @@ impl TranslationNodeCollection { /// Hashmap wrapper implementation. /// -/// Abstraction to easily collect a [`HashMap`] and wrap it -/// in a [`TranslationNodeCollection`]. +/// 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() + iter.into_iter() .collect(), ) } diff --git a/translatable_shared/src/translations/node.rs b/translatable_shared/src/translations/node.rs index 38ad988..7d206c2 100644 --- a/translatable_shared/src/translations/node.rs +++ b/translatable_shared/src/translations/node.rs @@ -24,14 +24,11 @@ use crate::misc::templating::{FormatString, TemplateError}; /// while trying seeking for it's content. #[derive(Error, Debug)] pub enum TranslationNodeError { - // We need to possibly solve the ambiguity between - // InvalidNesting and InvalidValue. - /// Invalid object type error. /// /// This error signals that the nesting rules were /// broken, thus the parsing cannot continue. - #[error("A nesting can contain either strings or other nestings, but not both.")] + #[error("A nesting can only contain translation objects or other nestings")] InvalidNesting, /// Template validation error. @@ -45,9 +42,12 @@ pub enum TranslationNodeError { /// Invalid value found inside a nesting. /// /// This error signals that an invalid value was found - /// inside a nesting. - #[error("Only strings and objects are allowed for nested objects.")] - InvalidValue, + /// 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. /// @@ -57,6 +57,13 @@ pub enum TranslationNodeError { /// 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. @@ -161,42 +168,40 @@ impl ToTokens for TranslationNode { /// /// This implementation parses a TOML table object /// into a [`TranslationNode`] for validation and -/// seeking the translations acording to the rules. +/// seeking the translations according to the rules. impl TryFrom
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 { Value::String(translation_value) => { - let result = result.get_or_insert_with(|| Self::Translation(HashMap::new())); - - match result { + match result.get_or_insert_with(|| Self::Translation(HashMap::new())) { Self::Translation(translation) => { translation.insert(key.parse()?, translation_value.parse()?); }, - Self::Nesting(_) => return Err(TranslationNodeError::InvalidNesting), + Self::Nesting(_) => return Err(TranslationNodeError::MixedValues), } }, Value::Table(nesting_value) => { - let result = result.get_or_insert_with(|| Self::Nesting(HashMap::new())); - - match result { + match result.get_or_insert_with(|| Self::Nesting(HashMap::new())) { Self::Nesting(nesting) => { nesting.insert(key, Self::try_from(nesting_value)?); }, - Self::Translation(_) => return Err(TranslationNodeError::InvalidNesting), + + Self::Translation(_) => return Err(TranslationNodeError::MixedValues), } }, - _ => return Err(TranslationNodeError::InvalidValue), + _ => return Err(TranslationNodeError::InvalidNesting), } } - result.ok_or(TranslationNodeError::InvalidValue) + result.ok_or(TranslationNodeError::EmptyTable) } } From cffcabd6577d3bc21ad47707ae5f97238fce64ae Mon Sep 17 00:00:00 2001 From: chikof Date: Sat, 19 Apr 2025 17:11:25 +0100 Subject: [PATCH 125/228] chore: add make pkg --- .envrc | 1 + .gitignore | 1 + flake.nix | 42 +++++++++++++----------------------------- 3 files changed, 15 insertions(+), 29 deletions(-) create mode 100644 .envrc diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore index e17ef0a..f63d5dd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target .bacon-locations +.direnv diff --git a/flake.nix b/flake.nix index 48b81cb..ef0eefb 100644 --- a/flake.nix +++ b/flake.nix @@ -1,5 +1,5 @@ { - description = "Flake configuration file for translatable.rs development."; + description = "Flake configuration file for translatable development."; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; crane.url = "github:ipetkov/crane"; @@ -7,44 +7,28 @@ flake-utils.url = "github:numtide/flake-utils"; }; - outputs = - { - nixpkgs, - flake-utils, - fenix, - ... - }@inputs: - flake-utils.lib.eachDefaultSystem ( - system: + outputs = { nixpkgs, flake-utils, fenix, ... }@inputs: + flake-utils.lib.eachDefaultSystem (system: let pkgs = nixpkgs.legacyPackages.${system}; crane = inputs.crane.mkLib pkgs; - toolchain = - with fenix.packages.${system}; + toolchain = with fenix.packages.${system}; combine [ - minimal.rustc - minimal.cargo - complete.rust-src + stable.rustc + stable.cargo + stable.rust-src complete.rustfmt - complete.clippy + stable.clippy + stable.rust-analyzer ]; craneLib = crane.overrideToolchain toolchain; - in - { + in { devShells.default = craneLib.devShell { - packages = with pkgs; [ - toolchain - rustfmt - clippy - qemu-user - ]; + packages = with pkgs; [ toolchain gnumake ]; - env = { - LAZYVIM_RUST_DIAGNOSTICS = "bacon-ls"; - }; + env = { LAZYVIM_RUST_DIAGNOSTICS = "bacon-ls"; }; }; - } - ); + }); } From ea1187fc5742655e964ec832a60ad4bf584b2658 Mon Sep 17 00:00:00 2001 From: stifskere Date: Sat, 19 Apr 2025 18:35:40 +0200 Subject: [PATCH 126/228] fix: typo fixes --- CONTRIBUTING.md | 10 +++++----- README.md | 2 +- translatable/Cargo.toml | 2 +- translatable_proc/Cargo.toml | 2 +- translatable_shared/Cargo.toml | 2 +- translatable_shared/src/misc/templating.rs | 6 +++--- translatable_shared/src/translations/node.rs | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index af6d91a..e8d1026 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,7 +7,7 @@ considering submitting any of the above. ## Submitting Bug Reports and Feature Requests To submit a bug report or feature request, you can open an issue in this -repository: [`FlakySL/translatable.rs`](https://github.com/FlakySL/translatable.rs). +repository: [`FlakySL/translatable`](https://github.com/FlakySL/translatable). When reporting a bug or requesting help, please include sufficient details to allow others to reproduce the behavior you're encountering. For guidance on @@ -28,14 +28,14 @@ We recommend using the issue templates provided in this repository. ## Running Tests and Compiling the Project -This project uses [cargo](https://github.com/rust-lang/cargo) the rust package manager. +This project uses GNU [make](https://www.gnu.org/software/make/). -- Run tests using `cargo test`. -- Compile the project using `cargo build`. +- Run tests using `make test`. +- Compile the project using `make build`. ## Code of Conduct -The Translatable community follows the[Rust Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct). +The Translatable community follows the [Rust Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct). For moderation issues or escalation, please contact Esteve or Luis at [moderation@flaky.es](mailto:moderation@flaky.es) rather than the Rust moderation team. diff --git a/README.md b/README.md index d0ebf8b..c92787f 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ all the files inside the path must be TOML files and sub folders, a `walk_dir` a to load all the translations inside that folder. The translation files have three rules -- Objects can only contain objects and. Top level can only contain objects. +- Objects can only contain objects and translations. Top level can only contain objects. - If an object contains another object, it can only contain other objects (known as nested object). - If an object contains a string, it can only contain other strings (known as translation object). diff --git a/translatable/Cargo.toml b/translatable/Cargo.toml index 4749433..a781cdf 100644 --- a/translatable/Cargo.toml +++ b/translatable/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "translatable" description = "A robust internationalization solution for Rust featuring compile-time validation, ISO 639-1 compliance, and TOML-based translation management. " -repository = "https://github.com/FlakySL/translatable.rs" +repository = "https://github.com/FlakySL/translatable" license = "MIT OR Apache-2.0" readme = "../README.md" version = "1.0.0" diff --git a/translatable_proc/Cargo.toml b/translatable_proc/Cargo.toml index 18e2347..d5f9cc3 100644 --- a/translatable_proc/Cargo.toml +++ b/translatable_proc/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "translatable_proc" description = "Proc macro crate for the translatable library." -repository = "https://github.com/FlakySL/translatable.rs" +repository = "https://github.com/FlakySL/translatable" license = "MIT OR Apache-2.0" readme = "../README-MACROS.md" version = "1.0.0" diff --git a/translatable_shared/Cargo.toml b/translatable_shared/Cargo.toml index 6b03ccc..0c6f030 100644 --- a/translatable_shared/Cargo.toml +++ b/translatable_shared/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "translatable_shared" description = "Shared dependencies crate for translatable." -repository = "https://github.com/FlakySL/translatable.rs" +repository = "https://github.com/FlakySL/translatable" license = "MIT OR Apache-2.0" readme = "../README-SHARED.md" version = "1.0.0" diff --git a/translatable_shared/src/misc/templating.rs b/translatable_shared/src/misc/templating.rs index 3e863d9..525533d 100644 --- a/translatable_shared/src/misc/templating.rs +++ b/translatable_shared/src/misc/templating.rs @@ -55,7 +55,7 @@ pub struct FormatString { /// with `self.spans`. /// /// This should never be mutated for the sake - /// of keping the alignment with `self.spans`. + /// of keeping the alignment with `self.spans`. original: String, /// Template spans. @@ -75,8 +75,8 @@ impl FormatString { /// /// This function takes data that may be generated /// from a macro output and constructs an instance - /// of [`FormatString`] keeping it's fields - /// private an inmutable. + /// 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 diff --git a/translatable_shared/src/translations/node.rs b/translatable_shared/src/translations/node.rs index 7d206c2..8809b7f 100644 --- a/translatable_shared/src/translations/node.rs +++ b/translatable_shared/src/translations/node.rs @@ -63,7 +63,7 @@ pub enum TranslationNodeError { /// This error signals that a created translation file /// is empty and cannot be parsed. #[error("A translation file cannot be empty")] - EmptyTable + EmptyTable, } /// Nesting type alias. From 19081c6c3d610911705d8adcf7dfd2373012f4ca Mon Sep 17 00:00:00 2001 From: stifskere Date: Sat, 19 Apr 2025 18:47:10 +0200 Subject: [PATCH 127/228] fix: documentation issues --- translatable/src/lib.rs | 6 +++--- translatable_proc/src/macro_generation/translation.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/translatable/src/lib.rs b/translatable/src/lib.rs index 845b19a..fff945c 100644 --- a/translatable/src/lib.rs +++ b/translatable/src/lib.rs @@ -25,11 +25,11 @@ pub use error::RuntimeError as Error; #[rustfmt::skip] pub use translatable_proc::translation; -/// User-facing util re-exports. +/// Language enum re-export. /// /// This `use` statement re-exports -/// all the shared module items that -/// are useful for the end-user. +/// from the hidden shared re-export +/// for user convenience on parsing. #[rustfmt::skip] pub use shared::misc::language::Language; diff --git a/translatable_proc/src/macro_generation/translation.rs b/translatable_proc/src/macro_generation/translation.rs index 33931ce..215c61c 100644 --- a/translatable_proc/src/macro_generation/translation.rs +++ b/translatable_proc/src/macro_generation/translation.rs @@ -19,7 +19,7 @@ use crate::macro_input::translation::TranslationMacroArgs; /// Macro compile-time translation resolution error. /// -/// Represents errors that can occur while compiling the [`translation!()`]. +/// 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. /// From 7c1574374015a33487390df63527d1d568a27f3a Mon Sep 17 00:00:00 2001 From: stifskere Date: Sat, 19 Apr 2025 19:22:54 +0200 Subject: [PATCH 128/228] docs: readme add examples on top --- README.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/README.md b/README.md index c92787f..4e462e5 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Our goal is not to be *blazingly fast* but to provide the most user-friendly exp ## Table of Contents πŸ“– +- [Use Cases](#use-cases-) - [Features](#features-) - [Installation](#installation-) - [Usage](#usage-%EF%B8%8F) @@ -25,6 +26,56 @@ Our goal is not to be *blazingly fast* but to provide the most user-friendly exp - **Custom file structure**: Translatable uses a walkdir implementation. Configure your translations folder. - **Conflict resolution**: Define translation processing rules with a `translatable.toml` file in the root directory. +## Use Cases πŸ” + +You may use translatable to write responses in back-end applications. Here is +an example of how you can integrate this with [actix-web](https://actix.rs/). + +```rust +use actix_web::{HttpRequest, HttpResponse, Responder, get}; +use translatable::{translation, Language}; + +#[get("/echo")] +pub async fn get_echo(req: HttpRequest) -> impl Responder { + let language = req + .headers() + .get("Accept-Language") + .and_then(|v| v.as_str().ok()) + .and_then(|v| v.parse::().ok()) + .unwrap_or(Language::EN); + + HttpResponse::Ok() + .body( + match translation!(language, static routes::responses::get_echo) { + Ok(t) => t, + Err(err) => concat!("Translation error ", err.to_string()) + } + ) +} +``` + +Or use it for front-end with [Leptos](https://leptos.dev/). + +```rust +use leptos::prelude::*; +use translatable::{translation, Language}; + +#[component] +pub fn Greeting(language: Language) -> impl IntoView { + let message = match translation!(language, static pages::misc::greeting) { + Ok(t) => t, + Err(err) => { + log::error!("Translation error {err:#}"); + "Translation error.".into() + } + } + + view! { +

{ message }

+ } +} +``` + ## Installation πŸ“¦ Add the following to your `Cargo.toml` under the `dependencies` section From 68aa6f8e0956b7ddf3df5b9a26e6c440642178f9 Mon Sep 17 00:00:00 2001 From: chikof Date: Sun, 20 Apr 2025 06:32:45 +0100 Subject: [PATCH 129/228] chore: new file structure --- Makefile | 2 +- translatable/tests/dynamic/args/empty_args.rs | 9 ----- translatable/tests/dynamic/args/extra_args.rs | 9 ----- .../tests/dynamic/args/inherit_args.rs | 10 ------ .../tests/dynamic/args/inherit_extra_args.rs | 11 ------ .../tests/dynamic/args/invalid_identifier.rs | 9 ----- .../dynamic/args/invalid_identifier.stderr | 5 --- translatable/tests/dynamic/args/with_args.rs | 9 ----- translatable/tests/dynamic/invalid_syntax.rs | 18 ---------- .../tests/dynamic/invalid_syntax.stderr | 23 ------------ translatable/tests/fixtures/translatable.toml | 0 .../tests/fixtures/translations/common.toml | 3 ++ .../tests/fixtures/translations/test.toml | 7 ++++ translatable/tests/integration.rs | 36 +++++++++++++++++++ translatable/tests/static/args/empty_args.rs | 6 ---- translatable/tests/static/args/extra_args.rs | 6 ---- .../tests/static/args/inherit_args.rs | 7 ---- .../tests/static/args/inherit_extra_args.rs | 9 ----- .../tests/static/args/invalid_identifier.rs | 6 ---- .../static/args/invalid_identifier.stderr | 5 --- translatable/tests/static/args/with_args.rs | 6 ---- translatable/tests/static/invalid_syntax.rs | 12 ------- .../tests/static/invalid_syntax.stderr | 31 ---------------- translatable/tests/tests.rs | 33 ----------------- translatable/tests/translations/test.toml | 7 ---- translatable/tests/ui.rs | 8 +++++ translatable/tests/ui/fail/invalid_lang.rs | 6 ++++ .../tests/ui/fail/invalid_lang.stderr | 5 +++ translatable/tests/ui/fail/missing_path.rs | 6 ++++ .../tests/ui/fail/missing_path.stderr | 7 ++++ .../tests/ui/fail/path_with_generics.rs | 5 +++ .../tests/ui/fail/path_with_generics.stderr | 5 +++ translatable/tests/ui/pass/dynamic_ok.rs | 10 ++++++ translatable/tests/ui/pass/empty_args.rs | 9 +++++ translatable/tests/ui/pass/extra_args.rs | 9 +++++ translatable/tests/ui/pass/static_ok.rs | 5 +++ 36 files changed, 122 insertions(+), 232 deletions(-) delete mode 100644 translatable/tests/dynamic/args/empty_args.rs delete mode 100644 translatable/tests/dynamic/args/extra_args.rs delete mode 100644 translatable/tests/dynamic/args/inherit_args.rs delete mode 100644 translatable/tests/dynamic/args/inherit_extra_args.rs delete mode 100644 translatable/tests/dynamic/args/invalid_identifier.rs delete mode 100644 translatable/tests/dynamic/args/invalid_identifier.stderr delete mode 100644 translatable/tests/dynamic/args/with_args.rs delete mode 100644 translatable/tests/dynamic/invalid_syntax.rs delete mode 100644 translatable/tests/dynamic/invalid_syntax.stderr create mode 100644 translatable/tests/fixtures/translatable.toml create mode 100644 translatable/tests/fixtures/translations/common.toml create mode 100644 translatable/tests/fixtures/translations/test.toml create mode 100644 translatable/tests/integration.rs delete mode 100644 translatable/tests/static/args/empty_args.rs delete mode 100644 translatable/tests/static/args/extra_args.rs delete mode 100644 translatable/tests/static/args/inherit_args.rs delete mode 100644 translatable/tests/static/args/inherit_extra_args.rs delete mode 100644 translatable/tests/static/args/invalid_identifier.rs delete mode 100644 translatable/tests/static/args/invalid_identifier.stderr delete mode 100644 translatable/tests/static/args/with_args.rs delete mode 100644 translatable/tests/static/invalid_syntax.rs delete mode 100644 translatable/tests/static/invalid_syntax.stderr delete mode 100644 translatable/tests/tests.rs delete mode 100644 translatable/tests/translations/test.toml create mode 100644 translatable/tests/ui.rs create mode 100644 translatable/tests/ui/fail/invalid_lang.rs create mode 100644 translatable/tests/ui/fail/invalid_lang.stderr create mode 100644 translatable/tests/ui/fail/missing_path.rs create mode 100644 translatable/tests/ui/fail/missing_path.stderr create mode 100644 translatable/tests/ui/fail/path_with_generics.rs create mode 100644 translatable/tests/ui/fail/path_with_generics.stderr create mode 100644 translatable/tests/ui/pass/dynamic_ok.rs create mode 100644 translatable/tests/ui/pass/empty_args.rs create mode 100644 translatable/tests/ui/pass/extra_args.rs create mode 100644 translatable/tests/ui/pass/static_ok.rs diff --git a/Makefile b/Makefile index cd7494b..ab2fe9a 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -export TRANSLATABLE_LOCALES_PATH=${PWD}/translatable/tests/translations +export TRANSLATABLE_LOCALES_PATH=${PWD}/translatable/tests/fixtures/translations test: cargo test -p translatable -- --nocapture --color=always diff --git a/translatable/tests/dynamic/args/empty_args.rs b/translatable/tests/dynamic/args/empty_args.rs deleted file mode 100644 index 407588e..0000000 --- a/translatable/tests/dynamic/args/empty_args.rs +++ /dev/null @@ -1,9 +0,0 @@ -use translatable::translation; - -fn main() { - let lang = "en"; - let path = "common.greeting"; - - let res = translation!(lang, path).unwrap(); - assert_eq!(res, "Hello {name}!"); -} diff --git a/translatable/tests/dynamic/args/extra_args.rs b/translatable/tests/dynamic/args/extra_args.rs deleted file mode 100644 index 6c4456b..0000000 --- a/translatable/tests/dynamic/args/extra_args.rs +++ /dev/null @@ -1,9 +0,0 @@ -use translatable::translation; - -fn main() { - let lang = "en"; - let path = "common.greeting"; - - let res = translation!(lang, path, name = "Juan", surname = "Doe").unwrap(); - assert_eq!(res, "Hello Juan!"); -} diff --git a/translatable/tests/dynamic/args/inherit_args.rs b/translatable/tests/dynamic/args/inherit_args.rs deleted file mode 100644 index 33654be..0000000 --- a/translatable/tests/dynamic/args/inherit_args.rs +++ /dev/null @@ -1,10 +0,0 @@ -use translatable::translation; - -fn main() { - let lang = "en"; - let path = "common.greeting"; - let name = "Juan"; - - let res = translation!(lang, path, name).unwrap(); - assert_eq!(res, "Hello Juan!"); -} diff --git a/translatable/tests/dynamic/args/inherit_extra_args.rs b/translatable/tests/dynamic/args/inherit_extra_args.rs deleted file mode 100644 index 7177f19..0000000 --- a/translatable/tests/dynamic/args/inherit_extra_args.rs +++ /dev/null @@ -1,11 +0,0 @@ -use translatable::translation; - -fn main() { - let lang = "en"; - let path = "common.greeting"; - let name = "Juan"; - let surname = "Doe"; - - let res = translation!(lang, path, name, surname).unwrap(); - assert_eq!(res, "Hello Juan!"); -} diff --git a/translatable/tests/dynamic/args/invalid_identifier.rs b/translatable/tests/dynamic/args/invalid_identifier.rs deleted file mode 100644 index 5462471..0000000 --- a/translatable/tests/dynamic/args/invalid_identifier.rs +++ /dev/null @@ -1,9 +0,0 @@ -use translatable::translation; - -fn main() { - let lang = "en"; - let path = "common.greeting"; - - // Invalid argument syntax - translation!(lang, path, 42 = "value"); -} diff --git a/translatable/tests/dynamic/args/invalid_identifier.stderr b/translatable/tests/dynamic/args/invalid_identifier.stderr deleted file mode 100644 index 9d6f252..0000000 --- a/translatable/tests/dynamic/args/invalid_identifier.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: expected identifier - --> tests/dynamic/args/invalid_identifier.rs:8:30 - | -8 | translation!(lang, path, 42 = "value"); - | ^^ diff --git a/translatable/tests/dynamic/args/with_args.rs b/translatable/tests/dynamic/args/with_args.rs deleted file mode 100644 index 3b1c745..0000000 --- a/translatable/tests/dynamic/args/with_args.rs +++ /dev/null @@ -1,9 +0,0 @@ -use translatable::translation; - -fn main() { - let lang = "en"; - let path = "common.greeting"; - - let res = translation!(lang, path, name = "Juan").unwrap(); - assert_eq!(res, "Hello Juan!"); -} diff --git a/translatable/tests/dynamic/invalid_syntax.rs b/translatable/tests/dynamic/invalid_syntax.rs deleted file mode 100644 index e877bd7..0000000 --- a/translatable/tests/dynamic/invalid_syntax.rs +++ /dev/null @@ -1,18 +0,0 @@ -use translatable::translation; - -fn main() { - // Missing arguments - let lang = "es"; - let _ = translation!(lang); - - // Invalid literal type - let lang = 42; - let path = "common.greeting"; - let _ = translation!(lang, path); - - // Malformed dynamic path - // TODO: I'm 100% sure that this is a bug. - let lang = "es"; - let path = "invalid.path"; - assert!(translation!(lang, path).is_err()); -} diff --git a/translatable/tests/dynamic/invalid_syntax.stderr b/translatable/tests/dynamic/invalid_syntax.stderr deleted file mode 100644 index 65e3f05..0000000 --- a/translatable/tests/dynamic/invalid_syntax.stderr +++ /dev/null @@ -1,23 +0,0 @@ -error: expected `,` - --> tests/dynamic/invalid_syntax.rs:6:13 - | -6 | let _ = translation!(lang); - | ^^^^^^^^^^^^^^^^^^ - | - = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) - -error[E0277]: the trait bound `String: From<{integer}>` is not satisfied - --> tests/dynamic/invalid_syntax.rs:11:13 - | -11 | let _ = translation!(lang, path); - | ^^^^^^^^^^^^^^^^^^^^^^^^ the trait `From<{integer}>` is not implemented for `String` - | - = help: the following other types implement trait `From`: - `String` implements `From<&String>` - `String` implements `From<&mut str>` - `String` implements `From<&str>` - `String` implements `From>` - `String` implements `From>` - `String` implements `From` - = note: required for `{integer}` to implement `Into` - = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/translatable/tests/fixtures/translatable.toml b/translatable/tests/fixtures/translatable.toml new file mode 100644 index 0000000..e69de29 diff --git a/translatable/tests/fixtures/translations/common.toml b/translatable/tests/fixtures/translations/common.toml new file mode 100644 index 0000000..869f64f --- /dev/null +++ b/translatable/tests/fixtures/translations/common.toml @@ -0,0 +1,3 @@ +[common.greeting] +en = "Hi {name}!" +es = "Β‘Hola {name}!" diff --git a/translatable/tests/fixtures/translations/test.toml b/translatable/tests/fixtures/translations/test.toml new file mode 100644 index 0000000..13ee676 --- /dev/null +++ b/translatable/tests/fixtures/translations/test.toml @@ -0,0 +1,7 @@ +[welcome_message] +en = "Welcome to our app!" +es = "Β‘Bienvenido a nuestra aplicaciΓ³n!" + +# [common.greeting] +# en = "Hello {name} {surname}! My name is {name} {surname} {{hello}}" +# es = "Β‘Hola {name} {surname}! Mi nombre es {name} {surname} {{hola}}" diff --git a/translatable/tests/integration.rs b/translatable/tests/integration.rs new file mode 100644 index 0000000..9aa6ef5 --- /dev/null +++ b/translatable/tests/integration.rs @@ -0,0 +1,36 @@ +use translatable::{Language, translation}; + +#[test] +fn test_both_dynamic() { + let lang = Language::EN; + let path = vec!["common", "greeting"]; + + let result = translation!(lang, path, name = "Everyone"); + assert_eq!(result.unwrap(), "Hi Everyone!"); +} + +#[test] +fn test_dynamic_lang_static_path() { + let lang = Language::ES; + let result = translation!(lang, static common::greeting, name = "Amigo"); + assert_eq!(result.unwrap(), "Β‘Hola Amigo!"); +} + +#[test] +fn test_static_lang_dynamic_path() { + let path = vec!["common", "greeting"]; + let result = translation!("en", path, name = "Friend"); + assert_eq!(result.unwrap(), "Hi Friend!"); +} + +#[test] +fn test_template_replacements() { + let result = translation!("es", static common::greeting, name = "Carlos"); + assert_eq!(result, "Β‘Hola Carlos!"); +} + +#[test] +fn test_static_lang_static_path() { + let result = translation!("en", static common::greeting, name = "Rustacean"); + assert_eq!(result, "Hi Rustacean!"); +} diff --git a/translatable/tests/static/args/empty_args.rs b/translatable/tests/static/args/empty_args.rs deleted file mode 100644 index 8762a36..0000000 --- a/translatable/tests/static/args/empty_args.rs +++ /dev/null @@ -1,6 +0,0 @@ -use translatable::translation; - -fn main() { - let res = translation!("en", static common::greeting); - assert_eq!(res, "Hello {name}!"); -} diff --git a/translatable/tests/static/args/extra_args.rs b/translatable/tests/static/args/extra_args.rs deleted file mode 100644 index a3e9f59..0000000 --- a/translatable/tests/static/args/extra_args.rs +++ /dev/null @@ -1,6 +0,0 @@ -use translatable::translation; - -fn main() { - let res = translation!("en", static common::greeting, name = "Juan", surname = "Doe"); - assert_eq!(res, "Hello Juan!"); -} diff --git a/translatable/tests/static/args/inherit_args.rs b/translatable/tests/static/args/inherit_args.rs deleted file mode 100644 index 3f2a7c4..0000000 --- a/translatable/tests/static/args/inherit_args.rs +++ /dev/null @@ -1,7 +0,0 @@ -use translatable::translation; - -fn main() { - let name = "Juan"; - let res = translation!("en", static common::greeting, name); - assert_eq!(res, "Hello Juan!"); -} diff --git a/translatable/tests/static/args/inherit_extra_args.rs b/translatable/tests/static/args/inherit_extra_args.rs deleted file mode 100644 index cbab001..0000000 --- a/translatable/tests/static/args/inherit_extra_args.rs +++ /dev/null @@ -1,9 +0,0 @@ -use translatable::translation; - -fn main() { - let name = "Juan"; - let surname = "Doe"; - - let res = translation!("en", static common::greeting, name, surname); - assert_eq!(res, "Hello Juan!"); -} diff --git a/translatable/tests/static/args/invalid_identifier.rs b/translatable/tests/static/args/invalid_identifier.rs deleted file mode 100644 index c08449c..0000000 --- a/translatable/tests/static/args/invalid_identifier.rs +++ /dev/null @@ -1,6 +0,0 @@ -use translatable::translation; - -fn main() { - // Invalid argument syntax - translation!("es", static common::greeting, 42 = "value"); -} diff --git a/translatable/tests/static/args/invalid_identifier.stderr b/translatable/tests/static/args/invalid_identifier.stderr deleted file mode 100644 index a87e974..0000000 --- a/translatable/tests/static/args/invalid_identifier.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: expected identifier - --> tests/static/args/invalid_identifier.rs:5:49 - | -5 | translation!("es", static common::greeting, 42 = "value"); - | ^^ diff --git a/translatable/tests/static/args/with_args.rs b/translatable/tests/static/args/with_args.rs deleted file mode 100644 index 4038d86..0000000 --- a/translatable/tests/static/args/with_args.rs +++ /dev/null @@ -1,6 +0,0 @@ -use translatable::translation; - -fn main() { - let res = translation!("en", static common::greeting, name = "Juan"); - assert_eq!(res, "Hello Juan!"); -} diff --git a/translatable/tests/static/invalid_syntax.rs b/translatable/tests/static/invalid_syntax.rs deleted file mode 100644 index ea3483d..0000000 --- a/translatable/tests/static/invalid_syntax.rs +++ /dev/null @@ -1,12 +0,0 @@ -use translatable::translation; - -fn main() { - // Missing arguments - let _ = translation!("es"); - - // Invalid literal type - let _ = translation!(42, static common::greeting); - - // Malformed static path - let _ = translation!("es", static invalid::path); -} diff --git a/translatable/tests/static/invalid_syntax.stderr b/translatable/tests/static/invalid_syntax.stderr deleted file mode 100644 index b08c3c5..0000000 --- a/translatable/tests/static/invalid_syntax.stderr +++ /dev/null @@ -1,31 +0,0 @@ -error: expected `,` - --> tests/static/invalid_syntax.rs:5:13 - | -5 | let _ = translation!("es"); - | ^^^^^^^^^^^^^^^^^^ - | - = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) - -error: The path 'invalid.path' is not found in any of the translation files as a translation object. - --> tests/static/invalid_syntax.rs:11:13 - | -11 | let _ = translation!("es", static invalid::path); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) - -error[E0277]: the trait bound `String: From<{integer}>` is not satisfied - --> tests/static/invalid_syntax.rs:8:13 - | -8 | let _ = translation!(42, static common::greeting); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `From<{integer}>` is not implemented for `String` - | - = help: the following other types implement trait `From`: - `String` implements `From<&String>` - `String` implements `From<&mut str>` - `String` implements `From<&str>` - `String` implements `From>` - `String` implements `From>` - `String` implements `From` - = note: required for `{integer}` to implement `Into` - = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/translatable/tests/tests.rs b/translatable/tests/tests.rs deleted file mode 100644 index 1e65fd8..0000000 --- a/translatable/tests/tests.rs +++ /dev/null @@ -1,33 +0,0 @@ -use trybuild::TestCases; - -#[test] -fn static_tests() { - let t = TestCases::new(); - - // implementation - t.compile_fail("tests/static/invalid_syntax.rs"); - - // args - t.pass("tests/static/args/empty_args.rs"); - t.pass("tests/static/args/extra_args.rs"); - t.pass("tests/static/args/inherit_args.rs"); - t.pass("tests/static/args/inherit_extra_args.rs"); - t.compile_fail("tests/static/args/invalid_identifier.rs"); - t.pass("tests/static/args/with_args.rs"); -} - -#[test] -fn dynamic_tests() { - let t = TestCases::new(); - - // implementation - t.compile_fail("tests/dynamic/invalid_syntax.rs"); - - // args - t.pass("tests/dynamic/args/empty_args.rs"); - t.pass("tests/dynamic/args/extra_args.rs"); - t.pass("tests/dynamic/args/inherit_args.rs"); - t.pass("tests/dynamic/args/inherit_extra_args.rs"); - t.compile_fail("tests/dynamic/args/invalid_identifier.rs"); - t.pass("tests/dynamic/args/with_args.rs"); -} diff --git a/translatable/tests/translations/test.toml b/translatable/tests/translations/test.toml deleted file mode 100644 index e63b1d2..0000000 --- a/translatable/tests/translations/test.toml +++ /dev/null @@ -1,7 +0,0 @@ -[welcome_message] -en = "Welcome to our app!" -es = "Β‘Bienvenido a nuestra aplicaciΓ³n!" - -[common.greeting] -en = "Hello {name} {surname}! My name is {name} {surname} {{hello}}" -es = "Β‘Hola {name} {surname}! Mi nombre es {name} {surname} {{hola}}" diff --git a/translatable/tests/ui.rs b/translatable/tests/ui.rs new file mode 100644 index 0000000..563b54a --- /dev/null +++ b/translatable/tests/ui.rs @@ -0,0 +1,8 @@ +use trybuild::TestCases; + +#[test] +fn ui_tests() { + let t = TestCases::new(); + t.pass("tests/ui/pass/*.rs"); + t.compile_fail("tests/ui/fail/*.rs"); +} diff --git a/translatable/tests/ui/fail/invalid_lang.rs b/translatable/tests/ui/fail/invalid_lang.rs new file mode 100644 index 0000000..3496f6c --- /dev/null +++ b/translatable/tests/ui/fail/invalid_lang.rs @@ -0,0 +1,6 @@ +use translatable::translation; + +fn main() { + // "zz" is not a valid ISO 639-1 language code + let _ = translation!("zz", static common::greeting, name = "Alice"); +} diff --git a/translatable/tests/ui/fail/invalid_lang.stderr b/translatable/tests/ui/fail/invalid_lang.stderr new file mode 100644 index 0000000..2e31968 --- /dev/null +++ b/translatable/tests/ui/fail/invalid_lang.stderr @@ -0,0 +1,5 @@ +error: The literal 'zz' is an invalid ISO 639-1 string, and cannot be parsed + --> tests/ui/fail/invalid_lang.rs:5:26 + | +5 | let _ = translation!("zz", static common::greeting, name = "Alice"); + | ^^^^ diff --git a/translatable/tests/ui/fail/missing_path.rs b/translatable/tests/ui/fail/missing_path.rs new file mode 100644 index 0000000..8769ec2 --- /dev/null +++ b/translatable/tests/ui/fail/missing_path.rs @@ -0,0 +1,6 @@ +use translatable::translation; + +fn main() { + // The path 'foo::bar' does not exist in TOML + let _ = translation!("en", static foo::bar, name = "X"); +} diff --git a/translatable/tests/ui/fail/missing_path.stderr b/translatable/tests/ui/fail/missing_path.stderr new file mode 100644 index 0000000..cddbe39 --- /dev/null +++ b/translatable/tests/ui/fail/missing_path.stderr @@ -0,0 +1,7 @@ +error: The path 'foo::bar' could not be found + --> tests/ui/fail/missing_path.rs:5:13 + | +5 | let _ = translation!("en", static foo::bar, name = "X"); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/translatable/tests/ui/fail/path_with_generics.rs b/translatable/tests/ui/fail/path_with_generics.rs new file mode 100644 index 0000000..b220c48 --- /dev/null +++ b/translatable/tests/ui/fail/path_with_generics.rs @@ -0,0 +1,5 @@ +use translatable::translation; + +fn main() { + let _ = translation!("en", static foo::Bar, name = "X"); +} diff --git a/translatable/tests/ui/fail/path_with_generics.stderr b/translatable/tests/ui/fail/path_with_generics.stderr new file mode 100644 index 0000000..fcc89fb --- /dev/null +++ b/translatable/tests/ui/fail/path_with_generics.stderr @@ -0,0 +1,5 @@ +error: This translation path contains generic arguments, and cannot be parsed + --> tests/ui/fail/path_with_generics.rs:4:47 + | +4 | let _ = translation!("en", static foo::Bar, name = "X"); + | ^^^ diff --git a/translatable/tests/ui/pass/dynamic_ok.rs b/translatable/tests/ui/pass/dynamic_ok.rs new file mode 100644 index 0000000..6b4729a --- /dev/null +++ b/translatable/tests/ui/pass/dynamic_ok.rs @@ -0,0 +1,10 @@ +use translatable::{Language, translation}; + +fn main() { + let lang = Language::EN; + let path = vec!["common", "greeting"]; + let name = "Juan"; + + let result = translation!(lang, path, name); + assert_eq!(result.unwrap(), "Hi Juan!"); +} diff --git a/translatable/tests/ui/pass/empty_args.rs b/translatable/tests/ui/pass/empty_args.rs new file mode 100644 index 0000000..a2765e6 --- /dev/null +++ b/translatable/tests/ui/pass/empty_args.rs @@ -0,0 +1,9 @@ +use translatable::{Language, translation}; + +fn main() { + let lang = Language::EN; + let path = vec!["common", "greeting"]; + + let res = translation!(lang, path); + assert_eq!(res.unwrap(), "Hi {name}!"); +} diff --git a/translatable/tests/ui/pass/extra_args.rs b/translatable/tests/ui/pass/extra_args.rs new file mode 100644 index 0000000..f1ad8cc --- /dev/null +++ b/translatable/tests/ui/pass/extra_args.rs @@ -0,0 +1,9 @@ +use translatable::{Language, translation}; + +fn main() { + let lang = Language::EN; + let path = vec!["common", "greeting"]; + + let res = translation!(lang, path, name = "Juan"); + assert_eq!(res.unwrap(), "Hi Juan!"); +} diff --git a/translatable/tests/ui/pass/static_ok.rs b/translatable/tests/ui/pass/static_ok.rs new file mode 100644 index 0000000..6bb123b --- /dev/null +++ b/translatable/tests/ui/pass/static_ok.rs @@ -0,0 +1,5 @@ +use translatable::translation; + +fn main() { + let _ = translation!("en", static common::greeting, name = "Alice"); +} From cef697cb2cd55de2210926e7ef30659e0c89c42f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Apr 2025 13:55:50 +0000 Subject: [PATCH 130/228] chore(deps): bump proc-macro2 from 1.0.94 to 1.0.95 Bumps [proc-macro2](https://github.com/dtolnay/proc-macro2) from 1.0.94 to 1.0.95. - [Release notes](https://github.com/dtolnay/proc-macro2/releases) - [Commits](https://github.com/dtolnay/proc-macro2/compare/1.0.94...1.0.95) --- updated-dependencies: - dependency-name: proc-macro2 dependency-version: 1.0.95 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- translatable_proc/Cargo.toml | 2 +- translatable_shared/Cargo.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4fc13ce..79e0f76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,9 +50,9 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] diff --git a/translatable_proc/Cargo.toml b/translatable_proc/Cargo.toml index d5f9cc3..5dd3e28 100644 --- a/translatable_proc/Cargo.toml +++ b/translatable_proc/Cargo.toml @@ -12,7 +12,7 @@ authors = ["Esteve Autet ", "Chiko "] proc-macro = true [dependencies] -proc-macro2 = "1.0.94" +proc-macro2 = "1.0.95" quote = "1.0.38" strum = { version = "0.27.1", features = ["derive"] } syn = { version = "2.0.98", features = ["full"] } diff --git a/translatable_shared/Cargo.toml b/translatable_shared/Cargo.toml index 0c6f030..5ae9cde 100644 --- a/translatable_shared/Cargo.toml +++ b/translatable_shared/Cargo.toml @@ -9,7 +9,7 @@ edition = "2024" authors = ["Esteve Autet ", "Chiko "] [dependencies] -proc-macro2 = "1.0.94" +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"] } From 435d24b7a1027838b1e78eefccd578f7f2022254 Mon Sep 17 00:00:00 2001 From: stifskere Date: Tue, 22 Apr 2025 00:47:46 +0200 Subject: [PATCH 131/228] chore: change license --- GPLv3-LICENSE | 619 +++++++++++++++++++++++++++++++++++++++++++++++++ LICENSE | 28 +++ LICENSE-APACHE | 176 -------------- LICENSE-MIT | 23 -- 4 files changed, 647 insertions(+), 199 deletions(-) create mode 100644 GPLv3-LICENSE create mode 100644 LICENSE delete mode 100644 LICENSE-APACHE delete mode 100644 LICENSE-MIT diff --git a/GPLv3-LICENSE b/GPLv3-LICENSE new file mode 100644 index 0000000..281d399 --- /dev/null +++ b/GPLv3-LICENSE @@ -0,0 +1,619 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..086a9a5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +This software is dual-licensed: + +1. GNU GENERAL PUBLIC LICENSE Version 3 (GPLv3) + You can redistribute and/or modify this software under the terms + of the GPLv3 as published by the Free Software Foundation. + + You should have received a copy of the GNU General Public License + along with this program (see `GPLv3-LICENSE`). If not, + see . + +2. Commercial License + Closed-source, proprietary, or commercial use of this software + requires a commercial license. + + If you want to use this software in a closed-source or proprietary + project, or if your project is not licensed under the GPLv3, please contact: + + Flaky, Sl. + licensing@flaky.es + + for licensing terms. + +--- + +Copyright (c) 2025-2030 Flaky, Sl. + +This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty +of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. diff --git a/LICENSE-APACHE b/LICENSE-APACHE deleted file mode 100644 index 1b5ec8b..0000000 --- a/LICENSE-APACHE +++ /dev/null @@ -1,176 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS diff --git a/LICENSE-MIT b/LICENSE-MIT deleted file mode 100644 index 31aa793..0000000 --- a/LICENSE-MIT +++ /dev/null @@ -1,23 +0,0 @@ -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. From 47987238d483dd3cf56766eb75603f3da2188176 Mon Sep 17 00:00:00 2001 From: stifskere Date: Tue, 22 Apr 2025 01:13:02 +0200 Subject: [PATCH 132/228] chore: replace licenses in other files and polish security --- LICENSE | 2 +- README.md | 15 +++++---------- SECURITY.md | 13 ++++++++----- translatable/Cargo.toml | 2 +- translatable_proc/Cargo.toml | 2 +- translatable_shared/Cargo.toml | 2 +- 6 files changed, 17 insertions(+), 19 deletions(-) diff --git a/LICENSE b/LICENSE index 086a9a5..0d012be 100644 --- a/LICENSE +++ b/LICENSE @@ -18,7 +18,7 @@ This software is dual-licensed: Flaky, Sl. licensing@flaky.es - for licensing terms. + for a custom license for your use case. --- diff --git a/README.md b/README.md index 4e462e5..76b9149 100644 --- a/README.md +++ b/README.md @@ -205,13 +205,8 @@ fn main() { ## License πŸ“œ - -This repository is licensed under either of Apache License, Version 2.0 -or MIT license at your option. - -
- -Unless you explicitly state any contribution intentionally submitted -for inclusion in translatable by you, as defined in the Apache-2.0 license, shall be -dual licensed as above, without any additional terms or conditions. - +This repository is dual licensed, TLDR. If your repository is open source, the library +is free of use, otherwise contact [licensing@flaky.es](mailto:licensing@flaky.es) for a custom license for your +use case. + +For more information read the [license](./LICENSE) file. diff --git a/SECURITY.md b/SECURITY.md index e51cf70..46fc58b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,16 +1,19 @@ # Security Vulnerabilities -This library does not directly interact with networking, or anything another person might be able to do to access the service using this library. - -**If you find any security issues, please reach out to any of the maintainers listed in our [GOVERNANCE.md].** We take all security reports seriously and will get back to you as soon as possible. +**If you find any security issues, please reach out to any of the maintainers +listed in our [governance.md].** We take all security reports seriously and +will get back to you as soon as possible. We also have security measures in place by using automated tools for managing dependencies. -Our project **strongly** relies on [Dependabot] to: +Our project **strongly** relies on [dependabot] to: + - Check for security vulnerabilities - Update dependencies when needed - Maintain all dependencies up to date -This automated system helps us apply security patches regularly, reducing the need for manual checks on dependencies and ensuring that we are using the latest versions of libraries to prevent security issues. +This automated system helps us apply security patches regularly, reducing the +need for manual checks on dependencies and ensuring that we are using the +latest versions of libraries to prevent security issues. [dependabot]: https://docs.github.com/en/code-security/dependabot [governance.md]: GOVERNANCE.md diff --git a/translatable/Cargo.toml b/translatable/Cargo.toml index a781cdf..65341db 100644 --- a/translatable/Cargo.toml +++ b/translatable/Cargo.toml @@ -2,7 +2,7 @@ name = "translatable" description = "A robust internationalization solution for Rust featuring compile-time validation, ISO 639-1 compliance, and TOML-based translation management. " repository = "https://github.com/FlakySL/translatable" -license = "MIT OR Apache-2.0" +license = "GPL-3.0" readme = "../README.md" version = "1.0.0" edition = "2024" diff --git a/translatable_proc/Cargo.toml b/translatable_proc/Cargo.toml index 5dd3e28..4734fe4 100644 --- a/translatable_proc/Cargo.toml +++ b/translatable_proc/Cargo.toml @@ -2,7 +2,7 @@ name = "translatable_proc" description = "Proc macro crate for the translatable library." repository = "https://github.com/FlakySL/translatable" -license = "MIT OR Apache-2.0" +license = "GPL-3.0" readme = "../README-MACROS.md" version = "1.0.0" edition = "2024" diff --git a/translatable_shared/Cargo.toml b/translatable_shared/Cargo.toml index 5ae9cde..36102f9 100644 --- a/translatable_shared/Cargo.toml +++ b/translatable_shared/Cargo.toml @@ -2,7 +2,7 @@ name = "translatable_shared" description = "Shared dependencies crate for translatable." repository = "https://github.com/FlakySL/translatable" -license = "MIT OR Apache-2.0" +license = "GPL-3.0" readme = "../README-SHARED.md" version = "1.0.0" edition = "2024" From bc27a7512ef2c9d35a240c8c0acc209968c7d6cb Mon Sep 17 00:00:00 2001 From: stifskere Date: Wed, 23 Apr 2025 21:33:02 +0200 Subject: [PATCH 133/228] chore: clear tests --- GOVERNANCE.md | 8 ++--- translatable/tests/fixtures/translatable.toml | 0 .../tests/fixtures/translations/common.toml | 3 -- .../tests/fixtures/translations/test.toml | 7 ---- translatable/tests/integration.rs | 36 ------------------- translatable/tests/ui.rs | 8 ----- translatable/tests/ui/fail/invalid_lang.rs | 6 ---- .../tests/ui/fail/invalid_lang.stderr | 5 --- translatable/tests/ui/fail/missing_path.rs | 6 ---- .../tests/ui/fail/missing_path.stderr | 7 ---- .../tests/ui/fail/path_with_generics.rs | 5 --- .../tests/ui/fail/path_with_generics.stderr | 5 --- translatable/tests/ui/pass/dynamic_ok.rs | 10 ------ translatable/tests/ui/pass/empty_args.rs | 9 ----- translatable/tests/ui/pass/extra_args.rs | 9 ----- translatable/tests/ui/pass/static_ok.rs | 5 --- translatable_proc/src/data/translations.rs | 2 +- 17 files changed, 4 insertions(+), 127 deletions(-) delete mode 100644 translatable/tests/fixtures/translatable.toml delete mode 100644 translatable/tests/fixtures/translations/common.toml delete mode 100644 translatable/tests/fixtures/translations/test.toml delete mode 100644 translatable/tests/integration.rs delete mode 100644 translatable/tests/ui.rs delete mode 100644 translatable/tests/ui/fail/invalid_lang.rs delete mode 100644 translatable/tests/ui/fail/invalid_lang.stderr delete mode 100644 translatable/tests/ui/fail/missing_path.rs delete mode 100644 translatable/tests/ui/fail/missing_path.stderr delete mode 100644 translatable/tests/ui/fail/path_with_generics.rs delete mode 100644 translatable/tests/ui/fail/path_with_generics.stderr delete mode 100644 translatable/tests/ui/pass/dynamic_ok.rs delete mode 100644 translatable/tests/ui/pass/empty_args.rs delete mode 100644 translatable/tests/ui/pass/extra_args.rs delete mode 100644 translatable/tests/ui/pass/static_ok.rs diff --git a/GOVERNANCE.md b/GOVERNANCE.md index b1387fe..80956b3 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -1,11 +1,9 @@ # Governance and Moderation -This project is mainly maintained by the authors listed in -`translatable/Cargo.toml`, `translatable_proc/Cargo.toml` and -`translatable_shared/Cargo.toml`. +This project is mainly maintained by the authors - Esteve Autet `esteve.autet@flaky.es` - Chiko `luis.degnan@flaky.es` -There is no hierarchy established (yet) but this might be subject to change soon. For any inquiries you can -contact any of the emails listed above. +There is no hierarchy established (yet) but this might be subject to +change soon. For any inquiries you can contact any of the emails listed above. diff --git a/translatable/tests/fixtures/translatable.toml b/translatable/tests/fixtures/translatable.toml deleted file mode 100644 index e69de29..0000000 diff --git a/translatable/tests/fixtures/translations/common.toml b/translatable/tests/fixtures/translations/common.toml deleted file mode 100644 index 869f64f..0000000 --- a/translatable/tests/fixtures/translations/common.toml +++ /dev/null @@ -1,3 +0,0 @@ -[common.greeting] -en = "Hi {name}!" -es = "Β‘Hola {name}!" diff --git a/translatable/tests/fixtures/translations/test.toml b/translatable/tests/fixtures/translations/test.toml deleted file mode 100644 index 13ee676..0000000 --- a/translatable/tests/fixtures/translations/test.toml +++ /dev/null @@ -1,7 +0,0 @@ -[welcome_message] -en = "Welcome to our app!" -es = "Β‘Bienvenido a nuestra aplicaciΓ³n!" - -# [common.greeting] -# en = "Hello {name} {surname}! My name is {name} {surname} {{hello}}" -# es = "Β‘Hola {name} {surname}! Mi nombre es {name} {surname} {{hola}}" diff --git a/translatable/tests/integration.rs b/translatable/tests/integration.rs deleted file mode 100644 index 9aa6ef5..0000000 --- a/translatable/tests/integration.rs +++ /dev/null @@ -1,36 +0,0 @@ -use translatable::{Language, translation}; - -#[test] -fn test_both_dynamic() { - let lang = Language::EN; - let path = vec!["common", "greeting"]; - - let result = translation!(lang, path, name = "Everyone"); - assert_eq!(result.unwrap(), "Hi Everyone!"); -} - -#[test] -fn test_dynamic_lang_static_path() { - let lang = Language::ES; - let result = translation!(lang, static common::greeting, name = "Amigo"); - assert_eq!(result.unwrap(), "Β‘Hola Amigo!"); -} - -#[test] -fn test_static_lang_dynamic_path() { - let path = vec!["common", "greeting"]; - let result = translation!("en", path, name = "Friend"); - assert_eq!(result.unwrap(), "Hi Friend!"); -} - -#[test] -fn test_template_replacements() { - let result = translation!("es", static common::greeting, name = "Carlos"); - assert_eq!(result, "Β‘Hola Carlos!"); -} - -#[test] -fn test_static_lang_static_path() { - let result = translation!("en", static common::greeting, name = "Rustacean"); - assert_eq!(result, "Hi Rustacean!"); -} diff --git a/translatable/tests/ui.rs b/translatable/tests/ui.rs deleted file mode 100644 index 563b54a..0000000 --- a/translatable/tests/ui.rs +++ /dev/null @@ -1,8 +0,0 @@ -use trybuild::TestCases; - -#[test] -fn ui_tests() { - let t = TestCases::new(); - t.pass("tests/ui/pass/*.rs"); - t.compile_fail("tests/ui/fail/*.rs"); -} diff --git a/translatable/tests/ui/fail/invalid_lang.rs b/translatable/tests/ui/fail/invalid_lang.rs deleted file mode 100644 index 3496f6c..0000000 --- a/translatable/tests/ui/fail/invalid_lang.rs +++ /dev/null @@ -1,6 +0,0 @@ -use translatable::translation; - -fn main() { - // "zz" is not a valid ISO 639-1 language code - let _ = translation!("zz", static common::greeting, name = "Alice"); -} diff --git a/translatable/tests/ui/fail/invalid_lang.stderr b/translatable/tests/ui/fail/invalid_lang.stderr deleted file mode 100644 index 2e31968..0000000 --- a/translatable/tests/ui/fail/invalid_lang.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: The literal 'zz' is an invalid ISO 639-1 string, and cannot be parsed - --> tests/ui/fail/invalid_lang.rs:5:26 - | -5 | let _ = translation!("zz", static common::greeting, name = "Alice"); - | ^^^^ diff --git a/translatable/tests/ui/fail/missing_path.rs b/translatable/tests/ui/fail/missing_path.rs deleted file mode 100644 index 8769ec2..0000000 --- a/translatable/tests/ui/fail/missing_path.rs +++ /dev/null @@ -1,6 +0,0 @@ -use translatable::translation; - -fn main() { - // The path 'foo::bar' does not exist in TOML - let _ = translation!("en", static foo::bar, name = "X"); -} diff --git a/translatable/tests/ui/fail/missing_path.stderr b/translatable/tests/ui/fail/missing_path.stderr deleted file mode 100644 index cddbe39..0000000 --- a/translatable/tests/ui/fail/missing_path.stderr +++ /dev/null @@ -1,7 +0,0 @@ -error: The path 'foo::bar' could not be found - --> tests/ui/fail/missing_path.rs:5:13 - | -5 | let _ = translation!("en", static foo::bar, name = "X"); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: this error originates in the macro `translation` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/translatable/tests/ui/fail/path_with_generics.rs b/translatable/tests/ui/fail/path_with_generics.rs deleted file mode 100644 index b220c48..0000000 --- a/translatable/tests/ui/fail/path_with_generics.rs +++ /dev/null @@ -1,5 +0,0 @@ -use translatable::translation; - -fn main() { - let _ = translation!("en", static foo::Bar, name = "X"); -} diff --git a/translatable/tests/ui/fail/path_with_generics.stderr b/translatable/tests/ui/fail/path_with_generics.stderr deleted file mode 100644 index fcc89fb..0000000 --- a/translatable/tests/ui/fail/path_with_generics.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: This translation path contains generic arguments, and cannot be parsed - --> tests/ui/fail/path_with_generics.rs:4:47 - | -4 | let _ = translation!("en", static foo::Bar, name = "X"); - | ^^^ diff --git a/translatable/tests/ui/pass/dynamic_ok.rs b/translatable/tests/ui/pass/dynamic_ok.rs deleted file mode 100644 index 6b4729a..0000000 --- a/translatable/tests/ui/pass/dynamic_ok.rs +++ /dev/null @@ -1,10 +0,0 @@ -use translatable::{Language, translation}; - -fn main() { - let lang = Language::EN; - let path = vec!["common", "greeting"]; - let name = "Juan"; - - let result = translation!(lang, path, name); - assert_eq!(result.unwrap(), "Hi Juan!"); -} diff --git a/translatable/tests/ui/pass/empty_args.rs b/translatable/tests/ui/pass/empty_args.rs deleted file mode 100644 index a2765e6..0000000 --- a/translatable/tests/ui/pass/empty_args.rs +++ /dev/null @@ -1,9 +0,0 @@ -use translatable::{Language, translation}; - -fn main() { - let lang = Language::EN; - let path = vec!["common", "greeting"]; - - let res = translation!(lang, path); - assert_eq!(res.unwrap(), "Hi {name}!"); -} diff --git a/translatable/tests/ui/pass/extra_args.rs b/translatable/tests/ui/pass/extra_args.rs deleted file mode 100644 index f1ad8cc..0000000 --- a/translatable/tests/ui/pass/extra_args.rs +++ /dev/null @@ -1,9 +0,0 @@ -use translatable::{Language, translation}; - -fn main() { - let lang = Language::EN; - let path = vec!["common", "greeting"]; - - let res = translation!(lang, path, name = "Juan"); - assert_eq!(res.unwrap(), "Hi Juan!"); -} diff --git a/translatable/tests/ui/pass/static_ok.rs b/translatable/tests/ui/pass/static_ok.rs deleted file mode 100644 index 6bb123b..0000000 --- a/translatable/tests/ui/pass/static_ok.rs +++ /dev/null @@ -1,5 +0,0 @@ -use translatable::translation; - -fn main() { - let _ = translation!("en", static common::greeting, name = "Alice"); -} diff --git a/translatable_proc/src/data/translations.rs b/translatable_proc/src/data/translations.rs index 21bb9bf..2334aba 100644 --- a/translatable_proc/src/data/translations.rs +++ b/translatable_proc/src/data/translations.rs @@ -46,7 +46,7 @@ pub enum TranslationDataError { /// /// [`From`]: std::io::Error /// [`Display`]: std::fmt::Display - #[error("There was a problem with an IO operation: {0:#}")] + #[error("IO Error: \"{0:#}\". Please check the specified path in your configuration file.")] Io(#[from] IoError), /// Configuration loading failure. From fdaddb90240b07512821d3d0d90cc9cb32e4d0df Mon Sep 17 00:00:00 2001 From: stifskere Date: Thu, 24 Apr 2025 05:20:22 +0200 Subject: [PATCH 134/228] chore: add test case files --- Cargo.lock | 1 - translatable/Cargo.toml | 1 - translatable/tests/ui.rs | 0 translatable/tests/ui/config/fail_config_invalid_enums.rs | 0 translatable/tests/ui/config/fail_config_path_missmatch.rs | 0 translatable/tests/ui/config/fail_translations_malformed.rs | 0 .../tests/ui/environments/config_invalid_value/translatable.toml | 0 .../ui/environments/config_path_missmatch/translatable.toml | 0 .../tests/ui/environments/everything_valid/translatable.toml | 0 .../ui/environments/translations_malformed/translatable.toml | 0 translatable/tests/ui/language/fail_static_invalid.rs | 0 translatable/tests/ui/language/pass_dynamic_enum.rs | 0 translatable/tests/ui/language/pass_dynamic_expr.rs | 0 translatable/tests/ui/language/pass_dynamic_invalid_runtime.rs | 0 translatable/tests/ui/language/pass_dynamic_str.rs | 0 translatable/tests/ui/language/pass_static_lowercase.rs | 0 translatable/tests/ui/language/pass_static_uppercase.rs | 0 translatable/tests/ui/path/fail_static_nonexistent.rs | 0 translatable/tests/ui/path/pass_dynamic_expr.rs | 0 translatable/tests/ui/path/pass_dynamic_invalid_runtime.rs | 0 translatable/tests/ui/path/pass_dynamic_var.rs | 0 translatable/tests/ui/path/pass_dynamic_vec.rs | 0 translatable/tests/ui/path/pass_static_existing.rs | 0 translatable/tests/ui/template/fail_invalid_ident.rs | 0 translatable/tests/ui/template/fail_not_display.rs | 0 translatable/tests/ui/template/pass_display.rs | 0 translatable/tests/ui/template/pass_ident_ref.rs | 0 translatable/tests/ui/template/pass_trailing_comma.rs | 0 translatable/tests/ui/template/pass_trailing_comma_no_args.rs | 0 29 files changed, 2 deletions(-) create mode 100644 translatable/tests/ui.rs create mode 100644 translatable/tests/ui/config/fail_config_invalid_enums.rs create mode 100644 translatable/tests/ui/config/fail_config_path_missmatch.rs create mode 100644 translatable/tests/ui/config/fail_translations_malformed.rs create mode 100644 translatable/tests/ui/environments/config_invalid_value/translatable.toml create mode 100644 translatable/tests/ui/environments/config_path_missmatch/translatable.toml create mode 100644 translatable/tests/ui/environments/everything_valid/translatable.toml create mode 100644 translatable/tests/ui/environments/translations_malformed/translatable.toml create mode 100644 translatable/tests/ui/language/fail_static_invalid.rs create mode 100644 translatable/tests/ui/language/pass_dynamic_enum.rs create mode 100644 translatable/tests/ui/language/pass_dynamic_expr.rs create mode 100644 translatable/tests/ui/language/pass_dynamic_invalid_runtime.rs create mode 100644 translatable/tests/ui/language/pass_dynamic_str.rs create mode 100644 translatable/tests/ui/language/pass_static_lowercase.rs create mode 100644 translatable/tests/ui/language/pass_static_uppercase.rs create mode 100644 translatable/tests/ui/path/fail_static_nonexistent.rs create mode 100644 translatable/tests/ui/path/pass_dynamic_expr.rs create mode 100644 translatable/tests/ui/path/pass_dynamic_invalid_runtime.rs create mode 100644 translatable/tests/ui/path/pass_dynamic_var.rs create mode 100644 translatable/tests/ui/path/pass_dynamic_vec.rs create mode 100644 translatable/tests/ui/path/pass_static_existing.rs create mode 100644 translatable/tests/ui/template/fail_invalid_ident.rs create mode 100644 translatable/tests/ui/template/fail_not_display.rs create mode 100644 translatable/tests/ui/template/pass_display.rs create mode 100644 translatable/tests/ui/template/pass_ident_ref.rs create mode 100644 translatable/tests/ui/template/pass_trailing_comma.rs create mode 100644 translatable/tests/ui/template/pass_trailing_comma_no_args.rs diff --git a/Cargo.lock b/Cargo.lock index 79e0f76..9b05ea1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -225,7 +225,6 @@ dependencies = [ name = "translatable" version = "1.0.0" dependencies = [ - "strum", "thiserror", "translatable_proc", "translatable_shared", diff --git a/translatable/Cargo.toml b/translatable/Cargo.toml index 65341db..8a7add9 100644 --- a/translatable/Cargo.toml +++ b/translatable/Cargo.toml @@ -16,7 +16,6 @@ keywords = [ ] [dependencies] -strum = { version = "0.27.1", features = ["derive"] } thiserror = "2.0.12" translatable_proc = { version = "1", path = "../translatable_proc" } translatable_shared = { version = "1", path = "../translatable_shared/" } diff --git a/translatable/tests/ui.rs b/translatable/tests/ui.rs new file mode 100644 index 0000000..e69de29 diff --git a/translatable/tests/ui/config/fail_config_invalid_enums.rs b/translatable/tests/ui/config/fail_config_invalid_enums.rs new file mode 100644 index 0000000..e69de29 diff --git a/translatable/tests/ui/config/fail_config_path_missmatch.rs b/translatable/tests/ui/config/fail_config_path_missmatch.rs new file mode 100644 index 0000000..e69de29 diff --git a/translatable/tests/ui/config/fail_translations_malformed.rs b/translatable/tests/ui/config/fail_translations_malformed.rs new file mode 100644 index 0000000..e69de29 diff --git a/translatable/tests/ui/environments/config_invalid_value/translatable.toml b/translatable/tests/ui/environments/config_invalid_value/translatable.toml new file mode 100644 index 0000000..e69de29 diff --git a/translatable/tests/ui/environments/config_path_missmatch/translatable.toml b/translatable/tests/ui/environments/config_path_missmatch/translatable.toml new file mode 100644 index 0000000..e69de29 diff --git a/translatable/tests/ui/environments/everything_valid/translatable.toml b/translatable/tests/ui/environments/everything_valid/translatable.toml new file mode 100644 index 0000000..e69de29 diff --git a/translatable/tests/ui/environments/translations_malformed/translatable.toml b/translatable/tests/ui/environments/translations_malformed/translatable.toml new file mode 100644 index 0000000..e69de29 diff --git a/translatable/tests/ui/language/fail_static_invalid.rs b/translatable/tests/ui/language/fail_static_invalid.rs new file mode 100644 index 0000000..e69de29 diff --git a/translatable/tests/ui/language/pass_dynamic_enum.rs b/translatable/tests/ui/language/pass_dynamic_enum.rs new file mode 100644 index 0000000..e69de29 diff --git a/translatable/tests/ui/language/pass_dynamic_expr.rs b/translatable/tests/ui/language/pass_dynamic_expr.rs new file mode 100644 index 0000000..e69de29 diff --git a/translatable/tests/ui/language/pass_dynamic_invalid_runtime.rs b/translatable/tests/ui/language/pass_dynamic_invalid_runtime.rs new file mode 100644 index 0000000..e69de29 diff --git a/translatable/tests/ui/language/pass_dynamic_str.rs b/translatable/tests/ui/language/pass_dynamic_str.rs new file mode 100644 index 0000000..e69de29 diff --git a/translatable/tests/ui/language/pass_static_lowercase.rs b/translatable/tests/ui/language/pass_static_lowercase.rs new file mode 100644 index 0000000..e69de29 diff --git a/translatable/tests/ui/language/pass_static_uppercase.rs b/translatable/tests/ui/language/pass_static_uppercase.rs new file mode 100644 index 0000000..e69de29 diff --git a/translatable/tests/ui/path/fail_static_nonexistent.rs b/translatable/tests/ui/path/fail_static_nonexistent.rs new file mode 100644 index 0000000..e69de29 diff --git a/translatable/tests/ui/path/pass_dynamic_expr.rs b/translatable/tests/ui/path/pass_dynamic_expr.rs new file mode 100644 index 0000000..e69de29 diff --git a/translatable/tests/ui/path/pass_dynamic_invalid_runtime.rs b/translatable/tests/ui/path/pass_dynamic_invalid_runtime.rs new file mode 100644 index 0000000..e69de29 diff --git a/translatable/tests/ui/path/pass_dynamic_var.rs b/translatable/tests/ui/path/pass_dynamic_var.rs new file mode 100644 index 0000000..e69de29 diff --git a/translatable/tests/ui/path/pass_dynamic_vec.rs b/translatable/tests/ui/path/pass_dynamic_vec.rs new file mode 100644 index 0000000..e69de29 diff --git a/translatable/tests/ui/path/pass_static_existing.rs b/translatable/tests/ui/path/pass_static_existing.rs new file mode 100644 index 0000000..e69de29 diff --git a/translatable/tests/ui/template/fail_invalid_ident.rs b/translatable/tests/ui/template/fail_invalid_ident.rs new file mode 100644 index 0000000..e69de29 diff --git a/translatable/tests/ui/template/fail_not_display.rs b/translatable/tests/ui/template/fail_not_display.rs new file mode 100644 index 0000000..e69de29 diff --git a/translatable/tests/ui/template/pass_display.rs b/translatable/tests/ui/template/pass_display.rs new file mode 100644 index 0000000..e69de29 diff --git a/translatable/tests/ui/template/pass_ident_ref.rs b/translatable/tests/ui/template/pass_ident_ref.rs new file mode 100644 index 0000000..e69de29 diff --git a/translatable/tests/ui/template/pass_trailing_comma.rs b/translatable/tests/ui/template/pass_trailing_comma.rs new file mode 100644 index 0000000..e69de29 diff --git a/translatable/tests/ui/template/pass_trailing_comma_no_args.rs b/translatable/tests/ui/template/pass_trailing_comma_no_args.rs new file mode 100644 index 0000000..e69de29 From 39d6158a35f3122a298c3d327bd2f7abc6f823b9 Mon Sep 17 00:00:00 2001 From: stifskere Date: Thu, 24 Apr 2025 05:30:01 +0200 Subject: [PATCH 135/228] chore: load test cases --- translatable/tests/ui.rs | 26 +++++++++++++++++++ .../fail_invalid_ident.rs | 0 .../fail_not_display.rs | 0 .../{template => templates}/pass_display.rs | 0 .../{template => templates}/pass_ident_ref.rs | 0 .../pass_trailing_comma.rs | 0 .../pass_trailing_comma_no_args.rs | 0 7 files changed, 26 insertions(+) rename translatable/tests/ui/{template => templates}/fail_invalid_ident.rs (100%) rename translatable/tests/ui/{template => templates}/fail_not_display.rs (100%) rename translatable/tests/ui/{template => templates}/pass_display.rs (100%) rename translatable/tests/ui/{template => templates}/pass_ident_ref.rs (100%) rename translatable/tests/ui/{template => templates}/pass_trailing_comma.rs (100%) rename translatable/tests/ui/{template => templates}/pass_trailing_comma_no_args.rs (100%) diff --git a/translatable/tests/ui.rs b/translatable/tests/ui.rs index e69de29..5bdea4d 100644 --- a/translatable/tests/ui.rs +++ b/translatable/tests/ui.rs @@ -0,0 +1,26 @@ +use std::env::set_current_dir; + +use trybuild::TestCases; + +fn set_test_environment(environment: &str) { + set_current_dir(format!("tests/ui/environments/{environment}")) + .expect("Should be able to change environment."); +} + +#[test] +fn ui_tests() { + set_test_environment("everything_valid"); + + let t = TestCases::new(); + + t.pass("./ui/language/pass*.rs"); + t.compile_fail("./ui/language/fail*.rs"); + + t.pass("./ui/path/pass*.rs"); + t.compile_fail("./ui/path/fail*.rs"); + + t.pass("./ui/templates/pass*.rs"); + t.compile_fail("./ui/templates/fail*.rs"); + + // TODO: run each test with it's set environment. +} diff --git a/translatable/tests/ui/template/fail_invalid_ident.rs b/translatable/tests/ui/templates/fail_invalid_ident.rs similarity index 100% rename from translatable/tests/ui/template/fail_invalid_ident.rs rename to translatable/tests/ui/templates/fail_invalid_ident.rs diff --git a/translatable/tests/ui/template/fail_not_display.rs b/translatable/tests/ui/templates/fail_not_display.rs similarity index 100% rename from translatable/tests/ui/template/fail_not_display.rs rename to translatable/tests/ui/templates/fail_not_display.rs diff --git a/translatable/tests/ui/template/pass_display.rs b/translatable/tests/ui/templates/pass_display.rs similarity index 100% rename from translatable/tests/ui/template/pass_display.rs rename to translatable/tests/ui/templates/pass_display.rs diff --git a/translatable/tests/ui/template/pass_ident_ref.rs b/translatable/tests/ui/templates/pass_ident_ref.rs similarity index 100% rename from translatable/tests/ui/template/pass_ident_ref.rs rename to translatable/tests/ui/templates/pass_ident_ref.rs diff --git a/translatable/tests/ui/template/pass_trailing_comma.rs b/translatable/tests/ui/templates/pass_trailing_comma.rs similarity index 100% rename from translatable/tests/ui/template/pass_trailing_comma.rs rename to translatable/tests/ui/templates/pass_trailing_comma.rs diff --git a/translatable/tests/ui/template/pass_trailing_comma_no_args.rs b/translatable/tests/ui/templates/pass_trailing_comma_no_args.rs similarity index 100% rename from translatable/tests/ui/template/pass_trailing_comma_no_args.rs rename to translatable/tests/ui/templates/pass_trailing_comma_no_args.rs From 7a128a36ea21e548ba2af4b9d3d861a6b84f5b54 Mon Sep 17 00:00:00 2001 From: stifskere Date: Thu, 24 Apr 2025 07:54:59 +0200 Subject: [PATCH 136/228] chore: specify configuration tests --- Makefile | 2 +- translatable/tests/ui.rs | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index ab2fe9a..1dd1733 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ export TRANSLATABLE_LOCALES_PATH=${PWD}/translatable/tests/fixtures/translations test: - cargo test -p translatable -- --nocapture --color=always + cargo test -p translatable -- --nocapture --color=always --test-threads=1 diff --git a/translatable/tests/ui.rs b/translatable/tests/ui.rs index 5bdea4d..3b2e4e2 100644 --- a/translatable/tests/ui.rs +++ b/translatable/tests/ui.rs @@ -9,10 +9,11 @@ fn set_test_environment(environment: &str) { #[test] fn ui_tests() { - set_test_environment("everything_valid"); - let t = TestCases::new(); + // general test cases. + set_test_environment("everything_valid"); + t.pass("./ui/language/pass*.rs"); t.compile_fail("./ui/language/fail*.rs"); @@ -22,5 +23,15 @@ fn ui_tests() { t.pass("./ui/templates/pass*.rs"); t.compile_fail("./ui/templates/fail*.rs"); - // TODO: run each test with it's set environment. + // invalid path in configuration. + set_test_environment("config_path_missmatch"); + t.compile_fail("./ui/config/fail_config_path_missmatch.rs"); + + // invalid enum value in configuration. + set_test_environment("config_invalid_value"); + t.compile_fail("./ui/config/fail_config_invalid_enums.rs"); + + // translation file rule broken. + set_test_environment("translations_malformed"); + t.compile_fail("./ui/config/fail_translations_malformed.rs"); } From 417b5abd8152eb7b87f1bebda367a7a0405a400c Mon Sep 17 00:00:00 2001 From: stifskere Date: Thu, 24 Apr 2025 08:12:14 +0200 Subject: [PATCH 137/228] docs: specify template validation and change call matrix return types --- README.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 76b9149..eff3ef4 100644 --- a/README.md +++ b/README.md @@ -137,15 +137,20 @@ that the value must always implement `Display`. Otherwise, if you want to have a you can escape it the same way `format!` does, by using `{{}}`. Just like object construction works in rust, if you have a parameter like `x = x`, you can shorten it to `x`. The keys inside braces are XID validated. +Have in mind that templates are specific to each translation, each language can contain it's own set +of templates, it is recommended that while loading a translation all the possible templates and combinations +are set if the language is dynamic. Templates are not validated, they are just replaced if found, otherwise +ignored, if not found the original template will remain untouched. + Depending on whether the parameters are static or dynamic the macro will act different, differing whether the checks are compile-time or run-time, the following table is a macro behavior matrix. -| Parameters | Compile-Time checks | Return type | -|----------------------------------------------------|----------------------------------------------------------|-----------------------------------------------------------------------------------| -| `static language` + `static path` (most optimized) | Path existence, Language validity | `&'static str` (stack) if there are no templates or `String` (heap) if there are. | -| `dynamic language` + `dynamic path` | None | `Result` (heap) | -| `static language` + `dynamic path` | Language validity | `Result` (heap) | -| `dynamic language` + `static path` (commonly used) | Path existence | `Result` (heap) | +| Parameters | Compile-Time checks | Return type | +|----------------------------------------------------|-----------------------------------|-------------------------| +| `static language` + `static path` (most optimized) | Path existence, Language validity | `String` | +| `dynamic language` + `dynamic path` | None | `Result` | +| `static language` + `dynamic path` | Language validity | `Result` | +| `dynamic language` + `static path` (commonly used) | Path existence | `Result` | - For the error handling, if you want to integrate this with `thiserror` you can use a `#[from] translatable::Error`, as a nested error, all the errors implement display. From 3e00c24f0adda94b3730d5f4e6dcf3922d3fdca2 Mon Sep 17 00:00:00 2001 From: stifskere Date: Thu, 24 Apr 2025 08:15:59 +0200 Subject: [PATCH 138/228] chore(tests): prepare test environments --- .../config_invalid_value/translatable.toml | 2 ++ .../config_invalid_value/translations/test.toml | 5 +++++ .../config_path_missmatch/translatable.toml | 2 ++ .../everything_valid/translatable.toml | 0 .../everything_valid/translations/test.toml | 16 ++++++++++++++++ .../translations_malformed/translatable.toml | 0 .../translations/test.toml | 3 +++ 7 files changed, 28 insertions(+) create mode 100644 translatable/tests/ui/environments/config_invalid_value/translations/test.toml delete mode 100644 translatable/tests/ui/environments/everything_valid/translatable.toml create mode 100644 translatable/tests/ui/environments/everything_valid/translations/test.toml delete mode 100644 translatable/tests/ui/environments/translations_malformed/translatable.toml create mode 100644 translatable/tests/ui/environments/translations_malformed/translations/test.toml diff --git a/translatable/tests/ui/environments/config_invalid_value/translatable.toml b/translatable/tests/ui/environments/config_invalid_value/translatable.toml index e69de29..caa05b2 100644 --- a/translatable/tests/ui/environments/config_invalid_value/translatable.toml +++ b/translatable/tests/ui/environments/config_invalid_value/translatable.toml @@ -0,0 +1,2 @@ + +seek_mode = "invalid value" diff --git a/translatable/tests/ui/environments/config_invalid_value/translations/test.toml b/translatable/tests/ui/environments/config_invalid_value/translations/test.toml new file mode 100644 index 0000000..527b087 --- /dev/null +++ b/translatable/tests/ui/environments/config_invalid_value/translations/test.toml @@ -0,0 +1,5 @@ +# There is nothing to load in this test set. +# This remains to avoid *other* errors while expecting invalid value error. +# +# By logic the configuration loading errors should precede. But this is +# better left undefined behavior. diff --git a/translatable/tests/ui/environments/config_path_missmatch/translatable.toml b/translatable/tests/ui/environments/config_path_missmatch/translatable.toml index e69de29..2bf7e69 100644 --- a/translatable/tests/ui/environments/config_path_missmatch/translatable.toml +++ b/translatable/tests/ui/environments/config_path_missmatch/translatable.toml @@ -0,0 +1,2 @@ +# A path missmatch should cause a "file not found" error. +# If the default path (./translations) does not exist, that will raise. diff --git a/translatable/tests/ui/environments/everything_valid/translatable.toml b/translatable/tests/ui/environments/everything_valid/translatable.toml deleted file mode 100644 index e69de29..0000000 diff --git a/translatable/tests/ui/environments/everything_valid/translations/test.toml b/translatable/tests/ui/environments/everything_valid/translations/test.toml new file mode 100644 index 0000000..6c88b20 --- /dev/null +++ b/translatable/tests/ui/environments/everything_valid/translations/test.toml @@ -0,0 +1,16 @@ + +# 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/ui/environments/translations_malformed/translatable.toml b/translatable/tests/ui/environments/translations_malformed/translatable.toml deleted file mode 100644 index e69de29..0000000 diff --git a/translatable/tests/ui/environments/translations_malformed/translations/test.toml b/translatable/tests/ui/environments/translations_malformed/translations/test.toml new file mode 100644 index 0000000..ba40268 --- /dev/null +++ b/translatable/tests/ui/environments/translations_malformed/translations/test.toml @@ -0,0 +1,3 @@ + +[some.translation] +value = 1 From 024c6204e9a69b06682e1a41c4f6ab02fc852c98 Mon Sep 17 00:00:00 2001 From: stifskere Date: Thu, 24 Apr 2025 23:11:21 +0200 Subject: [PATCH 139/228] chore(tests): change ui to integration and document possible test manual --- translatable/tests/README.md | 28 +++++++++++++++++++ .../config/fail_config_invalid_enums.rs | 8 ++++++ .../config/fail_config_path_missmatch.rs | 8 ++++++ .../config/fail_translations_malformed.rs | 8 ++++++ .../config_invalid_value/translatable.toml | 0 .../translations/test.toml | 0 .../config_path_missmatch/translatable.toml | 0 .../everything_valid/translations/test.toml | 0 .../translations/test.toml | 0 .../language/fail_static_invalid.rs | 5 ++++ .../tests/integration/language/mod.rs | 7 +++++ .../integration/language/pass_dynamic_enum.rs | 8 ++++++ .../integration/language/pass_dynamic_expr.rs | 5 ++++ .../language/pass_dynamic_invalid_runtime.rs | 0 .../language/pass_dynamic_str.rs | 0 .../language/pass_static_lowercase.rs | 0 .../language/pass_static_uppercase.rs | 0 translatable/tests/integration/mod.rs | 4 +++ .../path/fail_static_nonexistent.rs | 0 translatable/tests/integration/path/mod.rs | 6 ++++ .../path}/pass_dynamic_expr.rs | 0 .../path/pass_dynamic_invalid_runtime.rs | 0 .../path/pass_dynamic_var.rs | 0 .../path/pass_dynamic_vec.rs | 0 .../path/pass_static_existing.rs | 0 .../templates/fail_invalid_ident.rs | 0 .../templates/fail_not_display.rs | 0 .../tests/integration/templates/mod.rs | 5 ++++ .../templates/pass_display.rs | 0 .../templates/pass_ident_ref.rs | 0 .../templates/pass_trailing_comma.rs | 0 .../templates/pass_trailing_comma_no_args.rs | 0 .../tests/{ui.rs => integration_tests.rs} | 13 +++++++-- .../ui/config/fail_config_invalid_enums.rs | 0 .../ui/config/fail_config_path_missmatch.rs | 0 .../ui/config/fail_translations_malformed.rs | 0 .../tests/ui/language/fail_static_invalid.rs | 0 .../tests/ui/language/pass_dynamic_enum.rs | 0 .../tests/ui/path/pass_dynamic_expr.rs | 0 39 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 translatable/tests/README.md create mode 100644 translatable/tests/integration/config/fail_config_invalid_enums.rs create mode 100644 translatable/tests/integration/config/fail_config_path_missmatch.rs create mode 100644 translatable/tests/integration/config/fail_translations_malformed.rs rename translatable/tests/{ui => integration}/environments/config_invalid_value/translatable.toml (100%) rename translatable/tests/{ui => integration}/environments/config_invalid_value/translations/test.toml (100%) rename translatable/tests/{ui => integration}/environments/config_path_missmatch/translatable.toml (100%) rename translatable/tests/{ui => integration}/environments/everything_valid/translations/test.toml (100%) rename translatable/tests/{ui => integration}/environments/translations_malformed/translations/test.toml (100%) create mode 100644 translatable/tests/integration/language/fail_static_invalid.rs create mode 100644 translatable/tests/integration/language/mod.rs create mode 100644 translatable/tests/integration/language/pass_dynamic_enum.rs create mode 100644 translatable/tests/integration/language/pass_dynamic_expr.rs rename translatable/tests/{ui => integration}/language/pass_dynamic_invalid_runtime.rs (100%) rename translatable/tests/{ui => integration}/language/pass_dynamic_str.rs (100%) rename translatable/tests/{ui => integration}/language/pass_static_lowercase.rs (100%) rename translatable/tests/{ui => integration}/language/pass_static_uppercase.rs (100%) create mode 100644 translatable/tests/integration/mod.rs rename translatable/tests/{ui => integration}/path/fail_static_nonexistent.rs (100%) create mode 100644 translatable/tests/integration/path/mod.rs rename translatable/tests/{ui/language => integration/path}/pass_dynamic_expr.rs (100%) rename translatable/tests/{ui => integration}/path/pass_dynamic_invalid_runtime.rs (100%) rename translatable/tests/{ui => integration}/path/pass_dynamic_var.rs (100%) rename translatable/tests/{ui => integration}/path/pass_dynamic_vec.rs (100%) rename translatable/tests/{ui => integration}/path/pass_static_existing.rs (100%) rename translatable/tests/{ui => integration}/templates/fail_invalid_ident.rs (100%) rename translatable/tests/{ui => integration}/templates/fail_not_display.rs (100%) create mode 100644 translatable/tests/integration/templates/mod.rs rename translatable/tests/{ui => integration}/templates/pass_display.rs (100%) rename translatable/tests/{ui => integration}/templates/pass_ident_ref.rs (100%) rename translatable/tests/{ui => integration}/templates/pass_trailing_comma.rs (100%) rename translatable/tests/{ui => integration}/templates/pass_trailing_comma_no_args.rs (100%) rename translatable/tests/{ui.rs => integration_tests.rs} (82%) delete mode 100644 translatable/tests/ui/config/fail_config_invalid_enums.rs delete mode 100644 translatable/tests/ui/config/fail_config_path_missmatch.rs delete mode 100644 translatable/tests/ui/config/fail_translations_malformed.rs delete mode 100644 translatable/tests/ui/language/fail_static_invalid.rs delete mode 100644 translatable/tests/ui/language/pass_dynamic_enum.rs delete mode 100644 translatable/tests/ui/path/pass_dynamic_expr.rs diff --git a/translatable/tests/README.md b/translatable/tests/README.md new file mode 100644 index 0000000..999d72e --- /dev/null +++ b/translatable/tests/README.md @@ -0,0 +1,28 @@ +# 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. diff --git a/translatable/tests/integration/config/fail_config_invalid_enums.rs b/translatable/tests/integration/config/fail_config_invalid_enums.rs new file mode 100644 index 0000000..3b14ede --- /dev/null +++ b/translatable/tests/integration/config/fail_config_invalid_enums.rs @@ -0,0 +1,8 @@ +// the macro isn't filled because the expected +// failure is on configuration. + +use translatable::{translation, Language}; + +fn fail_config_invalid_enums() { + translation!(Language::ES, vec![]); +} diff --git a/translatable/tests/integration/config/fail_config_path_missmatch.rs b/translatable/tests/integration/config/fail_config_path_missmatch.rs new file mode 100644 index 0000000..52ea5ee --- /dev/null +++ b/translatable/tests/integration/config/fail_config_path_missmatch.rs @@ -0,0 +1,8 @@ +// the macro isn't filled because the expected +// failure is on configuration. + +use translatable::{translation, Language}; + +fn fail_config_path_missmatch() { + translation!(Language::ES, vec![]); +} diff --git a/translatable/tests/integration/config/fail_translations_malformed.rs b/translatable/tests/integration/config/fail_translations_malformed.rs new file mode 100644 index 0000000..604bb53 --- /dev/null +++ b/translatable/tests/integration/config/fail_translations_malformed.rs @@ -0,0 +1,8 @@ +// the macro isn't filled because the expected +// failure is on configuration. + +use translatable::{translation, Language}; + +fn fail_translations_malformed() { + translation!(Language::ES, vec![]); +} diff --git a/translatable/tests/ui/environments/config_invalid_value/translatable.toml b/translatable/tests/integration/environments/config_invalid_value/translatable.toml similarity index 100% rename from translatable/tests/ui/environments/config_invalid_value/translatable.toml rename to translatable/tests/integration/environments/config_invalid_value/translatable.toml diff --git a/translatable/tests/ui/environments/config_invalid_value/translations/test.toml b/translatable/tests/integration/environments/config_invalid_value/translations/test.toml similarity index 100% rename from translatable/tests/ui/environments/config_invalid_value/translations/test.toml rename to translatable/tests/integration/environments/config_invalid_value/translations/test.toml diff --git a/translatable/tests/ui/environments/config_path_missmatch/translatable.toml b/translatable/tests/integration/environments/config_path_missmatch/translatable.toml similarity index 100% rename from translatable/tests/ui/environments/config_path_missmatch/translatable.toml rename to translatable/tests/integration/environments/config_path_missmatch/translatable.toml diff --git a/translatable/tests/ui/environments/everything_valid/translations/test.toml b/translatable/tests/integration/environments/everything_valid/translations/test.toml similarity index 100% rename from translatable/tests/ui/environments/everything_valid/translations/test.toml rename to translatable/tests/integration/environments/everything_valid/translations/test.toml diff --git a/translatable/tests/ui/environments/translations_malformed/translations/test.toml b/translatable/tests/integration/environments/translations_malformed/translations/test.toml similarity index 100% rename from translatable/tests/ui/environments/translations_malformed/translations/test.toml rename to translatable/tests/integration/environments/translations_malformed/translations/test.toml diff --git a/translatable/tests/integration/language/fail_static_invalid.rs b/translatable/tests/integration/language/fail_static_invalid.rs new file mode 100644 index 0000000..4e82e70 --- /dev/null +++ b/translatable/tests/integration/language/fail_static_invalid.rs @@ -0,0 +1,5 @@ +use translatable::translation; + +fn fail_static_invalid() { + translation!("xx", static greetings::formal); +} diff --git a/translatable/tests/integration/language/mod.rs b/translatable/tests/integration/language/mod.rs new file mode 100644 index 0000000..5b1fbf8 --- /dev/null +++ b/translatable/tests/integration/language/mod.rs @@ -0,0 +1,7 @@ + +pub mod pass_dynamic_enum; +pub mod pass_dynamic_expr; +pub mod pass_dynamic_invalid_runtime; +pub mod pass_dynamic_str; +pub mod pass_static_lowercase; +pub mod pass_static_uppercase; diff --git a/translatable/tests/integration/language/pass_dynamic_enum.rs b/translatable/tests/integration/language/pass_dynamic_enum.rs new file mode 100644 index 0000000..375a2a9 --- /dev/null +++ b/translatable/tests/integration/language/pass_dynamic_enum.rs @@ -0,0 +1,8 @@ +use translatable::{translation, Language}; + +#[cfg(test)] +#[test] +fn pass_dynamic_enum() { + translation!(Language::ES, static greetings::informal); +} + diff --git a/translatable/tests/integration/language/pass_dynamic_expr.rs b/translatable/tests/integration/language/pass_dynamic_expr.rs new file mode 100644 index 0000000..872ccc6 --- /dev/null +++ b/translatable/tests/integration/language/pass_dynamic_expr.rs @@ -0,0 +1,5 @@ +use translatable::translation; + +fn main() { + translation!() +} diff --git a/translatable/tests/ui/language/pass_dynamic_invalid_runtime.rs b/translatable/tests/integration/language/pass_dynamic_invalid_runtime.rs similarity index 100% rename from translatable/tests/ui/language/pass_dynamic_invalid_runtime.rs rename to translatable/tests/integration/language/pass_dynamic_invalid_runtime.rs diff --git a/translatable/tests/ui/language/pass_dynamic_str.rs b/translatable/tests/integration/language/pass_dynamic_str.rs similarity index 100% rename from translatable/tests/ui/language/pass_dynamic_str.rs rename to translatable/tests/integration/language/pass_dynamic_str.rs diff --git a/translatable/tests/ui/language/pass_static_lowercase.rs b/translatable/tests/integration/language/pass_static_lowercase.rs similarity index 100% rename from translatable/tests/ui/language/pass_static_lowercase.rs rename to translatable/tests/integration/language/pass_static_lowercase.rs diff --git a/translatable/tests/ui/language/pass_static_uppercase.rs b/translatable/tests/integration/language/pass_static_uppercase.rs similarity index 100% rename from translatable/tests/ui/language/pass_static_uppercase.rs rename to translatable/tests/integration/language/pass_static_uppercase.rs diff --git a/translatable/tests/integration/mod.rs b/translatable/tests/integration/mod.rs new file mode 100644 index 0000000..17edfb7 --- /dev/null +++ b/translatable/tests/integration/mod.rs @@ -0,0 +1,4 @@ + +pub mod language; +pub mod path; +pub mod templates; diff --git a/translatable/tests/ui/path/fail_static_nonexistent.rs b/translatable/tests/integration/path/fail_static_nonexistent.rs similarity index 100% rename from translatable/tests/ui/path/fail_static_nonexistent.rs rename to translatable/tests/integration/path/fail_static_nonexistent.rs diff --git a/translatable/tests/integration/path/mod.rs b/translatable/tests/integration/path/mod.rs new file mode 100644 index 0000000..b531763 --- /dev/null +++ b/translatable/tests/integration/path/mod.rs @@ -0,0 +1,6 @@ + +pub mod pass_dynamic_expr; +pub mod pass_dynamic_invalid_runtime; +pub mod pass_dynamic_var; +pub mod pass_dynamic_vec; +pub mod pass_static_existing; diff --git a/translatable/tests/ui/language/pass_dynamic_expr.rs b/translatable/tests/integration/path/pass_dynamic_expr.rs similarity index 100% rename from translatable/tests/ui/language/pass_dynamic_expr.rs rename to translatable/tests/integration/path/pass_dynamic_expr.rs diff --git a/translatable/tests/ui/path/pass_dynamic_invalid_runtime.rs b/translatable/tests/integration/path/pass_dynamic_invalid_runtime.rs similarity index 100% rename from translatable/tests/ui/path/pass_dynamic_invalid_runtime.rs rename to translatable/tests/integration/path/pass_dynamic_invalid_runtime.rs diff --git a/translatable/tests/ui/path/pass_dynamic_var.rs b/translatable/tests/integration/path/pass_dynamic_var.rs similarity index 100% rename from translatable/tests/ui/path/pass_dynamic_var.rs rename to translatable/tests/integration/path/pass_dynamic_var.rs diff --git a/translatable/tests/ui/path/pass_dynamic_vec.rs b/translatable/tests/integration/path/pass_dynamic_vec.rs similarity index 100% rename from translatable/tests/ui/path/pass_dynamic_vec.rs rename to translatable/tests/integration/path/pass_dynamic_vec.rs diff --git a/translatable/tests/ui/path/pass_static_existing.rs b/translatable/tests/integration/path/pass_static_existing.rs similarity index 100% rename from translatable/tests/ui/path/pass_static_existing.rs rename to translatable/tests/integration/path/pass_static_existing.rs diff --git a/translatable/tests/ui/templates/fail_invalid_ident.rs b/translatable/tests/integration/templates/fail_invalid_ident.rs similarity index 100% rename from translatable/tests/ui/templates/fail_invalid_ident.rs rename to translatable/tests/integration/templates/fail_invalid_ident.rs diff --git a/translatable/tests/ui/templates/fail_not_display.rs b/translatable/tests/integration/templates/fail_not_display.rs similarity index 100% rename from translatable/tests/ui/templates/fail_not_display.rs rename to translatable/tests/integration/templates/fail_not_display.rs diff --git a/translatable/tests/integration/templates/mod.rs b/translatable/tests/integration/templates/mod.rs new file mode 100644 index 0000000..a96f2ac --- /dev/null +++ b/translatable/tests/integration/templates/mod.rs @@ -0,0 +1,5 @@ + +pub mod pass_display; +pub mod pass_ident_ref; +pub mod pass_trailing_comma; +pub mod pass_trailing_comma_no_args; diff --git a/translatable/tests/ui/templates/pass_display.rs b/translatable/tests/integration/templates/pass_display.rs similarity index 100% rename from translatable/tests/ui/templates/pass_display.rs rename to translatable/tests/integration/templates/pass_display.rs diff --git a/translatable/tests/ui/templates/pass_ident_ref.rs b/translatable/tests/integration/templates/pass_ident_ref.rs similarity index 100% rename from translatable/tests/ui/templates/pass_ident_ref.rs rename to translatable/tests/integration/templates/pass_ident_ref.rs diff --git a/translatable/tests/ui/templates/pass_trailing_comma.rs b/translatable/tests/integration/templates/pass_trailing_comma.rs similarity index 100% rename from translatable/tests/ui/templates/pass_trailing_comma.rs rename to translatable/tests/integration/templates/pass_trailing_comma.rs diff --git a/translatable/tests/ui/templates/pass_trailing_comma_no_args.rs b/translatable/tests/integration/templates/pass_trailing_comma_no_args.rs similarity index 100% rename from translatable/tests/ui/templates/pass_trailing_comma_no_args.rs rename to translatable/tests/integration/templates/pass_trailing_comma_no_args.rs diff --git a/translatable/tests/ui.rs b/translatable/tests/integration_tests.rs similarity index 82% rename from translatable/tests/ui.rs rename to translatable/tests/integration_tests.rs index 3b2e4e2..8e2b70d 100644 --- a/translatable/tests/ui.rs +++ b/translatable/tests/integration_tests.rs @@ -1,14 +1,23 @@ use std::env::set_current_dir; - use trybuild::TestCases; +// so dynamic tests also run. +#[allow(unused_imports)] +use integration::language::*; +#[allow(unused_imports)] +use integration::path::*; +#[allow(unused_imports)] +use integration::templates::*; + +mod integration; + fn set_test_environment(environment: &str) { set_current_dir(format!("tests/ui/environments/{environment}")) .expect("Should be able to change environment."); } #[test] -fn ui_tests() { +fn compile_tests() { let t = TestCases::new(); // general test cases. diff --git a/translatable/tests/ui/config/fail_config_invalid_enums.rs b/translatable/tests/ui/config/fail_config_invalid_enums.rs deleted file mode 100644 index e69de29..0000000 diff --git a/translatable/tests/ui/config/fail_config_path_missmatch.rs b/translatable/tests/ui/config/fail_config_path_missmatch.rs deleted file mode 100644 index e69de29..0000000 diff --git a/translatable/tests/ui/config/fail_translations_malformed.rs b/translatable/tests/ui/config/fail_translations_malformed.rs deleted file mode 100644 index e69de29..0000000 diff --git a/translatable/tests/ui/language/fail_static_invalid.rs b/translatable/tests/ui/language/fail_static_invalid.rs deleted file mode 100644 index e69de29..0000000 diff --git a/translatable/tests/ui/language/pass_dynamic_enum.rs b/translatable/tests/ui/language/pass_dynamic_enum.rs deleted file mode 100644 index e69de29..0000000 diff --git a/translatable/tests/ui/path/pass_dynamic_expr.rs b/translatable/tests/ui/path/pass_dynamic_expr.rs deleted file mode 100644 index e69de29..0000000 From fd5a8da23fb07e6be2c8a80d4b34ef777c0ae44c Mon Sep 17 00:00:00 2001 From: stifskere Date: Fri, 25 Apr 2025 02:47:27 +0200 Subject: [PATCH 140/228] chore(tests): complete language tests not tried out --- translatable.toml | 2 ++ .../config_invalid_value/translatable.toml | 0 .../config_invalid_value/translations/test.toml | 0 .../config_path_missmatch/translatable.toml | 0 .../everything_valid/translations/test.toml | 0 .../translations_malformed/translations/test.toml | 0 translatable/tests/integration/language/mod.rs | 1 - .../tests/integration/language/pass_dynamic_enum.rs | 7 +++++-- .../tests/integration/language/pass_dynamic_expr.rs | 12 ++++++++++-- .../language/pass_dynamic_invalid_runtime.rs | 11 +++++++++++ .../tests/integration/language/pass_dynamic_str.rs | 0 .../integration/language/pass_static_lowercase.rs | 9 +++++++++ .../integration/language/pass_static_uppercase.rs | 9 +++++++++ translatable/tests/integration_tests.rs | 2 +- 14 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 translatable.toml rename translatable/tests/{integration => }/environments/config_invalid_value/translatable.toml (100%) rename translatable/tests/{integration => }/environments/config_invalid_value/translations/test.toml (100%) rename translatable/tests/{integration => }/environments/config_path_missmatch/translatable.toml (100%) rename translatable/tests/{integration => }/environments/everything_valid/translations/test.toml (100%) rename translatable/tests/{integration => }/environments/translations_malformed/translations/test.toml (100%) delete mode 100644 translatable/tests/integration/language/pass_dynamic_str.rs diff --git a/translatable.toml b/translatable.toml new file mode 100644 index 0000000..a95aec2 --- /dev/null +++ b/translatable.toml @@ -0,0 +1,2 @@ + +path = "./translatable/tests/environments/everything_valid/translations/" diff --git a/translatable/tests/integration/environments/config_invalid_value/translatable.toml b/translatable/tests/environments/config_invalid_value/translatable.toml similarity index 100% rename from translatable/tests/integration/environments/config_invalid_value/translatable.toml rename to translatable/tests/environments/config_invalid_value/translatable.toml diff --git a/translatable/tests/integration/environments/config_invalid_value/translations/test.toml b/translatable/tests/environments/config_invalid_value/translations/test.toml similarity index 100% rename from translatable/tests/integration/environments/config_invalid_value/translations/test.toml rename to translatable/tests/environments/config_invalid_value/translations/test.toml diff --git a/translatable/tests/integration/environments/config_path_missmatch/translatable.toml b/translatable/tests/environments/config_path_missmatch/translatable.toml similarity index 100% rename from translatable/tests/integration/environments/config_path_missmatch/translatable.toml rename to translatable/tests/environments/config_path_missmatch/translatable.toml diff --git a/translatable/tests/integration/environments/everything_valid/translations/test.toml b/translatable/tests/environments/everything_valid/translations/test.toml similarity index 100% rename from translatable/tests/integration/environments/everything_valid/translations/test.toml rename to translatable/tests/environments/everything_valid/translations/test.toml diff --git a/translatable/tests/integration/environments/translations_malformed/translations/test.toml b/translatable/tests/environments/translations_malformed/translations/test.toml similarity index 100% rename from translatable/tests/integration/environments/translations_malformed/translations/test.toml rename to translatable/tests/environments/translations_malformed/translations/test.toml diff --git a/translatable/tests/integration/language/mod.rs b/translatable/tests/integration/language/mod.rs index 5b1fbf8..d23a2fa 100644 --- a/translatable/tests/integration/language/mod.rs +++ b/translatable/tests/integration/language/mod.rs @@ -2,6 +2,5 @@ pub mod pass_dynamic_enum; pub mod pass_dynamic_expr; pub mod pass_dynamic_invalid_runtime; -pub mod pass_dynamic_str; pub mod pass_static_lowercase; pub mod pass_static_uppercase; diff --git a/translatable/tests/integration/language/pass_dynamic_enum.rs b/translatable/tests/integration/language/pass_dynamic_enum.rs index 375a2a9..b53bc51 100644 --- a/translatable/tests/integration/language/pass_dynamic_enum.rs +++ b/translatable/tests/integration/language/pass_dynamic_enum.rs @@ -2,7 +2,10 @@ use translatable::{translation, Language}; #[cfg(test)] #[test] -fn pass_dynamic_enum() { - translation!(Language::ES, static greetings::informal); +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."); } diff --git a/translatable/tests/integration/language/pass_dynamic_expr.rs b/translatable/tests/integration/language/pass_dynamic_expr.rs index 872ccc6..63ffb50 100644 --- a/translatable/tests/integration/language/pass_dynamic_expr.rs +++ b/translatable/tests/integration/language/pass_dynamic_expr.rs @@ -1,5 +1,13 @@ use translatable::translation; -fn main() { - 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."); } diff --git a/translatable/tests/integration/language/pass_dynamic_invalid_runtime.rs b/translatable/tests/integration/language/pass_dynamic_invalid_runtime.rs index e69de29..929f3ed 100644 --- a/translatable/tests/integration/language/pass_dynamic_invalid_runtime.rs +++ b/translatable/tests/integration/language/pass_dynamic_invalid_runtime.rs @@ -0,0 +1,11 @@ +use translatable::{translation, Language}; + +#[cfg(test)] +#[test] +pub fn pass_dynamic_invalid_runtime() { + let language = "invalid".parse::(); + + assert!(language.is_err()); + + translation!(language.unwrap(), static greetings::formal).ok(); +} diff --git a/translatable/tests/integration/language/pass_dynamic_str.rs b/translatable/tests/integration/language/pass_dynamic_str.rs deleted file mode 100644 index e69de29..0000000 diff --git a/translatable/tests/integration/language/pass_static_lowercase.rs b/translatable/tests/integration/language/pass_static_lowercase.rs index e69de29..f8443fb 100644 --- a/translatable/tests/integration/language/pass_static_lowercase.rs +++ b/translatable/tests/integration/language/pass_static_lowercase.rs @@ -0,0 +1,9 @@ +use translatable::translation; + +#[cfg(test)] +#[test] +fn pass_static_lowercase() { + let translation = translation!("es", static greetings::formal); + + assert_eq!(translation, "Bueno conocerte."); +} diff --git a/translatable/tests/integration/language/pass_static_uppercase.rs b/translatable/tests/integration/language/pass_static_uppercase.rs index e69de29..c3c3ce7 100644 --- a/translatable/tests/integration/language/pass_static_uppercase.rs +++ b/translatable/tests/integration/language/pass_static_uppercase.rs @@ -0,0 +1,9 @@ +use translatable::translation; + +#[cfg(test)] +#[test] +fn pass_static_uppercase() { + let translation = translation!("ES", static greetings::formal); + + assert_eq!(translation, "Bueno conocerte."); +} diff --git a/translatable/tests/integration_tests.rs b/translatable/tests/integration_tests.rs index 8e2b70d..9cc75ae 100644 --- a/translatable/tests/integration_tests.rs +++ b/translatable/tests/integration_tests.rs @@ -12,7 +12,7 @@ use integration::templates::*; mod integration; fn set_test_environment(environment: &str) { - set_current_dir(format!("tests/ui/environments/{environment}")) + set_current_dir(format!("tests/environments/{environment}")) .expect("Should be able to change environment."); } From e5f5b0a027aa8de7ac268c3bb6edae1206a4ef1d Mon Sep 17 00:00:00 2001 From: stifskere Date: Fri, 25 Apr 2025 05:42:56 +0200 Subject: [PATCH 141/228] chore(tests): complete path tests not tried out --- .../integration/language/pass_static_lowercase.rs | 2 +- .../integration/language/pass_static_uppercase.rs | 2 +- .../tests/integration/path/fail_static_nonexistent.rs | 5 +++++ translatable/tests/integration/path/mod.rs | 3 --- .../tests/integration/path/pass_dynamic_expr.rs | 10 ++++++++++ .../integration/path/pass_dynamic_invalid_runtime.rs | 0 .../tests/integration/path/pass_dynamic_var.rs | 0 .../tests/integration/path/pass_dynamic_vec.rs | 0 .../tests/integration/path/pass_static_existing.rs | 7 +++++++ 9 files changed, 24 insertions(+), 5 deletions(-) delete mode 100644 translatable/tests/integration/path/pass_dynamic_invalid_runtime.rs delete mode 100644 translatable/tests/integration/path/pass_dynamic_var.rs delete mode 100644 translatable/tests/integration/path/pass_dynamic_vec.rs diff --git a/translatable/tests/integration/language/pass_static_lowercase.rs b/translatable/tests/integration/language/pass_static_lowercase.rs index f8443fb..6d2fefe 100644 --- a/translatable/tests/integration/language/pass_static_lowercase.rs +++ b/translatable/tests/integration/language/pass_static_lowercase.rs @@ -2,7 +2,7 @@ use translatable::translation; #[cfg(test)] #[test] -fn pass_static_lowercase() { +pub fn pass_static_lowercase() { let translation = translation!("es", static greetings::formal); assert_eq!(translation, "Bueno conocerte."); diff --git a/translatable/tests/integration/language/pass_static_uppercase.rs b/translatable/tests/integration/language/pass_static_uppercase.rs index c3c3ce7..3a79e08 100644 --- a/translatable/tests/integration/language/pass_static_uppercase.rs +++ b/translatable/tests/integration/language/pass_static_uppercase.rs @@ -2,7 +2,7 @@ use translatable::translation; #[cfg(test)] #[test] -fn pass_static_uppercase() { +pub fn pass_static_uppercase() { let translation = translation!("ES", static greetings::formal); assert_eq!(translation, "Bueno conocerte."); diff --git a/translatable/tests/integration/path/fail_static_nonexistent.rs b/translatable/tests/integration/path/fail_static_nonexistent.rs index e69de29..82d0e37 100644 --- a/translatable/tests/integration/path/fail_static_nonexistent.rs +++ b/translatable/tests/integration/path/fail_static_nonexistent.rs @@ -0,0 +1,5 @@ +use translatable::translation; + +fn fail_static_nonexistent() { + translation!("es", static non::existing::path); +} diff --git a/translatable/tests/integration/path/mod.rs b/translatable/tests/integration/path/mod.rs index b531763..0652130 100644 --- a/translatable/tests/integration/path/mod.rs +++ b/translatable/tests/integration/path/mod.rs @@ -1,6 +1,3 @@ pub mod pass_dynamic_expr; -pub mod pass_dynamic_invalid_runtime; -pub mod pass_dynamic_var; -pub mod pass_dynamic_vec; pub mod pass_static_existing; diff --git a/translatable/tests/integration/path/pass_dynamic_expr.rs b/translatable/tests/integration/path/pass_dynamic_expr.rs index e69de29..deef957 100644 --- a/translatable/tests/integration/path/pass_dynamic_expr.rs +++ b/translatable/tests/integration/path/pass_dynamic_expr.rs @@ -0,0 +1,10 @@ +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."); +} diff --git a/translatable/tests/integration/path/pass_dynamic_invalid_runtime.rs b/translatable/tests/integration/path/pass_dynamic_invalid_runtime.rs deleted file mode 100644 index e69de29..0000000 diff --git a/translatable/tests/integration/path/pass_dynamic_var.rs b/translatable/tests/integration/path/pass_dynamic_var.rs deleted file mode 100644 index e69de29..0000000 diff --git a/translatable/tests/integration/path/pass_dynamic_vec.rs b/translatable/tests/integration/path/pass_dynamic_vec.rs deleted file mode 100644 index e69de29..0000000 diff --git a/translatable/tests/integration/path/pass_static_existing.rs b/translatable/tests/integration/path/pass_static_existing.rs index e69de29..11d619e 100644 --- a/translatable/tests/integration/path/pass_static_existing.rs +++ b/translatable/tests/integration/path/pass_static_existing.rs @@ -0,0 +1,7 @@ +use translatable::translation; + +pub fn pass_static_existing() { + let translation = translation!("es", static greetings::formal); + + assert_eq!(translation, "Bueno conocerte."); +} From ca0f522c58fccb8e528d246498028b682f26e463 Mon Sep 17 00:00:00 2001 From: stifskere Date: Fri, 25 Apr 2025 05:50:25 +0200 Subject: [PATCH 142/228] chore(tests): add test annotation to pass_static_existing --- translatable/tests/integration/path/pass_static_existing.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/translatable/tests/integration/path/pass_static_existing.rs b/translatable/tests/integration/path/pass_static_existing.rs index 11d619e..62a7635 100644 --- a/translatable/tests/integration/path/pass_static_existing.rs +++ b/translatable/tests/integration/path/pass_static_existing.rs @@ -1,5 +1,7 @@ use translatable::translation; +#[cfg(test)] +#[test] pub fn pass_static_existing() { let translation = translation!("es", static greetings::formal); From fae0872a500bd69f191b4c2d5047552bbcc24cda Mon Sep 17 00:00:00 2001 From: stifskere Date: Fri, 25 Apr 2025 09:34:18 +0200 Subject: [PATCH 143/228] chore(tests): sync progress, need to load tests from environment variables instead --- .../language/fail_static_invalid.rs | 3 +- .../language/fail_static_invalid.stderr | 5 +++ .../integration/language/pass_dynamic_enum.rs | 3 ++ .../integration/language/pass_dynamic_expr.rs | 4 +++ .../language/pass_dynamic_invalid_runtime.rs | 6 ++-- .../language/pass_static_lowercase.rs | 4 +++ .../language/pass_static_uppercase.rs | 4 +++ .../path/fail_static_nonexistent.rs | 3 +- .../integration/path/pass_dynamic_expr.rs | 4 +++ .../integration/path/pass_static_existing.rs | 4 +++ translatable/tests/integration_tests.rs | 32 +++++++------------ 11 files changed, 48 insertions(+), 24 deletions(-) create mode 100644 translatable/tests/integration/language/fail_static_invalid.stderr diff --git a/translatable/tests/integration/language/fail_static_invalid.rs b/translatable/tests/integration/language/fail_static_invalid.rs index 4e82e70..4bd4c76 100644 --- a/translatable/tests/integration/language/fail_static_invalid.rs +++ b/translatable/tests/integration/language/fail_static_invalid.rs @@ -1,5 +1,6 @@ +#[allow(unused_imports)] use translatable::translation; -fn fail_static_invalid() { +fn main() { translation!("xx", static greetings::formal); } diff --git a/translatable/tests/integration/language/fail_static_invalid.stderr b/translatable/tests/integration/language/fail_static_invalid.stderr new file mode 100644 index 0000000..78bc53b --- /dev/null +++ b/translatable/tests/integration/language/fail_static_invalid.stderr @@ -0,0 +1,5 @@ +error: The literal 'xx' is an invalid ISO 639-1 string, and cannot be parsed + --> tests/integration/language/fail_static_invalid.rs:5:18 + | +5 | translation!("xx", static greetings::formal); + | ^^^^ diff --git a/translatable/tests/integration/language/pass_dynamic_enum.rs b/translatable/tests/integration/language/pass_dynamic_enum.rs index b53bc51..db41c06 100644 --- a/translatable/tests/integration/language/pass_dynamic_enum.rs +++ b/translatable/tests/integration/language/pass_dynamic_enum.rs @@ -1,3 +1,4 @@ +#[allow(unused_imports)] // trybuild use translatable::{translation, Language}; #[cfg(test)] @@ -9,3 +10,5 @@ pub fn pass_dynamic_enum() { assert_eq!(translation, "Bueno conocerte."); } +#[allow(dead_code)] +fn main() {} // trybuild diff --git a/translatable/tests/integration/language/pass_dynamic_expr.rs b/translatable/tests/integration/language/pass_dynamic_expr.rs index 63ffb50..bfea01b 100644 --- a/translatable/tests/integration/language/pass_dynamic_expr.rs +++ b/translatable/tests/integration/language/pass_dynamic_expr.rs @@ -1,3 +1,4 @@ +#[allow(unused_imports)] // trybuild use translatable::translation; #[cfg(test)] @@ -11,3 +12,6 @@ pub fn pass_dynamic_expr() { assert_eq!(translation, "Bueno conocerte."); } + +#[allow(dead_code)] +fn main() {} // trybuild diff --git a/translatable/tests/integration/language/pass_dynamic_invalid_runtime.rs b/translatable/tests/integration/language/pass_dynamic_invalid_runtime.rs index 929f3ed..b854b3d 100644 --- a/translatable/tests/integration/language/pass_dynamic_invalid_runtime.rs +++ b/translatable/tests/integration/language/pass_dynamic_invalid_runtime.rs @@ -1,3 +1,4 @@ +#[allow(unused_imports)] // trybuild use translatable::{translation, Language}; #[cfg(test)] @@ -6,6 +7,7 @@ pub fn pass_dynamic_invalid_runtime() { let language = "invalid".parse::(); assert!(language.is_err()); - - translation!(language.unwrap(), static greetings::formal).ok(); } + +#[allow(dead_code)] +fn main() {} // trybuild diff --git a/translatable/tests/integration/language/pass_static_lowercase.rs b/translatable/tests/integration/language/pass_static_lowercase.rs index 6d2fefe..c18a052 100644 --- a/translatable/tests/integration/language/pass_static_lowercase.rs +++ b/translatable/tests/integration/language/pass_static_lowercase.rs @@ -1,3 +1,4 @@ +#[allow(unused_imports)] // trybuild use translatable::translation; #[cfg(test)] @@ -7,3 +8,6 @@ pub fn pass_static_lowercase() { assert_eq!(translation, "Bueno conocerte."); } + +#[allow(dead_code)] +fn main() {} // trybuild diff --git a/translatable/tests/integration/language/pass_static_uppercase.rs b/translatable/tests/integration/language/pass_static_uppercase.rs index 3a79e08..710e048 100644 --- a/translatable/tests/integration/language/pass_static_uppercase.rs +++ b/translatable/tests/integration/language/pass_static_uppercase.rs @@ -1,3 +1,4 @@ +#[allow(unused_imports)] // trybuild use translatable::translation; #[cfg(test)] @@ -7,3 +8,6 @@ pub fn pass_static_uppercase() { assert_eq!(translation, "Bueno conocerte."); } + +#[allow(dead_code)] +fn main() {} // trybuild diff --git a/translatable/tests/integration/path/fail_static_nonexistent.rs b/translatable/tests/integration/path/fail_static_nonexistent.rs index 82d0e37..51a6865 100644 --- a/translatable/tests/integration/path/fail_static_nonexistent.rs +++ b/translatable/tests/integration/path/fail_static_nonexistent.rs @@ -1,5 +1,6 @@ +#[allow(unused_imports)] use translatable::translation; -fn fail_static_nonexistent() { +fn main() { translation!("es", static non::existing::path); } diff --git a/translatable/tests/integration/path/pass_dynamic_expr.rs b/translatable/tests/integration/path/pass_dynamic_expr.rs index deef957..4df4b7e 100644 --- a/translatable/tests/integration/path/pass_dynamic_expr.rs +++ b/translatable/tests/integration/path/pass_dynamic_expr.rs @@ -1,3 +1,4 @@ +#[allow(unused_imports)] // trybuild use translatable::translation; #[cfg(test)] @@ -8,3 +9,6 @@ pub fn pass_dynamic_expr() { assert_eq!(translation, "Bueno conocerte."); } + +#[allow(dead_code)] +fn main() {} // trybuild diff --git a/translatable/tests/integration/path/pass_static_existing.rs b/translatable/tests/integration/path/pass_static_existing.rs index 62a7635..d37c4ab 100644 --- a/translatable/tests/integration/path/pass_static_existing.rs +++ b/translatable/tests/integration/path/pass_static_existing.rs @@ -1,3 +1,4 @@ +#[allow(unused_imports)] // trybuild use translatable::translation; #[cfg(test)] @@ -7,3 +8,6 @@ pub fn pass_static_existing() { 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 index 9cc75ae..e570a37 100644 --- a/translatable/tests/integration_tests.rs +++ b/translatable/tests/integration_tests.rs @@ -1,4 +1,7 @@ +use std::env::current_dir; use std::env::set_current_dir; +use std::env::set_var; +use std::sync::OnceLock; use trybuild::TestCases; // so dynamic tests also run. @@ -11,36 +14,25 @@ use integration::templates::*; mod integration; -fn set_test_environment(environment: &str) { - set_current_dir(format!("tests/environments/{environment}")) - .expect("Should be able to change environment."); -} - #[test] fn compile_tests() { let t = TestCases::new(); - // general test cases. - set_test_environment("everything_valid"); - - t.pass("./ui/language/pass*.rs"); - t.compile_fail("./ui/language/fail*.rs"); + t.pass("./tests/integration/language/pass*.rs"); + t.compile_fail("./tests/integration/language/fail*.rs"); - t.pass("./ui/path/pass*.rs"); - t.compile_fail("./ui/path/fail*.rs"); + t.pass("./tests/integration/path/pass*.rs"); + t.compile_fail("./tests/integration/path/fail*.rs"); - t.pass("./ui/templates/pass*.rs"); - t.compile_fail("./ui/templates/fail*.rs"); + t.pass("./tests/integration/templates/pass*.rs"); + t.compile_fail("./tests/integration/templates/fail*.rs"); // invalid path in configuration. - set_test_environment("config_path_missmatch"); - t.compile_fail("./ui/config/fail_config_path_missmatch.rs"); +// t.compile_fail("../../integration/config/fail_config_path_missmatch.rs"); // invalid enum value in configuration. - set_test_environment("config_invalid_value"); - t.compile_fail("./ui/config/fail_config_invalid_enums.rs"); +// t.compile_fail("../../integration/config/fail_config_invalid_enums.rs"); // translation file rule broken. - set_test_environment("translations_malformed"); - t.compile_fail("./ui/config/fail_translations_malformed.rs"); +// t.compile_fail("../../integration/config/fail_translations_malformed.rs"); } From c82e0d203070459ace03a6305638d30b5314f480 Mon Sep 17 00:00:00 2001 From: stifskere Date: Sat, 26 Apr 2025 03:09:35 +0200 Subject: [PATCH 144/228] chore(tests): main tests passed --- Makefile | 2 -- .../path/fail_static_nonexistent.stderr | 7 ++++++ .../templates/fail_invalid_ident.rs | 6 +++++ .../templates/fail_invalid_ident.stderr | 5 +++++ .../integration/templates/fail_not_display.rs | 8 +++++++ .../templates/fail_not_display.stderr | 22 +++++++++++++++++++ .../templates/fail_value_not_found.rs | 8 +++++++ .../templates/fail_value_not_found.stderr | 5 +++++ .../tests/integration/templates/mod.rs | 2 +- .../integration/templates/pass_display.rs | 0 .../integration/templates/pass_ident_ref.rs | 15 +++++++++++++ .../templates/pass_multiple_templates.rs | 16 ++++++++++++++ .../templates/pass_trailing_comma.rs | 16 ++++++++++++++ .../templates/pass_trailing_comma_no_args.rs | 13 +++++++++++ translatable/tests/integration_tests.rs | 21 +++++++++++++++--- .../src/macro_input/translation.rs | 5 +++++ 16 files changed, 145 insertions(+), 6 deletions(-) create mode 100644 translatable/tests/integration/path/fail_static_nonexistent.stderr create mode 100644 translatable/tests/integration/templates/fail_invalid_ident.stderr create mode 100644 translatable/tests/integration/templates/fail_not_display.stderr create mode 100644 translatable/tests/integration/templates/fail_value_not_found.rs create mode 100644 translatable/tests/integration/templates/fail_value_not_found.stderr delete mode 100644 translatable/tests/integration/templates/pass_display.rs create mode 100644 translatable/tests/integration/templates/pass_multiple_templates.rs diff --git a/Makefile b/Makefile index 1dd1733..88f542d 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,2 @@ -export TRANSLATABLE_LOCALES_PATH=${PWD}/translatable/tests/fixtures/translations - test: cargo test -p translatable -- --nocapture --color=always --test-threads=1 diff --git a/translatable/tests/integration/path/fail_static_nonexistent.stderr b/translatable/tests/integration/path/fail_static_nonexistent.stderr new file mode 100644 index 0000000..946b269 --- /dev/null +++ b/translatable/tests/integration/path/fail_static_nonexistent.stderr @@ -0,0 +1,7 @@ +error: The path 'non::existing::path' could not be found + --> tests/integration/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/templates/fail_invalid_ident.rs b/translatable/tests/integration/templates/fail_invalid_ident.rs index e69de29..9542925 100644 --- a/translatable/tests/integration/templates/fail_invalid_ident.rs +++ b/translatable/tests/integration/templates/fail_invalid_ident.rs @@ -0,0 +1,6 @@ +#[allow(unused_imports)] +use translatable::translation; + +fn main() { + translation!("es", static greetings::informal, %%$invalid = $ident); +} diff --git a/translatable/tests/integration/templates/fail_invalid_ident.stderr b/translatable/tests/integration/templates/fail_invalid_ident.stderr new file mode 100644 index 0000000..9fab06b --- /dev/null +++ b/translatable/tests/integration/templates/fail_invalid_ident.stderr @@ -0,0 +1,5 @@ +error: expected identifier + --> tests/integration/templates/fail_invalid_ident.rs:5:52 + | +5 | translation!("es", static greetings::informal, %%$invalid = $ident); + | ^ diff --git a/translatable/tests/integration/templates/fail_not_display.rs b/translatable/tests/integration/templates/fail_not_display.rs index e69de29..d089999 100644 --- a/translatable/tests/integration/templates/fail_not_display.rs +++ b/translatable/tests/integration/templates/fail_not_display.rs @@ -0,0 +1,8 @@ +#[allow(unused_imports)] +use translatable::translation; + +struct NotDisplay; + +fn main() { + translation!("es", static greetings::informal, user = NotDisplay); +} diff --git a/translatable/tests/integration/templates/fail_not_display.stderr b/translatable/tests/integration/templates/fail_not_display.stderr new file mode 100644 index 0000000..22d079f --- /dev/null +++ b/translatable/tests/integration/templates/fail_not_display.stderr @@ -0,0 +1,22 @@ +error[E0599]: `NotDisplay` doesn't implement `std::fmt::Display` + --> tests/integration/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/templates/fail_value_not_found.rs b/translatable/tests/integration/templates/fail_value_not_found.rs new file mode 100644 index 0000000..2f665db --- /dev/null +++ b/translatable/tests/integration/templates/fail_value_not_found.rs @@ -0,0 +1,8 @@ +#[allow(unused_imports)] +use translatable::translation; + +struct NotDisplay; + +fn main() { + translation!("es", static greetings::informal, user); +} diff --git a/translatable/tests/integration/templates/fail_value_not_found.stderr b/translatable/tests/integration/templates/fail_value_not_found.stderr new file mode 100644 index 0000000..9ad6009 --- /dev/null +++ b/translatable/tests/integration/templates/fail_value_not_found.stderr @@ -0,0 +1,5 @@ +error[E0425]: cannot find value `user` in this scope + --> tests/integration/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/templates/mod.rs b/translatable/tests/integration/templates/mod.rs index a96f2ac..1993478 100644 --- a/translatable/tests/integration/templates/mod.rs +++ b/translatable/tests/integration/templates/mod.rs @@ -1,5 +1,5 @@ -pub mod pass_display; 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/templates/pass_display.rs b/translatable/tests/integration/templates/pass_display.rs deleted file mode 100644 index e69de29..0000000 diff --git a/translatable/tests/integration/templates/pass_ident_ref.rs b/translatable/tests/integration/templates/pass_ident_ref.rs index e69de29..34fdf84 100644 --- a/translatable/tests/integration/templates/pass_ident_ref.rs +++ b/translatable/tests/integration/templates/pass_ident_ref.rs @@ -0,0 +1,15 @@ +#[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/templates/pass_multiple_templates.rs b/translatable/tests/integration/templates/pass_multiple_templates.rs new file mode 100644 index 0000000..c59b18f --- /dev/null +++ b/translatable/tests/integration/templates/pass_multiple_templates.rs @@ -0,0 +1,16 @@ +#[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/templates/pass_trailing_comma.rs b/translatable/tests/integration/templates/pass_trailing_comma.rs index e69de29..c153c0f 100644 --- a/translatable/tests/integration/templates/pass_trailing_comma.rs +++ b/translatable/tests/integration/templates/pass_trailing_comma.rs @@ -0,0 +1,16 @@ +#[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/templates/pass_trailing_comma_no_args.rs b/translatable/tests/integration/templates/pass_trailing_comma_no_args.rs index e69de29..849de90 100644 --- a/translatable/tests/integration/templates/pass_trailing_comma_no_args.rs +++ b/translatable/tests/integration/templates/pass_trailing_comma_no_args.rs @@ -0,0 +1,13 @@ +#[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 index e570a37..7176e49 100644 --- a/translatable/tests/integration_tests.rs +++ b/translatable/tests/integration_tests.rs @@ -1,7 +1,6 @@ -use std::env::current_dir; -use std::env::set_current_dir; use std::env::set_var; -use std::sync::OnceLock; +use std::env::var; +use std::fs::canonicalize; use trybuild::TestCases; // so dynamic tests also run. @@ -14,10 +13,26 @@ use integration::templates::*; mod integration; +const PATH_ENV: &str = "TRANSLATABLE_LOCALES_PATH"; +const OVERLAP_ENV: &str = "TRANSLATABLE_OVERLAP"; +const SEEK_MODE_ENV: &str = "TRANSLATABLE_SEEK_MODE"; + +fn set_locales_env(env: &str) { + unsafe { + set_var( + PATH_ENV, + canonicalize(format!("./tests/environments/{env}/translations/")) + .unwrap() + ); + } +} + #[test] fn compile_tests() { let t = TestCases::new(); + set_locales_env("everything_valid"); + t.pass("./tests/integration/language/pass*.rs"); t.compile_fail("./tests/integration/language/fail*.rs"); diff --git a/translatable_proc/src/macro_input/translation.rs b/translatable_proc/src/macro_input/translation.rs index bd072c0..70b333f 100644 --- a/translatable_proc/src/macro_input/translation.rs +++ b/translatable_proc/src/macro_input/translation.rs @@ -127,6 +127,11 @@ impl Parse for TranslationMacroArgs { 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 From 666bf47f1c48ec5743e31d47bcd580ab8697034e Mon Sep 17 00:00:00 2001 From: stifskere Date: Sat, 26 Apr 2025 03:13:17 +0200 Subject: [PATCH 145/228] docs(tests): running tests documentation --- translatable/tests/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/translatable/tests/README.md b/translatable/tests/README.md index 999d72e..81554ee 100644 --- a/translatable/tests/README.md +++ b/translatable/tests/README.md @@ -26,3 +26,11 @@ modules and annotated conditionally with `#[cfg(test)] #[test]`. 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. From af9d829d7350e3116e9df1a91e631c0ac2c3132f Mon Sep 17 00:00:00 2001 From: stifskere Date: Sat, 26 Apr 2025 03:21:34 +0200 Subject: [PATCH 146/228] docs(tests): add pull request documentation on contributing --- CONTRIBUTING.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e8d1026..7331b59 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,6 +26,19 @@ you can use our [Discord channel](https://discord.gg/AJWFyps23a) at Flaky. We recommend using the issue templates provided in this repository. +## Making Pull Requests + +Before adding a feature on your behalf, we'd rather for it to be evaluated +in a issue before, we appreciate the time and effort our contributors have +and we don't want to waste it, so we'd rather talk about your feature before +you working on it. + +When submitting a pull request make sure the code you added is tested and +documented, if it isn't you will be asked to document/test it before merging. + +To add tests please refer to the [testing documentation] on the tests folder +in the `translatable` crate. + ## Running Tests and Compiling the Project This project uses GNU [make](https://www.gnu.org/software/make/). @@ -39,3 +52,5 @@ The Translatable community follows the [Rust Code of Conduct](https://www.rust-l For moderation issues or escalation, please contact Esteve or Luis at [moderation@flaky.es](mailto:moderation@flaky.es) rather than the Rust moderation team. + +[testing documentation]: ./translatable/tests/README.md From 79a0c99375fdd1bca0b49c4bc86a4b5d7d6737f6 Mon Sep 17 00:00:00 2001 From: stifskere Date: Sat, 26 Apr 2025 04:33:44 +0200 Subject: [PATCH 147/228] chore(tests): everything passes --- translatable/tests/README.md | 3 +- .../config_invalid_value/translatable.toml | 2 - .../translations/test.toml | 5 - .../config_path_missmatch/translatable.toml | 2 - .../config/fail_config_invalid_enums.rs | 5 +- .../config/fail_config_invalid_enums.stderr | 7 ++ .../config/fail_config_path_missmatch.rs | 5 +- .../config/fail_config_path_missmatch.stderr | 7 ++ .../config/fail_translations_malformed.rs | 5 +- .../config/fail_translations_malformed.stderr | 7 ++ translatable/tests/integration_tests.rs | 95 +++++++++++++++---- 11 files changed, 109 insertions(+), 34 deletions(-) delete mode 100644 translatable/tests/environments/config_invalid_value/translatable.toml delete mode 100644 translatable/tests/environments/config_invalid_value/translations/test.toml delete mode 100644 translatable/tests/environments/config_path_missmatch/translatable.toml create mode 100644 translatable/tests/integration/config/fail_config_invalid_enums.stderr create mode 100644 translatable/tests/integration/config/fail_config_path_missmatch.stderr create mode 100644 translatable/tests/integration/config/fail_translations_malformed.stderr diff --git a/translatable/tests/README.md b/translatable/tests/README.md index 81554ee..0bc1714 100644 --- a/translatable/tests/README.md +++ b/translatable/tests/README.md @@ -33,4 +33,5 @@ This project uses make for some command recipes. You can run `make test` and it 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. +there are locks in place so nothing happens, but to make sure you should do that +anyway. diff --git a/translatable/tests/environments/config_invalid_value/translatable.toml b/translatable/tests/environments/config_invalid_value/translatable.toml deleted file mode 100644 index caa05b2..0000000 --- a/translatable/tests/environments/config_invalid_value/translatable.toml +++ /dev/null @@ -1,2 +0,0 @@ - -seek_mode = "invalid value" diff --git a/translatable/tests/environments/config_invalid_value/translations/test.toml b/translatable/tests/environments/config_invalid_value/translations/test.toml deleted file mode 100644 index 527b087..0000000 --- a/translatable/tests/environments/config_invalid_value/translations/test.toml +++ /dev/null @@ -1,5 +0,0 @@ -# There is nothing to load in this test set. -# This remains to avoid *other* errors while expecting invalid value error. -# -# By logic the configuration loading errors should precede. But this is -# better left undefined behavior. diff --git a/translatable/tests/environments/config_path_missmatch/translatable.toml b/translatable/tests/environments/config_path_missmatch/translatable.toml deleted file mode 100644 index 2bf7e69..0000000 --- a/translatable/tests/environments/config_path_missmatch/translatable.toml +++ /dev/null @@ -1,2 +0,0 @@ -# A path missmatch should cause a "file not found" error. -# If the default path (./translations) does not exist, that will raise. diff --git a/translatable/tests/integration/config/fail_config_invalid_enums.rs b/translatable/tests/integration/config/fail_config_invalid_enums.rs index 3b14ede..9ba836e 100644 --- a/translatable/tests/integration/config/fail_config_invalid_enums.rs +++ b/translatable/tests/integration/config/fail_config_invalid_enums.rs @@ -1,8 +1,9 @@ // the macro isn't filled because the expected // failure is on configuration. +#[allow(unused_imports)] use translatable::{translation, Language}; -fn fail_config_invalid_enums() { - translation!(Language::ES, vec![]); +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 new file mode 100644 index 0000000..c985428 --- /dev/null +++ b/translatable/tests/integration/config/fail_config_invalid_enums.stderr @@ -0,0 +1,7 @@ +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 index 52ea5ee..9ba836e 100644 --- a/translatable/tests/integration/config/fail_config_path_missmatch.rs +++ b/translatable/tests/integration/config/fail_config_path_missmatch.rs @@ -1,8 +1,9 @@ // the macro isn't filled because the expected // failure is on configuration. +#[allow(unused_imports)] use translatable::{translation, Language}; -fn fail_config_path_missmatch() { - translation!(Language::ES, vec![]); +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 new file mode 100644 index 0000000..9a6e191 --- /dev/null +++ b/translatable/tests/integration/config/fail_config_path_missmatch.stderr @@ -0,0 +1,7 @@ +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 index 604bb53..9ba836e 100644 --- a/translatable/tests/integration/config/fail_translations_malformed.rs +++ b/translatable/tests/integration/config/fail_translations_malformed.rs @@ -1,8 +1,9 @@ // the macro isn't filled because the expected // failure is on configuration. +#[allow(unused_imports)] use translatable::{translation, Language}; -fn fail_translations_malformed() { - translation!(Language::ES, vec![]); +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 new file mode 100644 index 0000000..4da0199 --- /dev/null +++ b/translatable/tests/integration/config/fail_translations_malformed.stderr @@ -0,0 +1,7 @@ +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_tests.rs b/translatable/tests/integration_tests.rs index 7176e49..cb8b6ab 100644 --- a/translatable/tests/integration_tests.rs +++ b/translatable/tests/integration_tests.rs @@ -1,6 +1,7 @@ +use std::env::remove_var; use std::env::set_var; -use std::env::var; use std::fs::canonicalize; +use std::sync::Mutex; use trybuild::TestCases; // so dynamic tests also run. @@ -15,9 +16,25 @@ mod integration; const PATH_ENV: &str = "TRANSLATABLE_LOCALES_PATH"; const OVERLAP_ENV: &str = "TRANSLATABLE_OVERLAP"; -const SEEK_MODE_ENV: &str = "TRANSLATABLE_SEEK_MODE"; -fn set_locales_env(env: &str) { +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, @@ -28,26 +45,68 @@ fn set_locales_env(env: &str) { } #[test] -fn compile_tests() { - let t = TestCases::new(); +fn valid_environment() { + unsafe { + let t = TestCases::new(); - set_locales_env("everything_valid"); + lock_env!(); - t.pass("./tests/integration/language/pass*.rs"); - t.compile_fail("./tests/integration/language/fail*.rs"); + set_default_env(); + set_locales_env("everything_valid"); - t.pass("./tests/integration/path/pass*.rs"); - t.compile_fail("./tests/integration/path/fail*.rs"); + t.pass("./tests/integration/language/pass*.rs"); + t.compile_fail("./tests/integration/language/fail*.rs"); - t.pass("./tests/integration/templates/pass*.rs"); - t.compile_fail("./tests/integration/templates/fail*.rs"); + t.pass("./tests/integration/path/pass*.rs"); + t.compile_fail("./tests/integration/path/fail*.rs"); - // invalid path in configuration. -// t.compile_fail("../../integration/config/fail_config_path_missmatch.rs"); + t.pass("./tests/integration/templates/pass*.rs"); + t.compile_fail("./tests/integration/templates/fail*.rs"); + } +} + +#[test] +fn invalid_tests_path() { + unsafe { + let t = TestCases::new(); + + lock_env!(); - // invalid enum value in configuration. -// t.compile_fail("../../integration/config/fail_config_invalid_enums.rs"); + set_default_env(); + set_var(PATH_ENV, "something_invalid"); - // translation file rule broken. -// t.compile_fail("../../integration/config/fail_translations_malformed.rs"); + // 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"); + } } From 83c9ba305640fc9b52829695ae85dae997d9e2cc Mon Sep 17 00:00:00 2001 From: stifskere Date: Sat, 26 Apr 2025 09:44:22 +0200 Subject: [PATCH 148/228] chore(tests): add unitary tests --- .github/workflows/pr-coverage.yml | 24 ++++++++ Cargo.lock | 25 +++++--- translatable/Cargo.toml | 2 + .../tests/integration/language/mod.rs | 1 - .../integration/language/pass_dynamic_enum.rs | 2 +- .../integration/language/pass_dynamic_expr.rs | 2 +- .../language/pass_dynamic_invalid_runtime.rs | 2 +- translatable/tests/integration/mod.rs | 1 - translatable/tests/integration/path/mod.rs | 1 - .../integration/path/pass_dynamic_expr.rs | 9 ++- .../tests/integration/templates/mod.rs | 1 - translatable/tests/integration_tests.rs | 15 +---- .../tests/unitary/collection_generation.rs | 38 ++++++++++++ translatable/tests/unitary/language_enum.rs | 26 ++++++++ translatable/tests/unitary/mod.rs | 4 ++ translatable/tests/unitary/templating.rs | 59 +++++++++++++++++++ .../tests/unitary/translation_collection.rs | 55 +++++++++++++++++ translatable/tests/unitary_tests.rs | 3 + .../src/macro_generation/translation.rs | 4 +- translatable_shared/src/misc/templating.rs | 6 +- 20 files changed, 248 insertions(+), 32 deletions(-) create mode 100644 .github/workflows/pr-coverage.yml create mode 100644 translatable/tests/unitary/collection_generation.rs create mode 100644 translatable/tests/unitary/language_enum.rs create mode 100644 translatable/tests/unitary/mod.rs create mode 100644 translatable/tests/unitary/templating.rs create mode 100644 translatable/tests/unitary/translation_collection.rs create mode 100644 translatable/tests/unitary_tests.rs diff --git a/.github/workflows/pr-coverage.yml b/.github/workflows/pr-coverage.yml new file mode 100644 index 0000000..a0672a8 --- /dev/null +++ b/.github/workflows/pr-coverage.yml @@ -0,0 +1,24 @@ +name: PR Coverage check + +on: + pull_request: + branches: [main] + +jobs: + coverage: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set-up Rust + uses: actions-rs/toolchain@v1 + with: + tool: cargo-llvm-cov + + - name: Fail if code coverage is below 80% + run: | + cargo llvm-cov clean --workspace + cargo llvm-cov --workspace --lcov --fail-under-lines 80 + diff --git a/Cargo.lock b/Cargo.lock index 9b05ea1..71117ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -189,9 +189,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +checksum = "900f6c86a685850b1bc9f6223b20125115ee3f31e01207d81655bbcc0aea9231" dependencies = [ "serde", "serde_spanned", @@ -201,31 +201,40 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.24" +version = "0.22.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +checksum = "10558ed0bd2a1562e630926a2d1f0b98c827da99fabd3fe20920a59642504485" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", + "toml_write", "winnow", ] +[[package]] +name = "toml_write" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28391a4201ba7eb1984cfeb6862c0b3ea2cfe23332298967c749dddc0d6cd976" + [[package]] name = "translatable" version = "1.0.0" dependencies = [ + "quote", "thiserror", + "toml", "translatable_proc", "translatable_shared", "trybuild", @@ -361,9 +370,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.7.4" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" +checksum = "6cb8234a863ea0e8cd7284fcdd4f145233eb00fee02bbdd9861aec44e6477bc5" dependencies = [ "memchr", ] diff --git a/translatable/Cargo.toml b/translatable/Cargo.toml index 8a7add9..861ae8f 100644 --- a/translatable/Cargo.toml +++ b/translatable/Cargo.toml @@ -21,4 +21,6 @@ translatable_proc = { version = "1", path = "../translatable_proc" } translatable_shared = { version = "1", path = "../translatable_shared/" } [dev-dependencies] +quote = "1.0.40" +toml = "0.8.21" trybuild = "1.0.104" diff --git a/translatable/tests/integration/language/mod.rs b/translatable/tests/integration/language/mod.rs index d23a2fa..0676883 100644 --- a/translatable/tests/integration/language/mod.rs +++ b/translatable/tests/integration/language/mod.rs @@ -1,4 +1,3 @@ - pub mod pass_dynamic_enum; pub mod pass_dynamic_expr; pub mod pass_dynamic_invalid_runtime; diff --git a/translatable/tests/integration/language/pass_dynamic_enum.rs b/translatable/tests/integration/language/pass_dynamic_enum.rs index db41c06..537165d 100644 --- a/translatable/tests/integration/language/pass_dynamic_enum.rs +++ b/translatable/tests/integration/language/pass_dynamic_enum.rs @@ -1,5 +1,5 @@ #[allow(unused_imports)] // trybuild -use translatable::{translation, Language}; +use translatable::{Language, translation}; #[cfg(test)] #[test] diff --git a/translatable/tests/integration/language/pass_dynamic_expr.rs b/translatable/tests/integration/language/pass_dynamic_expr.rs index bfea01b..d5afe75 100644 --- a/translatable/tests/integration/language/pass_dynamic_expr.rs +++ b/translatable/tests/integration/language/pass_dynamic_expr.rs @@ -8,7 +8,7 @@ pub fn pass_dynamic_expr() { "es".parse().expect("Expected language parsing to be OK"), static greetings::formal ) - .expect("Expected translation generation to be OK"); + .expect("Expected translation generation to be OK"); assert_eq!(translation, "Bueno conocerte."); } diff --git a/translatable/tests/integration/language/pass_dynamic_invalid_runtime.rs b/translatable/tests/integration/language/pass_dynamic_invalid_runtime.rs index b854b3d..8e8dd02 100644 --- a/translatable/tests/integration/language/pass_dynamic_invalid_runtime.rs +++ b/translatable/tests/integration/language/pass_dynamic_invalid_runtime.rs @@ -1,5 +1,5 @@ #[allow(unused_imports)] // trybuild -use translatable::{translation, Language}; +use translatable::{Language, translation}; #[cfg(test)] #[test] diff --git a/translatable/tests/integration/mod.rs b/translatable/tests/integration/mod.rs index 17edfb7..b600327 100644 --- a/translatable/tests/integration/mod.rs +++ b/translatable/tests/integration/mod.rs @@ -1,4 +1,3 @@ - pub mod language; pub mod path; pub mod templates; diff --git a/translatable/tests/integration/path/mod.rs b/translatable/tests/integration/path/mod.rs index 0652130..c4c603d 100644 --- a/translatable/tests/integration/path/mod.rs +++ b/translatable/tests/integration/path/mod.rs @@ -1,3 +1,2 @@ - pub mod pass_dynamic_expr; pub mod pass_static_existing; diff --git a/translatable/tests/integration/path/pass_dynamic_expr.rs b/translatable/tests/integration/path/pass_dynamic_expr.rs index 4df4b7e..ada7c14 100644 --- a/translatable/tests/integration/path/pass_dynamic_expr.rs +++ b/translatable/tests/integration/path/pass_dynamic_expr.rs @@ -4,8 +4,13 @@ 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"); + let translation = translation!( + "es", + "greetings.formal" + .split(".") + .collect() + ) + .expect("Expected translation generation to be OK"); assert_eq!(translation, "Bueno conocerte."); } diff --git a/translatable/tests/integration/templates/mod.rs b/translatable/tests/integration/templates/mod.rs index 1993478..306f03a 100644 --- a/translatable/tests/integration/templates/mod.rs +++ b/translatable/tests/integration/templates/mod.rs @@ -1,4 +1,3 @@ - pub mod pass_ident_ref; pub mod pass_multiple_templates; pub mod pass_trailing_comma; diff --git a/translatable/tests/integration_tests.rs b/translatable/tests/integration_tests.rs index cb8b6ab..3078c91 100644 --- a/translatable/tests/integration_tests.rs +++ b/translatable/tests/integration_tests.rs @@ -1,16 +1,8 @@ -use std::env::remove_var; -use std::env::set_var; +use std::env::{remove_var, set_var}; use std::fs::canonicalize; use std::sync::Mutex; -use trybuild::TestCases; -// so dynamic tests also run. -#[allow(unused_imports)] -use integration::language::*; -#[allow(unused_imports)] -use integration::path::*; -#[allow(unused_imports)] -use integration::templates::*; +use trybuild::TestCases; mod integration; @@ -38,8 +30,7 @@ unsafe fn set_locales_env(env: &str) { unsafe { set_var( PATH_ENV, - canonicalize(format!("./tests/environments/{env}/translations/")) - .unwrap() + canonicalize(format!("./tests/environments/{env}/translations/")).unwrap(), ); } } diff --git a/translatable/tests/unitary/collection_generation.rs b/translatable/tests/unitary/collection_generation.rs new file mode 100644 index 0000000..40b2f83 --- /dev/null +++ b/translatable/tests/unitary/collection_generation.rs @@ -0,0 +1,38 @@ +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/language_enum.rs b/translatable/tests/unitary/language_enum.rs new file mode 100644 index 0000000..ffd40bd --- /dev/null +++ b/translatable/tests/unitary/language_enum.rs @@ -0,0 +1,26 @@ +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 new file mode 100644 index 0000000..3b3e0a0 --- /dev/null +++ b/translatable/tests/unitary/mod.rs @@ -0,0 +1,4 @@ +pub mod collection_generation; +pub mod language_enum; +pub mod templating; +pub mod translation_collection; diff --git a/translatable/tests/unitary/templating.rs b/translatable/tests/unitary/templating.rs new file mode 100644 index 0000000..921a80a --- /dev/null +++ b/translatable/tests/unitary/templating.rs @@ -0,0 +1,59 @@ +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 }}.") +} diff --git a/translatable/tests/unitary/translation_collection.rs b/translatable/tests/unitary/translation_collection.rs new file mode 100644 index 0000000..5aaa1d2 --- /dev/null +++ b/translatable/tests/unitary/translation_collection.rs @@ -0,0 +1,55 @@ +use std::collections::HashMap; + +use toml::Table; +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."), + ) + .expect("TOML to follow the translation rules."), + ), + ( + "b".into(), + TranslationNode::try_from( + FILE_2 + .parse::
() + .expect("TOML to be parsed correctly."), + ) + .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 new file mode 100644 index 0000000..9f69699 --- /dev/null +++ b/translatable/tests/unitary_tests.rs @@ -0,0 +1,3 @@ +// Errors are not tested. + +mod unitary; diff --git a/translatable_proc/src/macro_generation/translation.rs b/translatable_proc/src/macro_generation/translation.rs index 215c61c..3a7a879 100644 --- a/translatable_proc/src/macro_generation/translation.rs +++ b/translatable_proc/src/macro_generation/translation.rs @@ -19,8 +19,8 @@ use crate::macro_input::translation::TranslationMacroArgs; /// 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 +/// 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` diff --git a/translatable_shared/src/misc/templating.rs b/translatable_shared/src/misc/templating.rs index 525533d..b66183d 100644 --- a/translatable_shared/src/misc/templating.rs +++ b/translatable_shared/src/misc/templating.rs @@ -173,7 +173,11 @@ impl FromStr for FormatString { parse_str::(&key) .map_err(|_| TemplateError::InvalidIdent(key))? .to_string(), - char_to_byte[open_idx]..char_to_byte[char_idx + 1], // inclusive + char_to_byte[open_idx] + ..char_to_byte + .get(char_idx + 1) + .copied() + .unwrap_or_else(|| s.len()), )); last_bracket_idx = None; From 3e0d1105a818d0d5a704bd5f82d46da6c070323f Mon Sep 17 00:00:00 2001 From: stifskere Date: Sat, 26 Apr 2025 09:56:04 +0200 Subject: [PATCH 149/228] feat(workflows): pull request coverage --- .github/workflows/pr-coverage.yml | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr-coverage.yml b/.github/workflows/pr-coverage.yml index a0672a8..2912378 100644 --- a/.github/workflows/pr-coverage.yml +++ b/.github/workflows/pr-coverage.yml @@ -17,8 +17,27 @@ jobs: with: tool: cargo-llvm-cov - - name: Fail if code coverage is below 80% + - name: Install cargo-llvm-cov run: | - cargo llvm-cov clean --workspace - cargo llvm-cov --workspace --lcov --fail-under-lines 80 + rustup component add llvm-tools-preview + - name: Generate Coverage Report + run: | + cargo llvm-cov --workspace --lcov > coverage.lcov + + + - name: Fail if overall coverage is below 80% + run: | + apt-get install lcov -y + + coverage_percentage=$(lcov --summary coverage.lcov | grep "lines:" | awk '{print $2}' | sed 's/%//') + + echo "Coverage Percentage: $coverage_percentage%" + + if (( $(echo "$coverage_percentage < 80" | bc -l) )); then + echo "Coverage is below 80%. Failing the build." + exit 1 + else + echo "Coverage is above 80%. Continuing." + exit 0 + fi From 3a5383d41de2daa56cdc220619d0c16464495e06 Mon Sep 17 00:00:00 2001 From: stifskere Date: Sat, 26 Apr 2025 09:59:49 +0200 Subject: [PATCH 150/228] feat(workflows): coverage uploading --- .github/workflows/overall-coverage.yml | 47 ++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/workflows/overall-coverage.yml diff --git a/.github/workflows/overall-coverage.yml b/.github/workflows/overall-coverage.yml new file mode 100644 index 0000000..e42b317 --- /dev/null +++ b/.github/workflows/overall-coverage.yml @@ -0,0 +1,47 @@ +name: Overall coverage upload on main + +on: + push: + branches: [main] + +jobs: + coverage: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set-up Rust + uses: actions-rs/toolchain@v1 + with: + tool: cargo-llvm-cov + + - name: Install cargo-llvm-cov + run: | + rustup component add llvm-tools-preview + + - name: Generate Coverage Report + run: | + cargo llvm-cov --workspace --lcov > coverage.lcov + + - name: Fail if overall coverage is below 80% + run: | + apt-get install lcov -y + + coverage_percentage=$(lcov --summary coverage.lcov | grep "lines:" | awk '{print $2}' | sed 's/%//') + + echo "Coverage Percentage: $coverage_percentage%" + + if (( $(echo "$coverage_percentage < 80" | bc -l) )); then + echo "Coverage is below 80%. Failing the build." + exit 1 + else + echo "Coverage is above 80%. Continuing." + fi + + - name: Upload coverage to Codecov + if: success() + uses: codecov/codecov-action@v3 + with: + file: coverage.lcov From cfb8e9fc15f445262940b116a1e67145f8f08b83 Mon Sep 17 00:00:00 2001 From: stifskere Date: Sat, 26 Apr 2025 10:02:17 +0200 Subject: [PATCH 151/228] docs(readme): add codecov badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index eff3ef4..97826ee 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![Crates.io](https://img.shields.io/crates/v/translatable)](https://crates.io/crates/translatable) [![Docs.rs](https://docs.rs/translatable/badge.svg)](https://docs.rs/translatable) +[![CodeCov](https://codecov.io/github/FlakySL/translatable/config/badge)](https://codecov.io/github/FlakySL/translatable) A robust internationalization solution for Rust featuring compile-time validation, ISO 639-1 compliance, and TOML-based translation management. From 05a2c5b5521b2c3b2272042140f75a7d18ff6bf4 Mon Sep 17 00:00:00 2001 From: stifskere Date: Sat, 26 Apr 2025 10:06:50 +0200 Subject: [PATCH 152/228] fix(readme): CodeCov badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 97826ee..76b8def 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Crates.io](https://img.shields.io/crates/v/translatable)](https://crates.io/crates/translatable) [![Docs.rs](https://docs.rs/translatable/badge.svg)](https://docs.rs/translatable) -[![CodeCov](https://codecov.io/github/FlakySL/translatable/config/badge)](https://codecov.io/github/FlakySL/translatable) +![Codecov](https://img.shields.io/codecov/c/github/FlakySL/translatable) A robust internationalization solution for Rust featuring compile-time validation, ISO 639-1 compliance, and TOML-based translation management. From d827989e526b5203e7bb5c2720a3e89f03d8e681 Mon Sep 17 00:00:00 2001 From: stifskere Date: Sat, 26 Apr 2025 10:08:29 +0200 Subject: [PATCH 153/228] fix(workflows): add toolchain to setup rust --- .github/workflows/overall-coverage.yml | 1 + .github/workflows/pr-coverage.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/overall-coverage.yml b/.github/workflows/overall-coverage.yml index e42b317..f94150d 100644 --- a/.github/workflows/overall-coverage.yml +++ b/.github/workflows/overall-coverage.yml @@ -15,6 +15,7 @@ jobs: - name: Set-up Rust uses: actions-rs/toolchain@v1 with: + toolchain: stable tool: cargo-llvm-cov - name: Install cargo-llvm-cov diff --git a/.github/workflows/pr-coverage.yml b/.github/workflows/pr-coverage.yml index 2912378..2a2bb80 100644 --- a/.github/workflows/pr-coverage.yml +++ b/.github/workflows/pr-coverage.yml @@ -15,6 +15,7 @@ jobs: - name: Set-up Rust uses: actions-rs/toolchain@v1 with: + toolchain: stable tool: cargo-llvm-cov - name: Install cargo-llvm-cov From 1de86a6c5148da7e110c09c0990099bbc351331b Mon Sep 17 00:00:00 2001 From: stifskere Date: Sat, 26 Apr 2025 10:11:06 +0200 Subject: [PATCH 154/228] fix(workflows): change llvm-cov installation --- .github/workflows/overall-coverage.yml | 2 +- .github/workflows/pr-coverage.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/overall-coverage.yml b/.github/workflows/overall-coverage.yml index f94150d..9d5c260 100644 --- a/.github/workflows/overall-coverage.yml +++ b/.github/workflows/overall-coverage.yml @@ -16,10 +16,10 @@ jobs: uses: actions-rs/toolchain@v1 with: toolchain: stable - tool: cargo-llvm-cov - name: Install cargo-llvm-cov run: | + cargo install cargo-llvm-cov rustup component add llvm-tools-preview - name: Generate Coverage Report diff --git a/.github/workflows/pr-coverage.yml b/.github/workflows/pr-coverage.yml index 2a2bb80..b526a43 100644 --- a/.github/workflows/pr-coverage.yml +++ b/.github/workflows/pr-coverage.yml @@ -16,10 +16,10 @@ jobs: uses: actions-rs/toolchain@v1 with: toolchain: stable - tool: cargo-llvm-cov - name: Install cargo-llvm-cov run: | + cargo install cargo-llvm-cov rustup component add llvm-tools-preview - name: Generate Coverage Report From b98453f4830364d4bdd85236b922d2d270a67787 Mon Sep 17 00:00:00 2001 From: stifskere Date: Sat, 26 Apr 2025 10:15:39 +0200 Subject: [PATCH 155/228] fix(workflows): run single threaded with nocapture --- .github/workflows/overall-coverage.yml | 2 +- .github/workflows/pr-coverage.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/overall-coverage.yml b/.github/workflows/overall-coverage.yml index 9d5c260..025329e 100644 --- a/.github/workflows/overall-coverage.yml +++ b/.github/workflows/overall-coverage.yml @@ -24,7 +24,7 @@ jobs: - name: Generate Coverage Report run: | - cargo llvm-cov --workspace --lcov > coverage.lcov + cargo llvm-cov --workspace --lcov > coverage.lcov -- --nocapture --test-threads=1 --color=always - name: Fail if overall coverage is below 80% run: | diff --git a/.github/workflows/pr-coverage.yml b/.github/workflows/pr-coverage.yml index b526a43..4983dee 100644 --- a/.github/workflows/pr-coverage.yml +++ b/.github/workflows/pr-coverage.yml @@ -24,7 +24,7 @@ jobs: - name: Generate Coverage Report run: | - cargo llvm-cov --workspace --lcov > coverage.lcov + cargo llvm-cov --workspace --lcov > coverage.lcov -- --nocapture --test-threads=1 --color=always - name: Fail if overall coverage is below 80% From a71f621ea5d7dcd5c4a060378eb5638e2c2c1b2d Mon Sep 17 00:00:00 2001 From: stifskere Date: Sat, 26 Apr 2025 10:34:29 +0200 Subject: [PATCH 156/228] fix(toolchain): add toolchain override --- .github/workflows/overall-coverage.yml | 2 -- .github/workflows/pr-coverage.yml | 3 --- rust-toolchain.toml | 3 +++ .../tests/integration/templates/fail_not_display.stderr | 3 --- 4 files changed, 3 insertions(+), 8 deletions(-) create mode 100644 rust-toolchain.toml diff --git a/.github/workflows/overall-coverage.yml b/.github/workflows/overall-coverage.yml index 025329e..c1feea6 100644 --- a/.github/workflows/overall-coverage.yml +++ b/.github/workflows/overall-coverage.yml @@ -14,8 +14,6 @@ jobs: - name: Set-up Rust uses: actions-rs/toolchain@v1 - with: - toolchain: stable - name: Install cargo-llvm-cov run: | diff --git a/.github/workflows/pr-coverage.yml b/.github/workflows/pr-coverage.yml index 4983dee..da40453 100644 --- a/.github/workflows/pr-coverage.yml +++ b/.github/workflows/pr-coverage.yml @@ -14,8 +14,6 @@ jobs: - name: Set-up Rust uses: actions-rs/toolchain@v1 - with: - toolchain: stable - name: Install cargo-llvm-cov run: | @@ -26,7 +24,6 @@ jobs: run: | cargo llvm-cov --workspace --lcov > coverage.lcov -- --nocapture --test-threads=1 --color=always - - name: Fail if overall coverage is below 80% run: | apt-get install lcov -y diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..f7b2b43 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.86.0" +profile = "minimal" diff --git a/translatable/tests/integration/templates/fail_not_display.stderr b/translatable/tests/integration/templates/fail_not_display.stderr index 22d079f..9698c14 100644 --- a/translatable/tests/integration/templates/fail_not_display.stderr +++ b/translatable/tests/integration/templates/fail_not_display.stderr @@ -13,9 +13,6 @@ error[E0599]: `NotDisplay` doesn't implement `std::fmt::Display` = 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` From 50b0c4ab973a97f748fb7c3ecc5e418f63736920 Mon Sep 17 00:00:00 2001 From: stifskere Date: Sat, 26 Apr 2025 10:37:18 +0200 Subject: [PATCH 157/228] fix(workflows): add override to setup rust --- .github/workflows/overall-coverage.yml | 2 ++ .github/workflows/pr-coverage.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/overall-coverage.yml b/.github/workflows/overall-coverage.yml index c1feea6..03a233b 100644 --- a/.github/workflows/overall-coverage.yml +++ b/.github/workflows/overall-coverage.yml @@ -14,6 +14,8 @@ jobs: - name: Set-up Rust uses: actions-rs/toolchain@v1 + with: + override: true - name: Install cargo-llvm-cov run: | diff --git a/.github/workflows/pr-coverage.yml b/.github/workflows/pr-coverage.yml index da40453..fc219aa 100644 --- a/.github/workflows/pr-coverage.yml +++ b/.github/workflows/pr-coverage.yml @@ -14,6 +14,8 @@ jobs: - name: Set-up Rust uses: actions-rs/toolchain@v1 + with: + override: true - name: Install cargo-llvm-cov run: | From 88757d20051bf077d1f786bbf8be996b3ed7ae8a Mon Sep 17 00:00:00 2001 From: stifskere Date: Sat, 26 Apr 2025 10:42:34 +0200 Subject: [PATCH 158/228] fix(workflows): replace deprecated actions-rs --- .github/workflows/overall-coverage.yml | 4 +--- .github/workflows/pr-coverage.yml | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/overall-coverage.yml b/.github/workflows/overall-coverage.yml index 03a233b..1f3074b 100644 --- a/.github/workflows/overall-coverage.yml +++ b/.github/workflows/overall-coverage.yml @@ -13,9 +13,7 @@ jobs: uses: actions/checkout@v4 - name: Set-up Rust - uses: actions-rs/toolchain@v1 - with: - override: true + uses: dtolnay/rust-toolchain@stable - name: Install cargo-llvm-cov run: | diff --git a/.github/workflows/pr-coverage.yml b/.github/workflows/pr-coverage.yml index fc219aa..975a81e 100644 --- a/.github/workflows/pr-coverage.yml +++ b/.github/workflows/pr-coverage.yml @@ -13,9 +13,7 @@ jobs: uses: actions/checkout@v4 - name: Set-up Rust - uses: actions-rs/toolchain@v1 - with: - override: true + uses: dtolnay/rust-toolchain@stable - name: Install cargo-llvm-cov run: | From 4526242d9eb730cdc8593447ddafa8639b271d86 Mon Sep 17 00:00:00 2001 From: stifskere Date: Sat, 26 Apr 2025 10:44:51 +0200 Subject: [PATCH 159/228] fix(workflows): sudo the thing --- .github/workflows/overall-coverage.yml | 2 +- .github/workflows/pr-coverage.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/overall-coverage.yml b/.github/workflows/overall-coverage.yml index 1f3074b..7de41bd 100644 --- a/.github/workflows/overall-coverage.yml +++ b/.github/workflows/overall-coverage.yml @@ -26,7 +26,7 @@ jobs: - name: Fail if overall coverage is below 80% run: | - apt-get install lcov -y + sudo apt-get install lcov -y coverage_percentage=$(lcov --summary coverage.lcov | grep "lines:" | awk '{print $2}' | sed 's/%//') diff --git a/.github/workflows/pr-coverage.yml b/.github/workflows/pr-coverage.yml index 975a81e..16a7b7c 100644 --- a/.github/workflows/pr-coverage.yml +++ b/.github/workflows/pr-coverage.yml @@ -26,7 +26,7 @@ jobs: - name: Fail if overall coverage is below 80% run: | - apt-get install lcov -y + sudo apt-get install lcov -y coverage_percentage=$(lcov --summary coverage.lcov | grep "lines:" | awk '{print $2}' | sed 's/%//') From e1e0e6f5ddcf64fc2295e6fee8747c8fa14e0740 Mon Sep 17 00:00:00 2001 From: stifskere Date: Sat, 26 Apr 2025 10:47:58 +0200 Subject: [PATCH 160/228] chore(toolchain): allow updates --- rust-toolchain.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index f7b2b43..2fc3eef 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.86.0" +channel = "stable" profile = "minimal" From 9b9a1aa4e7bdbc57c465565bfc28c0839d65c453 Mon Sep 17 00:00:00 2001 From: chikof Date: Sat, 26 Apr 2025 16:52:23 +0100 Subject: [PATCH 161/228] chore: update nix flake and add rust toolchain components --- flake.lock | 30 +++++++++++++++--------------- flake.nix | 24 ++++++++++++++++-------- rust-toolchain.toml | 1 + 3 files changed, 32 insertions(+), 23 deletions(-) diff --git a/flake.lock b/flake.lock index 603a706..6df32df 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "crane": { "locked": { - "lastModified": 1742394900, - "narHash": "sha256-vVOAp9ahvnU+fQoKd4SEXB2JG2wbENkpqcwlkIXgUC0=", + "lastModified": 1745454774, + "narHash": "sha256-oLvmxOnsEKGtwczxp/CwhrfmQUG2ym24OMWowcoRhH8=", "owner": "ipetkov", "repo": "crane", - "rev": "70947c1908108c0c551ddfd73d4f750ff2ea67cd", + "rev": "efd36682371678e2b6da3f108fdb5c613b3ec598", "type": "github" }, "original": { @@ -21,11 +21,11 @@ "rust-analyzer-src": "rust-analyzer-src" }, "locked": { - "lastModified": 1742452566, - "narHash": "sha256-sVuLDQ2UIWfXUBbctzrZrXM2X05YjX08K7XHMztt36E=", + "lastModified": 1745649180, + "narHash": "sha256-3Ptviong+IYr9y3W6ddJMQDn/VpnTQHgwGU3i022HtA=", "owner": "nix-community", "repo": "fenix", - "rev": "7d9ba794daf5e8cc7ee728859bc688d8e26d5f06", + "rev": "585fc772cd167cad7d30222b2eb5f5e4bb2166b9", "type": "github" }, "original": { @@ -54,11 +54,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1742288794, - "narHash": "sha256-Txwa5uO+qpQXrNG4eumPSD+hHzzYi/CdaM80M9XRLCo=", + "lastModified": 1745526057, + "narHash": "sha256-ITSpPDwvLBZBnPRS2bUcHY3gZSwis/uTe255QgMtTLA=", "owner": "nixos", "repo": "nixpkgs", - "rev": "b6eaf97c6960d97350c584de1b6dcff03c9daf42", + "rev": "f771eb401a46846c1aebd20552521b233dd7e18b", "type": "github" }, "original": { @@ -70,11 +70,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1742923925, - "narHash": "sha256-biPjLws6FiBVUUDHEMFq5pUQL84Wf7PntPYdo3oKkFw=", + "lastModified": 1745377448, + "narHash": "sha256-jhZDfXVKdD7TSEGgzFJQvEEZ2K65UMiqW5YJ2aIqxMA=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "25d1b84f5c90632a623c48d83a2faf156451e6b1", + "rev": "507b63021ada5fee621b6ca371c4fca9ca46f52c", "type": "github" }, "original": { @@ -95,11 +95,11 @@ "rust-analyzer-src": { "flake": false, "locked": { - "lastModified": 1742296961, - "narHash": "sha256-gCpvEQOrugHWLimD1wTFOJHagnSEP6VYBDspq96Idu0=", + "lastModified": 1745591749, + "narHash": "sha256-ynI1QfQEMHHuO+hJ8RLzhCo31XLm86vI7zRjKMQ45BQ=", "owner": "rust-lang", "repo": "rust-analyzer", - "rev": "15d87419f1a123d8f888d608129c3ce3ff8f13d4", + "rev": "df594ba8f4f72064002a4170eea031ba4300f087", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index ef0eefb..2bbeadb 100644 --- a/flake.nix +++ b/flake.nix @@ -12,17 +12,25 @@ let pkgs = nixpkgs.legacyPackages.${system}; crane = inputs.crane.mkLib pkgs; + toolchainToml = ./rust-toolchain.toml; + # Determine the Rust toolchain toolchain = with fenix.packages.${system}; - combine [ - stable.rustc - stable.cargo - stable.rust-src - complete.rustfmt - stable.clippy - stable.rust-analyzer - ]; + if builtins.pathExists toolchainToml then + fromToolchainFile { + file = toolchainToml; + sha256 = "sha256-X/4ZBHO3iW0fOenQ3foEvscgAPJYl2abspaBThDOukI="; + } + else + combine [ + stable.rustc + stable.cargo + complete.rustfmt + stable.clippy + stable.rust-analyzer + ]; + # Override the toolchain in crane craneLib = crane.overrideToolchain toolchain; in { devShells.default = craneLib.devShell { diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 2fc3eef..bb80082 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,4 @@ [toolchain] channel = "stable" profile = "minimal" +components = ["rustfmt", "clippy", "rust-analyzer", "rustc"] From 9dff43ad9e2cada0ee6c5d84f95aa9e186ac2a3d Mon Sep 17 00:00:00 2001 From: chikof Date: Sat, 26 Apr 2025 17:12:28 +0100 Subject: [PATCH 162/228] fix: i didn't have to remove rust-src --- flake.nix | 1 + rust-toolchain.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 2bbeadb..3b330db 100644 --- a/flake.nix +++ b/flake.nix @@ -24,6 +24,7 @@ else combine [ stable.rustc + stable.rust-src stable.cargo complete.rustfmt stable.clippy diff --git a/rust-toolchain.toml b/rust-toolchain.toml index bb80082..dac3b5c 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] channel = "stable" profile = "minimal" -components = ["rustfmt", "clippy", "rust-analyzer", "rustc"] +components = ["rustfmt", "clippy", "rust-analyzer", "rustc", "rust-src"] From 2689cbf95d104a86e5d9fd52bb5be17243326d57 Mon Sep 17 00:00:00 2001 From: chikof Date: Sat, 26 Apr 2025 17:37:45 +0100 Subject: [PATCH 163/228] fix: update error message --- .../tests/integration/templates/fail_not_display.stderr | 3 +++ 1 file changed, 3 insertions(+) diff --git a/translatable/tests/integration/templates/fail_not_display.stderr b/translatable/tests/integration/templates/fail_not_display.stderr index 9698c14..22d079f 100644 --- a/translatable/tests/integration/templates/fail_not_display.stderr +++ b/translatable/tests/integration/templates/fail_not_display.stderr @@ -13,6 +13,9 @@ error[E0599]: `NotDisplay` doesn't implement `std::fmt::Display` = 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` From ec8e409c2818e4a9e29f9258153606a5c9ea28cf Mon Sep 17 00:00:00 2001 From: chikof Date: Sat, 26 Apr 2025 18:11:21 +0100 Subject: [PATCH 164/228] ci: install cargoo-llvm-cov with cargo-binstall --- .github/workflows/overall-coverage.yml | 5 ++++- .github/workflows/pr-coverage.yml | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/overall-coverage.yml b/.github/workflows/overall-coverage.yml index 7de41bd..b1c3c80 100644 --- a/.github/workflows/overall-coverage.yml +++ b/.github/workflows/overall-coverage.yml @@ -15,9 +15,12 @@ jobs: - name: Set-up Rust uses: dtolnay/rust-toolchain@stable + - name: Install cargo-binstall@latest + uses: cargo-bins/cargo-binstall@main + - name: Install cargo-llvm-cov run: | - cargo install cargo-llvm-cov + cargo binstall cargo-llvm-cov rustup component add llvm-tools-preview - name: Generate Coverage Report diff --git a/.github/workflows/pr-coverage.yml b/.github/workflows/pr-coverage.yml index 16a7b7c..8327168 100644 --- a/.github/workflows/pr-coverage.yml +++ b/.github/workflows/pr-coverage.yml @@ -15,9 +15,12 @@ jobs: - name: Set-up Rust uses: dtolnay/rust-toolchain@stable + - name: Install cargo-binstall@latest + uses: cargo-bins/cargo-binstall@main + - name: Install cargo-llvm-cov run: | - cargo install cargo-llvm-cov + cargo binstall cargo-llvm-cov rustup component add llvm-tools-preview - name: Generate Coverage Report From cd00418f2beb662cc4f869d171cb2a69c9d85def Mon Sep 17 00:00:00 2001 From: chikof Date: Sat, 26 Apr 2025 19:30:29 +0100 Subject: [PATCH 165/228] ci: improve PR coverage check to avoid LCOV generation - Remove LCOV file generation from PR coverage workflow - Simplify to only check coverage percentage against threshold - Keep identical coverage calculation between CI and local - Maintain 80% coverage requirement check --- .github/workflows/overall-coverage.yml | 29 +++++++++++++------------- .github/workflows/pr-coverage.yml | 24 ++++++++++----------- .gitignore | 2 ++ Makefile | 14 +++++++++++++ flake.nix | 7 ++++--- rust-toolchain.toml | 9 +++++++- 6 files changed, 53 insertions(+), 32 deletions(-) diff --git a/.github/workflows/overall-coverage.yml b/.github/workflows/overall-coverage.yml index b1c3c80..9c410ec 100644 --- a/.github/workflows/overall-coverage.yml +++ b/.github/workflows/overall-coverage.yml @@ -15,35 +15,34 @@ jobs: - name: Set-up Rust uses: dtolnay/rust-toolchain@stable - - name: Install cargo-binstall@latest - uses: cargo-bins/cargo-binstall@main + - name: Set-up Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + components: llvm-tools-preview - name: Install cargo-llvm-cov run: | cargo binstall cargo-llvm-cov - rustup component add llvm-tools-preview - - name: Generate Coverage Report + - name: Generate coverage and get percentage + id: coverage run: | - cargo llvm-cov --workspace --lcov > coverage.lcov -- --nocapture --test-threads=1 --color=always + make cov export-lcov=1 | tee output.log + coverage=$(grep 'Total Coverage: ' output.log | awk '{print $3}') + echo "coverage_percentage=${coverage%\%}" >> $GITHUB_OUTPUT + echo "Detected coverage: ${coverage}" - name: Fail if overall coverage is below 80% run: | - sudo apt-get install lcov -y - - coverage_percentage=$(lcov --summary coverage.lcov | grep "lines:" | awk '{print $2}' | sed 's/%//') - - echo "Coverage Percentage: $coverage_percentage%" - - if (( $(echo "$coverage_percentage < 80" | bc -l) )); then - echo "Coverage is below 80%. Failing the build." + if (( $(echo "${{ steps.coverage.outputs.coverage_percentage }} < 80" | bc -l) )); then + echo "❌ Coverage is below 80% (${{ steps.coverage.outputs.coverage_percentage }}%)" exit 1 else - echo "Coverage is above 80%. Continuing." + echo "βœ… Coverage meets requirement (${{ steps.coverage.outputs.coverage_percentage }}%)" fi - name: Upload coverage to Codecov - if: success() uses: codecov/codecov-action@v3 with: file: coverage.lcov diff --git a/.github/workflows/pr-coverage.yml b/.github/workflows/pr-coverage.yml index 8327168..5f2b073 100644 --- a/.github/workflows/pr-coverage.yml +++ b/.github/workflows/pr-coverage.yml @@ -14,6 +14,9 @@ jobs: - name: Set-up Rust uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + components: llvm-tools-preview - name: Install cargo-binstall@latest uses: cargo-bins/cargo-binstall@main @@ -21,24 +24,19 @@ jobs: - name: Install cargo-llvm-cov run: | cargo binstall cargo-llvm-cov - rustup component add llvm-tools-preview - - name: Generate Coverage Report + - name: Check coverage percentage + id: coverage run: | - cargo llvm-cov --workspace --lcov > coverage.lcov -- --nocapture --test-threads=1 --color=always + coverage=$(make cov | grep 'Total Coverage: ' | awk '{print $3}') + echo "coverage_percentage=${coverage%\%}" >> $GITHUB_OUTPUT + echo "Detected coverage: ${coverage}" - name: Fail if overall coverage is below 80% run: | - sudo apt-get install lcov -y - - coverage_percentage=$(lcov --summary coverage.lcov | grep "lines:" | awk '{print $2}' | sed 's/%//') - - echo "Coverage Percentage: $coverage_percentage%" - - if (( $(echo "$coverage_percentage < 80" | bc -l) )); then - echo "Coverage is below 80%. Failing the build." + if (( $(echo "${{ steps.coverage.outputs.coverage_percentage }} < 80" | bc -l) )); then + echo "❌ Coverage is below 80% (${{ steps.coverage.outputs.coverage_percentage }}%)" exit 1 else - echo "Coverage is above 80%. Continuing." - exit 0 + echo "βœ… Coverage meets requirement (${{ steps.coverage.outputs.coverage_percentage }}%)" fi diff --git a/.gitignore b/.gitignore index f63d5dd..e594a4d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /target .bacon-locations .direnv + +**/*.lcov diff --git a/Makefile b/Makefile index 88f542d..1250935 100644 --- a/Makefile +++ b/Makefile @@ -1,2 +1,16 @@ +LCOV_FILE ?= coverage.lcov + test: cargo test -p translatable -- --nocapture --color=always --test-threads=1 + +cov: +ifdef export-lcov + @echo "Generating LCOV report..." + @coverage=$$(cargo llvm-cov -- --nocapture --test-threads=1 --color=never | grep '^TOTAL' | awk '{print $$10}'); \ + cargo llvm-cov --lcov -- --nocapture --test-threads=1 --color=always > $(LCOV_FILE); \ + echo "LCOV report saved to $(LCOV_FILE)"; \ + echo "Total Coverage: $$coverage%" +else + @coverage=$$(cargo llvm-cov -- --nocapture --test-threads=1 --color=never | grep '^TOTAL' | awk '{print $$10}'); \ + echo "Total Coverage: $$coverage%" +endif diff --git a/flake.nix b/flake.nix index 3b330db..66b8dcf 100644 --- a/flake.nix +++ b/flake.nix @@ -16,7 +16,7 @@ # Determine the Rust toolchain toolchain = with fenix.packages.${system}; - if builtins.pathExists toolchainToml then + (if builtins.pathExists toolchainToml then fromToolchainFile { file = toolchainToml; sha256 = "sha256-X/4ZBHO3iW0fOenQ3foEvscgAPJYl2abspaBThDOukI="; @@ -29,13 +29,14 @@ complete.rustfmt stable.clippy stable.rust-analyzer - ]; + stable.llvm-tools-preview + ]); # Override the toolchain in crane craneLib = crane.overrideToolchain toolchain; in { devShells.default = craneLib.devShell { - packages = with pkgs; [ toolchain gnumake ]; + packages = with pkgs; [ toolchain gnumake cargo-llvm-cov ]; env = { LAZYVIM_RUST_DIAGNOSTICS = "bacon-ls"; }; }; diff --git a/rust-toolchain.toml b/rust-toolchain.toml index dac3b5c..fb606ef 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,11 @@ [toolchain] channel = "stable" profile = "minimal" -components = ["rustfmt", "clippy", "rust-analyzer", "rustc", "rust-src"] +components = [ + "rustfmt", + "clippy", + "rust-analyzer", + "rustc", + "rust-src", + "llvm-tools-preview", +] From b54ce12333ac8fd71adfe034a0ee7b6b87833dc4 Mon Sep 17 00:00:00 2001 From: chikof Date: Sat, 26 Apr 2025 19:42:39 +0100 Subject: [PATCH 166/228] fix(ci): forgot to install binstall --- .github/workflows/overall-coverage.yml | 3 +++ README.md | 1 + 2 files changed, 4 insertions(+) diff --git a/.github/workflows/overall-coverage.yml b/.github/workflows/overall-coverage.yml index 9c410ec..d21ff05 100644 --- a/.github/workflows/overall-coverage.yml +++ b/.github/workflows/overall-coverage.yml @@ -21,6 +21,9 @@ jobs: toolchain: stable components: llvm-tools-preview + - name: Install cargo-binstall@latest + uses: cargo-bins/cargo-binstall@main + - name: Install cargo-llvm-cov run: | cargo binstall cargo-llvm-cov diff --git a/README.md b/README.md index 76b8def..ac13ab0 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![Crates.io](https://img.shields.io/crates/v/translatable)](https://crates.io/crates/translatable) [![Docs.rs](https://docs.rs/translatable/badge.svg)](https://docs.rs/translatable) ![Codecov](https://img.shields.io/codecov/c/github/FlakySL/translatable) +![tests](https://github.com/FlakySL/translatable/actions/workflows/overall-coverage.yml/badge.svg) A robust internationalization solution for Rust featuring compile-time validation, ISO 639-1 compliance, and TOML-based translation management. From 0552827ceceeebe11b7359a9e2cba0aa980d8e4b Mon Sep 17 00:00:00 2001 From: stifskere Date: Sat, 26 Apr 2025 20:46:00 +0200 Subject: [PATCH 167/228] chore(workflows): update titles --- .github/workflows/overall-coverage.yml | 2 +- .github/workflows/pr-coverage.yml | 2 +- README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/overall-coverage.yml b/.github/workflows/overall-coverage.yml index d21ff05..0856925 100644 --- a/.github/workflows/overall-coverage.yml +++ b/.github/workflows/overall-coverage.yml @@ -1,4 +1,4 @@ -name: Overall coverage upload on main +name: tests on: push: diff --git a/.github/workflows/pr-coverage.yml b/.github/workflows/pr-coverage.yml index 5f2b073..d0a7ea6 100644 --- a/.github/workflows/pr-coverage.yml +++ b/.github/workflows/pr-coverage.yml @@ -1,4 +1,4 @@ -name: PR Coverage check +name: pr coverage on: pull_request: diff --git a/README.md b/README.md index ac13ab0..6e34810 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ pub fn Greeting(language: Language) -> impl IntoView { log::error!("Translation error {err:#}"); "Translation error.".into() } - } + }; view! {

{ message }

From 782f538466b9c7b8e4447d080093dc25fbf98915 Mon Sep 17 00:00:00 2001 From: chikof Date: Sat, 26 Apr 2025 20:10:55 +0100 Subject: [PATCH 168/228] fix(ci): add codecov token --- .github/workflows/overall-coverage.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/overall-coverage.yml b/.github/workflows/overall-coverage.yml index 0856925..ce00674 100644 --- a/.github/workflows/overall-coverage.yml +++ b/.github/workflows/overall-coverage.yml @@ -49,3 +49,5 @@ jobs: uses: codecov/codecov-action@v3 with: file: coverage.lcov + fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} From 75a4611e739a3cf4db5f3559e4606ddaa622ecf1 Mon Sep 17 00:00:00 2001 From: chikof Date: Sat, 26 Apr 2025 20:23:33 +0100 Subject: [PATCH 169/228] fix(ci): add missing slug --- .github/workflows/overall-coverage.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/overall-coverage.yml b/.github/workflows/overall-coverage.yml index ce00674..c8540d4 100644 --- a/.github/workflows/overall-coverage.yml +++ b/.github/workflows/overall-coverage.yml @@ -51,3 +51,4 @@ jobs: file: coverage.lcov fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} + slug: FlakySL/translatable From e30d8b0d3f4371ac845ef754aaede83a2dbef97d Mon Sep 17 00:00:00 2001 From: stifskere Date: Sat, 26 Apr 2025 21:34:29 +0200 Subject: [PATCH 170/228] chore(ci): codecov verbose --- .github/workflows/overall-coverage.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/overall-coverage.yml b/.github/workflows/overall-coverage.yml index c8540d4..62db4e7 100644 --- a/.github/workflows/overall-coverage.yml +++ b/.github/workflows/overall-coverage.yml @@ -52,3 +52,4 @@ jobs: fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} slug: FlakySL/translatable + verbose: true From 758ee6f7d5fbc090d5beec757925a9f04c9740ef Mon Sep 17 00:00:00 2001 From: stifskere Date: Sat, 26 Apr 2025 21:36:09 +0200 Subject: [PATCH 171/228] chore(ci): update codecov --- .github/workflows/overall-coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/overall-coverage.yml b/.github/workflows/overall-coverage.yml index 62db4e7..caaa687 100644 --- a/.github/workflows/overall-coverage.yml +++ b/.github/workflows/overall-coverage.yml @@ -46,7 +46,7 @@ jobs: fi - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v5 with: file: coverage.lcov fail_ci_if_error: true From 70c43b0ad556a4a2a17fead11ca0c779a1964b4b Mon Sep 17 00:00:00 2001 From: stifskere Date: Sat, 26 Apr 2025 21:43:41 +0200 Subject: [PATCH 172/228] chore(test): test runtime error display --- README.md | 2 +- translatable/tests/unitary/mod.rs | 1 + translatable/tests/unitary/runtime_error.rs | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 translatable/tests/unitary/runtime_error.rs diff --git a/README.md b/README.md index 6e34810..141fc96 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Crates.io](https://img.shields.io/crates/v/translatable)](https://crates.io/crates/translatable) [![Docs.rs](https://docs.rs/translatable/badge.svg)](https://docs.rs/translatable) -![Codecov](https://img.shields.io/codecov/c/github/FlakySL/translatable) +[![Codecov](https://img.shields.io/codecov/c/github/FlakySL/translatable)](https://app.codecov.io/gh/FlakySL/translatable) ![tests](https://github.com/FlakySL/translatable/actions/workflows/overall-coverage.yml/badge.svg) A robust internationalization solution for Rust featuring compile-time validation, ISO 639-1 compliance, and TOML-based translation management. diff --git a/translatable/tests/unitary/mod.rs b/translatable/tests/unitary/mod.rs index 3b3e0a0..c80cded 100644 --- a/translatable/tests/unitary/mod.rs +++ b/translatable/tests/unitary/mod.rs @@ -1,4 +1,5 @@ pub mod collection_generation; pub mod language_enum; +pub mod runtime_error; pub mod templating; pub mod translation_collection; diff --git a/translatable/tests/unitary/runtime_error.rs b/translatable/tests/unitary/runtime_error.rs new file mode 100644 index 0000000..7cab73a --- /dev/null +++ b/translatable/tests/unitary/runtime_error.rs @@ -0,0 +1,14 @@ +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'" + ) +} From 67e7cb8a0e8bd2591a0653e1674aff4742c7c6d2 Mon Sep 17 00:00:00 2001 From: stifskere Date: Sun, 27 Apr 2025 00:05:22 +0200 Subject: [PATCH 173/228] chore(publish): publish attempt 1 --- .github/workflows/publish.yml | 73 +++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..e6ea789 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,73 @@ +name: publish to crates.io + +on: + workflow_run: + workflows: ["tests"] + types: + - completed + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set-up Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + + - name: Set-up semver + run: | + curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - + sudo apt install -y nodejs + npm install -g semver + + - name: Get crate version + id: crate_version + run: | + VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version') + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Get latest git tag + id: latest_tag + run: | + TAG=$(git describe --tags --abbrev=0 || echo "") + echo "tag=$TAG" >> $GITHUB_OUTPUT + + - name: Install guile and guile-semver + run: | + sudo apt-get update + sudo apt-get install -y guile-semver + + - name: Compare versions + id: should_publish + run: | + VERSION="${{ steps.crate_version.outputs.version }}" + TAG="${{ steps.latest_tag.outputs.tag }}" + + if semver -r gt "$VERSION" "$TAG"; then + echo "publish=true" >> $GITHUB_OUTPUT + else + echo "publish=false" >> $GITHUB_OUTPUT + fi + + - name: Publish to crates.io + if: steps.should_publish.outputs.publish == 'true' + run: | + cargo publish + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} + + - name: Crate and push new git tag + if: steps.should_publish.outputs.publish == 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag ${{ steps.crate_version.outputs.version }} + git push origin ${{ steps.crate_version.outputs.version }} + From a4f09fb024dd5af7fd7a2be99a6cfdb4539e10b0 Mon Sep 17 00:00:00 2001 From: stifskere Date: Sun, 27 Apr 2025 00:08:15 +0200 Subject: [PATCH 174/228] chore(publish): publish attempt 1.1 // cancelled workflow --- .github/workflows/publish.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e6ea789..0ebf22e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -39,11 +39,6 @@ jobs: TAG=$(git describe --tags --abbrev=0 || echo "") echo "tag=$TAG" >> $GITHUB_OUTPUT - - name: Install guile and guile-semver - run: | - sudo apt-get update - sudo apt-get install -y guile-semver - - name: Compare versions id: should_publish run: | From a5b308400a661f498938529361b0044450a84536 Mon Sep 17 00:00:00 2001 From: stifskere Date: Sun, 27 Apr 2025 00:12:45 +0200 Subject: [PATCH 175/228] chore(publish): publish attempt 2 --- .github/workflows/publish.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0ebf22e..e4f3165 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -45,6 +45,10 @@ jobs: VERSION="${{ steps.crate_version.outputs.version }}" TAG="${{ steps.latest_tag.outputs.tag }}" + if [ -z "$TAG" ]; then + TAG="0.0.0" + fi + if semver -r gt "$VERSION" "$TAG"; then echo "publish=true" >> $GITHUB_OUTPUT else From 1a5cc4142132aeb840ee12a9182f935a4edb810d Mon Sep 17 00:00:00 2001 From: stifskere Date: Sun, 27 Apr 2025 01:17:11 +0200 Subject: [PATCH 176/228] chore(publish): publish attempt 3 --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e4f3165..17ce557 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -49,7 +49,7 @@ jobs: TAG="0.0.0" fi - if semver -r gt "$VERSION" "$TAG"; then + if semver -r "> $TAG" "$VERSION"; then echo "publish=true" >> $GITHUB_OUTPUT else echo "publish=false" >> $GITHUB_OUTPUT From 4de044d37769b54cc1b4cd154fbcab827a1a5ea4 Mon Sep 17 00:00:00 2001 From: stifskere Date: Sun, 27 Apr 2025 01:20:23 +0200 Subject: [PATCH 177/228] chore(publish): publish attempt 4 // specify packages --- .github/workflows/publish.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 17ce557..04d09a4 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -58,7 +58,9 @@ jobs: - name: Publish to crates.io if: steps.should_publish.outputs.publish == 'true' run: | - cargo publish + cargo publish -p translatable_shared + cargo publish -p translatable_proc + cargo publish -p translatable env: CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} From 3ef06029696e9728cc7db8682f6c03422f6dcefa Mon Sep 17 00:00:00 2001 From: stifskere Date: Sun, 27 Apr 2025 02:46:37 +0200 Subject: [PATCH 178/228] chore(ci): set workflow permissions --- .github/workflows/overall-coverage.yml | 2 ++ .github/workflows/pr-coverage.yml | 2 ++ .github/workflows/publish.yml | 2 ++ 3 files changed, 6 insertions(+) diff --git a/.github/workflows/overall-coverage.yml b/.github/workflows/overall-coverage.yml index caaa687..c61a647 100644 --- a/.github/workflows/overall-coverage.yml +++ b/.github/workflows/overall-coverage.yml @@ -7,6 +7,8 @@ on: jobs: coverage: runs-on: ubuntu-latest + permissions: + actions: read steps: - name: Checkout code diff --git a/.github/workflows/pr-coverage.yml b/.github/workflows/pr-coverage.yml index d0a7ea6..581cfcf 100644 --- a/.github/workflows/pr-coverage.yml +++ b/.github/workflows/pr-coverage.yml @@ -7,6 +7,8 @@ on: jobs: coverage: runs-on: ubuntu-latest + permissions: + actions: read steps: - name: Checkout code diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 04d09a4..b739bb5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,6 +9,8 @@ on: jobs: publish: runs-on: ubuntu-latest + permissions: + contents: write steps: - name: Checkout code From 0693938655cd371dfc417a4c3f8dbdf7c9408701 Mon Sep 17 00:00:00 2001 From: stifskere Date: Sun, 27 Apr 2025 09:06:55 +0200 Subject: [PATCH 179/228] feat(context): macro parsing --- translatable/src/lib.rs | 3 + .../integration/context/context_macro.rs | 7 ++ translatable/tests/integration/context/mod.rs | 1 + translatable/tests/integration/mod.rs | 1 + translatable_proc/src/lib.rs | 9 ++ .../src/macro_generation/context.rs | 1 + translatable_proc/src/macro_generation/mod.rs | 1 + translatable_proc/src/macro_input/context.rs | 88 +++++++++++++++++++ translatable_proc/src/macro_input/mod.rs | 1 + 9 files changed, 112 insertions(+) create mode 100644 translatable/tests/integration/context/context_macro.rs create mode 100644 translatable/tests/integration/context/mod.rs create mode 100644 translatable_proc/src/macro_generation/context.rs create mode 100644 translatable_proc/src/macro_input/context.rs diff --git a/translatable/src/lib.rs b/translatable/src/lib.rs index fff945c..68d9039 100644 --- a/translatable/src/lib.rs +++ b/translatable/src/lib.rs @@ -25,6 +25,9 @@ pub use error::RuntimeError as Error; #[rustfmt::skip] pub use translatable_proc::translation; +#[rustfmt::skip] +pub use translatable_proc::translation_context; + /// Language enum re-export. /// /// This `use` statement re-exports diff --git a/translatable/tests/integration/context/context_macro.rs b/translatable/tests/integration/context/context_macro.rs new file mode 100644 index 0000000..c5aa50c --- /dev/null +++ b/translatable/tests/integration/context/context_macro.rs @@ -0,0 +1,7 @@ +use translatable::translation_context; + +#[translation_context] +pub struct TestContext { + pub xd: path::to::translation, + lol: path::to::other_translation +} diff --git a/translatable/tests/integration/context/mod.rs b/translatable/tests/integration/context/mod.rs new file mode 100644 index 0000000..9169472 --- /dev/null +++ b/translatable/tests/integration/context/mod.rs @@ -0,0 +1 @@ +pub mod context_macro; diff --git a/translatable/tests/integration/mod.rs b/translatable/tests/integration/mod.rs index b600327..a75dfd7 100644 --- a/translatable/tests/integration/mod.rs +++ b/translatable/tests/integration/mod.rs @@ -1,3 +1,4 @@ +pub mod context; pub mod language; pub mod path; pub mod templates; diff --git a/translatable_proc/src/lib.rs b/translatable_proc/src/lib.rs index 74d7267..87f70ab 100644 --- a/translatable_proc/src/lib.rs +++ b/translatable_proc/src/lib.rs @@ -7,7 +7,9 @@ //! The `translatable` library re-exports the macros //! declared in this crate. +use macro_input::context::ContextMacroArgs; use proc_macro::TokenStream; +use quote::quote; use syn::parse_macro_input; use crate::macro_generation::translation::translation_macro; @@ -63,3 +65,10 @@ mod macro_input; pub fn translation(input: TokenStream) -> TokenStream { translation_macro(parse_macro_input!(input as TranslationMacroArgs).into()).into() } + +#[proc_macro_attribute] +pub fn translation_context(_attr: TokenStream, item: TokenStream) -> TokenStream { + parse_macro_input!(item as ContextMacroArgs); + + quote! { struct Name {} }.into() +} diff --git a/translatable_proc/src/macro_generation/context.rs b/translatable_proc/src/macro_generation/context.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/translatable_proc/src/macro_generation/context.rs @@ -0,0 +1 @@ + diff --git a/translatable_proc/src/macro_generation/mod.rs b/translatable_proc/src/macro_generation/mod.rs index a3a3ef1..eeddfdf 100644 --- a/translatable_proc/src/macro_generation/mod.rs +++ b/translatable_proc/src/macro_generation/mod.rs @@ -10,4 +10,5 @@ //! //! [`macro_input`]: crate::macro_input +pub mod context; pub mod translation; diff --git a/translatable_proc/src/macro_input/context.rs b/translatable_proc/src/macro_input/context.rs new file mode 100644 index 0000000..cf3e994 --- /dev/null +++ b/translatable_proc/src/macro_input/context.rs @@ -0,0 +1,88 @@ +use proc_macro2::Span; +use syn::{braced, Ident, Path, PathArguments, Result as SynResult, Token}; +use syn::parse::{Parse, ParseStream}; +use thiserror::Error; +use translatable_shared::macros::errors::IntoCompileError; + +#[derive(Error, Debug)] +enum ContextMacroArgsError { + #[error("This translation path contains generic arguments, and cannot be parsed")] + InvalidPathContainsGenerics, + + #[error("This macro must be applied on a struct")] + NotAStruct +} + +pub struct ContextMacroArgs { + is_pub: bool, + ident: String, + fields: Vec, +} + +pub struct ContextMacroPathField { + is_pub: bool, + path: Vec, + ident: String, +} + +impl Parse for ContextMacroPathField { + fn parse(input: ParseStream) -> SynResult { + let mut is_pub = false; + + if input.peek(Token![pub]) { + input.parse::()?; + is_pub = true; + } + + let ident = input.parse::()? + .to_string(); + + input.parse::()?; + + let path = input.parse::()? + .segments + .iter() + .map(|segment| match &segment.arguments { + PathArguments::None => Ok(segment.ident.to_string()), + + other => Err(ContextMacroArgsError::InvalidPathContainsGenerics + .to_syn_error(other)), + }) + .collect::, _>>()?; + + Ok(Self { is_pub, path, ident }) + } +} + +impl Parse for ContextMacroArgs { + fn parse(input: ParseStream) -> SynResult { + let mut is_pub = false; + + if input.peek(Token![pub]) { + input.parse::()?; + is_pub = true; + } + + if !input.peek(Token![struct]) { + let dummy_ident = Ident::new("_", Span::call_site()); + return Err( + ContextMacroArgsError::NotAStruct + .to_syn_error(dummy_ident) + ); + } + + input.parse::()?; + + let ident = input.parse::()?.to_string(); + + let content; + braced!(content in input); + + let fields = content + .parse_terminated(ContextMacroPathField::parse, Token![,])? + .into_iter() + .collect::>(); + + Ok(Self { is_pub, ident, fields }) + } +} diff --git a/translatable_proc/src/macro_input/mod.rs b/translatable_proc/src/macro_input/mod.rs index 5ae5e87..53e232e 100644 --- a/translatable_proc/src/macro_input/mod.rs +++ b/translatable_proc/src/macro_input/mod.rs @@ -10,5 +10,6 @@ //! //! [`macro_generation`]: crate::macro_generation +pub mod context; pub mod input_type; pub mod translation; From 09204738d761b632fb6b9e03242104e86e79204b Mon Sep 17 00:00:00 2001 From: stifskere Date: Sun, 27 Apr 2025 09:56:14 +0200 Subject: [PATCH 180/228] feat(macros): abstract translation_path into its own type --- translatable_proc/src/lib.rs | 4 +- .../src/macro_generation/context.rs | 4 ++ .../src/macro_generation/translation.rs | 14 ++++--- translatable_proc/src/macro_input/context.rs | 21 ++++------ translatable_proc/src/macro_input/mod.rs | 2 +- .../src/macro_input/translation.rs | 37 +++++------------ .../src/macro_input/{ => utils}/input_type.rs | 0 .../src/macro_input/utils/mod.rs | 2 + .../src/macro_input/utils/translation_path.rs | 40 +++++++++++++++++++ 9 files changed, 73 insertions(+), 51 deletions(-) rename translatable_proc/src/macro_input/{ => utils}/input_type.rs (100%) create mode 100644 translatable_proc/src/macro_input/utils/mod.rs create mode 100644 translatable_proc/src/macro_input/utils/translation_path.rs diff --git a/translatable_proc/src/lib.rs b/translatable_proc/src/lib.rs index 87f70ab..f0f9a3f 100644 --- a/translatable_proc/src/lib.rs +++ b/translatable_proc/src/lib.rs @@ -7,7 +7,7 @@ //! The `translatable` library re-exports the macros //! declared in this crate. -use macro_input::context::ContextMacroArgs; +use macro_input::context::ContextMacroStruct; use proc_macro::TokenStream; use quote::quote; use syn::parse_macro_input; @@ -68,7 +68,7 @@ pub fn translation(input: TokenStream) -> TokenStream { #[proc_macro_attribute] pub fn translation_context(_attr: TokenStream, item: TokenStream) -> TokenStream { - parse_macro_input!(item as ContextMacroArgs); + parse_macro_input!(item as ContextMacroStruct); quote! { struct Name {} }.into() } diff --git a/translatable_proc/src/macro_generation/context.rs b/translatable_proc/src/macro_generation/context.rs index 8b13789..e6d115b 100644 --- a/translatable_proc/src/macro_generation/context.rs +++ b/translatable_proc/src/macro_generation/context.rs @@ -1 +1,5 @@ +use proc_macro::TokenStream; +pub fn context_macro() -> TokenStream { + todo!() +} diff --git a/translatable_proc/src/macro_generation/translation.rs b/translatable_proc/src/macro_generation/translation.rs index 3a7a879..e948d10 100644 --- a/translatable_proc/src/macro_generation/translation.rs +++ b/translatable_proc/src/macro_generation/translation.rs @@ -14,7 +14,7 @@ use translatable_shared::macros::collections::{map_to_tokens, map_transform_to_t use translatable_shared::misc::language::Language; use crate::data::translations::load_translations; -use crate::macro_input::input_type::InputType; +use crate::macro_input::utils::input_type::InputType; use crate::macro_input::translation::TranslationMacroArgs; /// Macro compile-time translation resolution error. @@ -76,10 +76,11 @@ pub fn translation_macro(input: TranslationMacroArgs) -> TokenStream2 { if let InputType::Static(language) = input.language() { if let InputType::Static(path) = input.path() { - let static_path_display = path.join("::"); + let path_segments = path.segments(); + let static_path_display = path_segments.join("::"); let translation_object = translations - .find_path(path) + .find_path(path_segments) .ok_or_else(|| MacroCompileError::PathNotFound(static_path_display.clone())); let translation = handle_macro_result!( @@ -111,17 +112,18 @@ pub fn translation_macro(input: TranslationMacroArgs) -> TokenStream2 { let translation_object = match input.path() { InputType::Static(path) => { - let static_path_display = path.join("::"); + let path_segments = path.segments(); + let static_path_display = path_segments.join("::"); let translation_object = translations - .find_path(path) + .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.to_string()),*]; + let path: Vec<_> = vec![#(#path_segments.to_string()),*]; #translations_tokens } diff --git a/translatable_proc/src/macro_input/context.rs b/translatable_proc/src/macro_input/context.rs index cf3e994..f6ce285 100644 --- a/translatable_proc/src/macro_input/context.rs +++ b/translatable_proc/src/macro_input/context.rs @@ -1,9 +1,11 @@ use proc_macro2::Span; -use syn::{braced, Ident, Path, PathArguments, Result as SynResult, Token}; +use syn::{braced, Ident, Result as SynResult, Token}; use syn::parse::{Parse, ParseStream}; use thiserror::Error; use translatable_shared::macros::errors::IntoCompileError; +use super::utils::translation_path::TranslationPath; + #[derive(Error, Debug)] enum ContextMacroArgsError { #[error("This translation path contains generic arguments, and cannot be parsed")] @@ -13,7 +15,7 @@ enum ContextMacroArgsError { NotAStruct } -pub struct ContextMacroArgs { +pub struct ContextMacroStruct { is_pub: bool, ident: String, fields: Vec, @@ -21,7 +23,7 @@ pub struct ContextMacroArgs { pub struct ContextMacroPathField { is_pub: bool, - path: Vec, + path: TranslationPath, ident: String, } @@ -39,22 +41,13 @@ impl Parse for ContextMacroPathField { input.parse::()?; - let path = input.parse::()? - .segments - .iter() - .map(|segment| match &segment.arguments { - PathArguments::None => Ok(segment.ident.to_string()), - - other => Err(ContextMacroArgsError::InvalidPathContainsGenerics - .to_syn_error(other)), - }) - .collect::, _>>()?; + let path = input.parse::()?; Ok(Self { is_pub, path, ident }) } } -impl Parse for ContextMacroArgs { +impl Parse for ContextMacroStruct { fn parse(input: ParseStream) -> SynResult { let mut is_pub = false; diff --git a/translatable_proc/src/macro_input/mod.rs b/translatable_proc/src/macro_input/mod.rs index 53e232e..e91b8be 100644 --- a/translatable_proc/src/macro_input/mod.rs +++ b/translatable_proc/src/macro_input/mod.rs @@ -10,6 +10,6 @@ //! //! [`macro_generation`]: crate::macro_generation +pub mod utils; pub mod context; -pub mod input_type; pub mod translation; diff --git a/translatable_proc/src/macro_input/translation.rs b/translatable_proc/src/macro_input/translation.rs index 70b333f..fa902ae 100644 --- a/translatable_proc/src/macro_input/translation.rs +++ b/translatable_proc/src/macro_input/translation.rs @@ -12,12 +12,13 @@ use proc_macro2::TokenStream as TokenStream2; use quote::ToTokens; use syn::parse::{Parse, ParseStream}; use syn::token::Static; -use syn::{Expr, ExprLit, Ident, Lit, Path, PathArguments, Result as SynResult, Token}; +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::input_type::InputType; +use super::utils::input_type::InputType; +use super::utils::translation_path::TranslationPath; /// Parse error for [`TranslationMacroArgs`]. /// @@ -30,13 +31,6 @@ enum TranslationMacroArgsError { /// was found. #[error("The literal '{0}' is an invalid ISO 639-1 string, and cannot be parsed")] InvalidIsoLiteral(String), - - /// Extra tokens were found while parsing a static path for - /// the [`translation!()`] macro, specifically generic arguments. - /// - /// [`translation!()`]: crate::translation - #[error("This translation path contains generic arguments, and cannot be parsed")] - InvalidPathContainsGenerics, } /// [`translation!()`] macro input arguments. @@ -63,7 +57,7 @@ pub struct TranslationMacroArgs { /// as `static path::to::translation` or dynamic if /// it's another expression, this way represented as a /// [`TokenStream2`]. - path: InputType>, + path: InputType, /// Stores the replacement arguments for the translation /// templates such as `Hello {name}` if found on a translation. @@ -98,23 +92,10 @@ impl Parse for TranslationMacroArgs { input.parse::()?; let parsed_path_arg = match input.parse::() { - Ok(_) => { - let language_arg = input - .parse::()? - .segments - .into_iter() - .map(|segment| match segment.arguments { - PathArguments::None => Ok(segment - .ident - .to_string()), - - other => Err(TranslationMacroArgsError::InvalidPathContainsGenerics - .to_syn_error(other)), - }) - .collect::, _>>()?; - - InputType::Static(language_arg) - }, + Ok(_) => InputType::Static( + input + .parse::()? + ), Err(_) => InputType::Dynamic( input @@ -172,7 +153,7 @@ impl TranslationMacroArgs { /// A reference to `self.path` as [`InputType>`] #[inline] #[allow(unused)] - pub fn path(&self) -> &InputType> { + pub fn path(&self) -> &InputType { &self.path } diff --git a/translatable_proc/src/macro_input/input_type.rs b/translatable_proc/src/macro_input/utils/input_type.rs similarity index 100% rename from translatable_proc/src/macro_input/input_type.rs rename to translatable_proc/src/macro_input/utils/input_type.rs diff --git a/translatable_proc/src/macro_input/utils/mod.rs b/translatable_proc/src/macro_input/utils/mod.rs new file mode 100644 index 0000000..d4a7ad5 --- /dev/null +++ b/translatable_proc/src/macro_input/utils/mod.rs @@ -0,0 +1,2 @@ +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 new file mode 100644 index 0000000..85369b3 --- /dev/null +++ b/translatable_proc/src/macro_input/utils/translation_path.rs @@ -0,0 +1,40 @@ +use proc_macro2::Span; +use syn::{parse::{Parse, ParseStream}, spanned::Spanned, Path, PathArguments, Result as SynResult, Error as SynError}; + +pub struct TranslationPath { + segments: Vec, + span: Span +} + +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 }) + } +} + +impl TranslationPath { + #[inline] + #[allow(unused)] + pub fn segments(&self) -> &Vec { + &self.segments + } + + #[inline] + #[allow(unused)] + pub fn span(&self) -> Span { + self.span + } +} From 9b84b7a39834895b7edda4576e64e946df9e9b47 Mon Sep 17 00:00:00 2001 From: stifskere Date: Sun, 27 Apr 2025 10:27:28 +0200 Subject: [PATCH 181/228] feat(context): allow optional base paths --- .../integration/context/context_macro.rs | 4 +- translatable_proc/src/lib.rs | 14 ++-- .../src/macro_generation/context.rs | 9 ++- .../src/macro_generation/translation.rs | 2 +- translatable_proc/src/macro_input/context.rs | 70 ++++++++++++++++--- translatable_proc/src/macro_input/mod.rs | 2 +- .../src/macro_input/translation.rs | 5 +- .../src/macro_input/utils/translation_path.rs | 15 ++-- 8 files changed, 89 insertions(+), 32 deletions(-) diff --git a/translatable/tests/integration/context/context_macro.rs b/translatable/tests/integration/context/context_macro.rs index c5aa50c..dabdcba 100644 --- a/translatable/tests/integration/context/context_macro.rs +++ b/translatable/tests/integration/context/context_macro.rs @@ -1,7 +1,7 @@ use translatable::translation_context; -#[translation_context] +#[translation_context(base::path)] pub struct TestContext { pub xd: path::to::translation, - lol: path::to::other_translation + lol: path::to::other_translation, } diff --git a/translatable_proc/src/lib.rs b/translatable_proc/src/lib.rs index f0f9a3f..4fa620b 100644 --- a/translatable_proc/src/lib.rs +++ b/translatable_proc/src/lib.rs @@ -7,9 +7,9 @@ //! The `translatable` library re-exports the macros //! declared in this crate. -use macro_input::context::ContextMacroStruct; +use macro_generation::context::context_macro; +use macro_input::context::{ContextMacroArgs, ContextMacroStruct}; use proc_macro::TokenStream; -use quote::quote; use syn::parse_macro_input; use crate::macro_generation::translation::translation_macro; @@ -67,8 +67,10 @@ pub fn translation(input: TokenStream) -> TokenStream { } #[proc_macro_attribute] -pub fn translation_context(_attr: TokenStream, item: TokenStream) -> TokenStream { - parse_macro_input!(item as ContextMacroStruct); - - quote! { struct Name {} }.into() +pub fn translation_context(attr: TokenStream, item: TokenStream) -> TokenStream { + context_macro( + parse_macro_input!(attr as ContextMacroArgs), + parse_macro_input!(item as ContextMacroStruct) + ) + .into() } diff --git a/translatable_proc/src/macro_generation/context.rs b/translatable_proc/src/macro_generation/context.rs index e6d115b..6e6e8d3 100644 --- a/translatable_proc/src/macro_generation/context.rs +++ b/translatable_proc/src/macro_generation/context.rs @@ -1,5 +1,8 @@ -use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; -pub fn context_macro() -> TokenStream { - todo!() +use crate::macro_input::context::{ContextMacroArgs, ContextMacroStruct}; + +pub fn context_macro(base_path: ContextMacroArgs, macro_input: ContextMacroStruct) -> TokenStream2 { + quote! { struct Thing {} } } diff --git a/translatable_proc/src/macro_generation/translation.rs b/translatable_proc/src/macro_generation/translation.rs index e948d10..d01af68 100644 --- a/translatable_proc/src/macro_generation/translation.rs +++ b/translatable_proc/src/macro_generation/translation.rs @@ -14,8 +14,8 @@ use translatable_shared::macros::collections::{map_to_tokens, map_transform_to_t use translatable_shared::misc::language::Language; use crate::data::translations::load_translations; -use crate::macro_input::utils::input_type::InputType; use crate::macro_input::translation::TranslationMacroArgs; +use crate::macro_input::utils::input_type::InputType; /// Macro compile-time translation resolution error. /// diff --git a/translatable_proc/src/macro_input/context.rs b/translatable_proc/src/macro_input/context.rs index f6ce285..b716349 100644 --- a/translatable_proc/src/macro_input/context.rs +++ b/translatable_proc/src/macro_input/context.rs @@ -1,6 +1,6 @@ use proc_macro2::Span; -use syn::{braced, Ident, Result as SynResult, Token}; use syn::parse::{Parse, ParseStream}; +use syn::{Ident, Result as SynResult, Token, braced}; use thiserror::Error; use translatable_shared::macros::errors::IntoCompileError; @@ -8,13 +8,12 @@ use super::utils::translation_path::TranslationPath; #[derive(Error, Debug)] enum ContextMacroArgsError { - #[error("This translation path contains generic arguments, and cannot be parsed")] - InvalidPathContainsGenerics, - #[error("This macro must be applied on a struct")] - NotAStruct + NotAStruct, } +pub struct ContextMacroArgs(Option); + pub struct ContextMacroStruct { is_pub: bool, ident: String, @@ -36,7 +35,8 @@ impl Parse for ContextMacroPathField { is_pub = true; } - let ident = input.parse::()? + let ident = input + .parse::()? .to_string(); input.parse::()?; @@ -58,15 +58,14 @@ impl Parse for ContextMacroStruct { if !input.peek(Token![struct]) { let dummy_ident = Ident::new("_", Span::call_site()); - return Err( - ContextMacroArgsError::NotAStruct - .to_syn_error(dummy_ident) - ); + return Err(ContextMacroArgsError::NotAStruct.to_syn_error(dummy_ident)); } input.parse::()?; - let ident = input.parse::()?.to_string(); + let ident = input + .parse::()? + .to_string(); let content; braced!(content in input); @@ -79,3 +78,52 @@ impl Parse for ContextMacroStruct { Ok(Self { is_pub, ident, fields }) } } + +impl Parse for ContextMacroArgs { + fn parse(input: ParseStream) -> SynResult { + let path = input.parse::() + .ok(); + + Ok(ContextMacroArgs(path)) + } +} + +impl ContextMacroStruct { + #[inline] + #[allow(unused)] + pub fn is_pub(&self) -> bool { + self.is_pub + } + + #[inline] + #[allow(unused)] + pub fn ident(&self) -> &str { + &self.ident + } + + #[inline] + #[allow(unused)] + pub fn fields(&self) -> &[ContextMacroPathField] { + &self.fields + } +} + +impl ContextMacroPathField { + #[inline] + #[allow(unused)] + pub fn is_pub(&self) -> bool { + self.is_pub + } + + #[inline] + #[allow(unused)] + pub fn path(&self) -> &TranslationPath { + &self.path + } + + #[inline] + #[allow(unused)] + pub fn ident(&self) -> &str { + &self.ident + } +} diff --git a/translatable_proc/src/macro_input/mod.rs b/translatable_proc/src/macro_input/mod.rs index e91b8be..1ff13d5 100644 --- a/translatable_proc/src/macro_input/mod.rs +++ b/translatable_proc/src/macro_input/mod.rs @@ -10,6 +10,6 @@ //! //! [`macro_generation`]: crate::macro_generation -pub mod utils; 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 index fa902ae..9bb86b4 100644 --- a/translatable_proc/src/macro_input/translation.rs +++ b/translatable_proc/src/macro_input/translation.rs @@ -92,10 +92,7 @@ impl Parse for TranslationMacroArgs { input.parse::()?; let parsed_path_arg = match input.parse::() { - Ok(_) => InputType::Static( - input - .parse::()? - ), + Ok(_) => InputType::Static(input.parse::()?), Err(_) => InputType::Dynamic( input diff --git a/translatable_proc/src/macro_input/utils/translation_path.rs b/translatable_proc/src/macro_input/utils/translation_path.rs index 85369b3..5424170 100644 --- a/translatable_proc/src/macro_input/utils/translation_path.rs +++ b/translatable_proc/src/macro_input/utils/translation_path.rs @@ -1,9 +1,11 @@ use proc_macro2::Span; -use syn::{parse::{Parse, ParseStream}, spanned::Spanned, Path, PathArguments, Result as SynResult, Error as SynError}; +use syn::parse::{Parse, ParseStream}; +use syn::spanned::Spanned; +use syn::{Error as SynError, Path, PathArguments, Result as SynResult}; pub struct TranslationPath { segments: Vec, - span: Span + span: Span, } impl Parse for TranslationPath { @@ -15,9 +17,14 @@ impl Parse for TranslationPath { .segments .into_iter() .map(|segment| match segment.arguments { - PathArguments::None => Ok(segment.ident.to_string()), + PathArguments::None => Ok(segment + .ident + .to_string()), - error => Err(SynError::new_spanned(error, "A translation path can't contain generic arguments.")) + error => Err(SynError::new_spanned( + error, + "A translation path can't contain generic arguments.", + )), }) .collect::>()?; From aadf6abf9d9d95b4567189db3fb244c747bb1907 Mon Sep 17 00:00:00 2001 From: chikof Date: Sun, 27 Apr 2025 23:13:14 +0100 Subject: [PATCH 182/228] ci: codecov reports when mergint to main and making a PR --- .github/workflows/overall-coverage.yml | 4 +-- .github/workflows/pr-coverage.yml | 42 -------------------------- 2 files changed, 1 insertion(+), 45 deletions(-) delete mode 100644 .github/workflows/pr-coverage.yml diff --git a/.github/workflows/overall-coverage.yml b/.github/workflows/overall-coverage.yml index caaa687..37c88d7 100644 --- a/.github/workflows/overall-coverage.yml +++ b/.github/workflows/overall-coverage.yml @@ -3,6 +3,7 @@ name: tests on: push: branches: [main] + pull_request: jobs: coverage: @@ -12,9 +13,6 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Set-up Rust - uses: dtolnay/rust-toolchain@stable - - name: Set-up Rust uses: dtolnay/rust-toolchain@stable with: diff --git a/.github/workflows/pr-coverage.yml b/.github/workflows/pr-coverage.yml deleted file mode 100644 index d0a7ea6..0000000 --- a/.github/workflows/pr-coverage.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: pr coverage - -on: - pull_request: - branches: [main] - -jobs: - coverage: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set-up Rust - uses: dtolnay/rust-toolchain@stable - with: - toolchain: stable - components: llvm-tools-preview - - - name: Install cargo-binstall@latest - uses: cargo-bins/cargo-binstall@main - - - name: Install cargo-llvm-cov - run: | - cargo binstall cargo-llvm-cov - - - name: Check coverage percentage - id: coverage - run: | - coverage=$(make cov | grep 'Total Coverage: ' | awk '{print $3}') - echo "coverage_percentage=${coverage%\%}" >> $GITHUB_OUTPUT - echo "Detected coverage: ${coverage}" - - - name: Fail if overall coverage is below 80% - run: | - if (( $(echo "${{ steps.coverage.outputs.coverage_percentage }} < 80" | bc -l) )); then - echo "❌ Coverage is below 80% (${{ steps.coverage.outputs.coverage_percentage }}%)" - exit 1 - else - echo "βœ… Coverage meets requirement (${{ steps.coverage.outputs.coverage_percentage }}%)" - fi From 678fa3cdc682d49e19a16455e3a638905f79c152 Mon Sep 17 00:00:00 2001 From: chikof Date: Sun, 27 Apr 2025 23:13:38 +0100 Subject: [PATCH 183/228] chore: pull request template --- .github/pull_request_template.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..7d5ab0d --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,11 @@ +## Pre-submission Checklist +- [] I've checked existing issues and pull requests +- [] I've read the [Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct) +- [] I've tested my changes +- [] I've listed all my changes in the `Changes` section + +## Changes +- + +## Linked Issues +- fixes # From 4413ab72eb87021a9ff37e38d5b26aa2e4c7745d Mon Sep 17 00:00:00 2001 From: chikof Date: Sun, 27 Apr 2025 23:14:53 +0100 Subject: [PATCH 184/228] fix: pr template checklists --- .github/pull_request_template.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 7d5ab0d..ae8eb47 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,8 +1,8 @@ ## Pre-submission Checklist -- [] I've checked existing issues and pull requests -- [] I've read the [Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct) -- [] I've tested my changes -- [] I've listed all my changes in the `Changes` section +- [ ] I've checked existing issues and pull requests +- [ ] I've read the [Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct) +- [ ] I've tested my changes +- [ ] I've listed all my changes in the `Changes` section ## Changes - From 3c8fcb0ea14fbbe967dfe0f1eeb911c4b1ffb29b Mon Sep 17 00:00:00 2001 From: chikof Date: Sun, 27 Apr 2025 23:34:05 +0100 Subject: [PATCH 185/228] fix(ci): add workflow permissions --- .github/workflows/overall-coverage.yml | 2 ++ .github/workflows/publish.yml | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/overall-coverage.yml b/.github/workflows/overall-coverage.yml index 37c88d7..a471b5e 100644 --- a/.github/workflows/overall-coverage.yml +++ b/.github/workflows/overall-coverage.yml @@ -8,6 +8,8 @@ on: jobs: coverage: runs-on: ubuntu-latest + permissions: + contents: read steps: - name: Checkout code diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 04d09a4..7eab478 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,6 +9,8 @@ on: jobs: publish: runs-on: ubuntu-latest + permissions: + contents: write steps: - name: Checkout code @@ -71,4 +73,3 @@ jobs: git config user.email "github-actions[bot]@users.noreply.github.com" git tag ${{ steps.crate_version.outputs.version }} git push origin ${{ steps.crate_version.outputs.version }} - From 0935615cb440983cd1e3bcc9079b1dd97dae177b Mon Sep 17 00:00:00 2001 From: chikof Date: Sun, 27 Apr 2025 23:56:16 +0100 Subject: [PATCH 186/228] chore: add more badges --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 141fc96..35f31f6 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ ![translatable-readme](https://github.com/user-attachments/assets/4994514f-bbcc-48ea-a086-32e684adcd3a) -[![Crates.io](https://img.shields.io/crates/v/translatable)](https://crates.io/crates/translatable) -[![Docs.rs](https://docs.rs/translatable/badge.svg)](https://docs.rs/translatable) +[![Crates.io](https://badges.ws/crates/v/translatable)](https://crates.io/crates/translatable) +[![License](https://badges.ws/crates/l/translatable)](https://docs.rs/translatable) +[![Docs.rs](https://badges.ws/crates/docs/translatable)](https://docs.rs/translatable) +[![Downloads](https://badges.ws/crates/dt/translatable)](https://docs.rs/translatable) [![Codecov](https://img.shields.io/codecov/c/github/FlakySL/translatable)](https://app.codecov.io/gh/FlakySL/translatable) ![tests](https://github.com/FlakySL/translatable/actions/workflows/overall-coverage.yml/badge.svg) From 54fb5cd1744d34c386a9d861e4295718d22940fe Mon Sep 17 00:00:00 2001 From: stifskere Date: Mon, 28 Apr 2025 03:28:06 +0200 Subject: [PATCH 187/228] feat(context): re parse macro context --- .../integration/context/context_macro.rs | 7 +- translatable_proc/src/lib.rs | 4 +- .../src/macro_generation/context.rs | 112 +++++++++++- translatable_proc/src/macro_input/context.rs | 170 +++++++----------- .../src/macro_input/translation.rs | 4 +- .../src/macro_input/utils/translation_path.rs | 9 + 6 files changed, 196 insertions(+), 110 deletions(-) diff --git a/translatable/tests/integration/context/context_macro.rs b/translatable/tests/integration/context/context_macro.rs index dabdcba..16965e7 100644 --- a/translatable/tests/integration/context/context_macro.rs +++ b/translatable/tests/integration/context/context_macro.rs @@ -1,7 +1,12 @@ -use translatable::translation_context; +use translatable::{translation_context, Language}; #[translation_context(base::path)] pub struct TestContext { pub xd: path::to::translation, lol: path::to::other_translation, } + +#[test] +fn test() { + TestContext::lol(&Language::ES); +} diff --git a/translatable_proc/src/lib.rs b/translatable_proc/src/lib.rs index 4fa620b..8a4e818 100644 --- a/translatable_proc/src/lib.rs +++ b/translatable_proc/src/lib.rs @@ -70,7 +70,7 @@ pub fn translation(input: TokenStream) -> TokenStream { pub fn translation_context(attr: TokenStream, item: TokenStream) -> TokenStream { context_macro( parse_macro_input!(attr as ContextMacroArgs), - parse_macro_input!(item as ContextMacroStruct) + parse_macro_input!(item as ContextMacroStruct), ) - .into() + .into() } diff --git a/translatable_proc/src/macro_generation/context.rs b/translatable_proc/src/macro_generation/context.rs index 6e6e8d3..be12a34 100644 --- a/translatable_proc/src/macro_generation/context.rs +++ b/translatable_proc/src/macro_generation/context.rs @@ -1,8 +1,114 @@ -use proc_macro2::TokenStream as TokenStream2; -use quote::quote; +use proc_macro2::{Span, TokenStream as TokenStream2}; +use quote::{format_ident, quote}; +use syn::Ident; +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}; +#[derive(Error, Debug)] +enum MacroCompileError { + +} + +macro_rules! pub_token { + ($input:expr) => { + if $input { + quote! { pub } + } else { + quote! { } + } + }; +} + pub fn context_macro(base_path: ContextMacroArgs, macro_input: ContextMacroStruct) -> TokenStream2 { - quote! { struct Thing {} } + let translations = handle_macro_result!(load_translations()); + + let pub_token = pub_token!(macro_input.is_pub()); + let struct_ident = Ident::new(macro_input.ident(), Span::call_site()); + + let base_path = base_path + .or_empty() + .segments() + .to_vec(); + + let translations = macro_input + .fields() + .iter() + .map(|field| ( + field.is_pub(), + field.ident(), + { + let path_segments = field + .path() + .segments() + .to_vec(); + + let path = base_path + .iter() + .chain(&path_segments) + .collect(); + + translations.find_path(&path) + } + )) + .collect::>(); + + let struct_fields = macro_input + .fields() + .iter() + .map(|field| { + let pub_token = pub_token!(field.is_pub()); + + let ident = Ident::new( + field.ident(), + Span::call_site() + ); + + quote! { #pub_token #ident: String } + }); + + let field_impls = translations + .iter() + .map(|(is_pub, ident, translation)| { + let pub_token = pub_token!(*is_pub); + + let ident = Ident::new( + ident, + Span::call_site() + ); + + let templated_ident = format_ident!("templated_{ident}"); + + let translation = translation + .map(|translation| map_to_tokens(translation)) + .ok_or(); + + quote! { + #[inline] + #pub_token fn #templated_ident(language: &translatable::Language) + -> Option { + #translation + .remove(language) + } + + #[inline] + #pub_token fn #ident(language: &translatable::Language) -> Option { + Self::#templated_ident(language) + .map(|lang| lang.replace_with(std::collections::HashMap::new())) + } + } + }); + + quote! { + #pub_token struct #struct_ident { + #(#struct_fields),* + } + + impl #struct_ident { + #(#field_impls)* + } + } } diff --git a/translatable_proc/src/macro_input/context.rs b/translatable_proc/src/macro_input/context.rs index b716349..f467f14 100644 --- a/translatable_proc/src/macro_input/context.rs +++ b/translatable_proc/src/macro_input/context.rs @@ -1,129 +1,95 @@ -use proc_macro2::Span; -use syn::parse::{Parse, ParseStream}; -use syn::{Ident, Result as SynResult, Token, braced}; +use syn::{parse::{Parse, ParseStream}, Field, Ident, ItemStruct, Result as SynResult, Type, Visibility, Error as SynError}; use thiserror::Error; use translatable_shared::macros::errors::IntoCompileError; use super::utils::translation_path::TranslationPath; #[derive(Error, Debug)] -enum ContextMacroArgsError { - #[error("This macro must be applied on a struct")] - NotAStruct, +enum MacroArgsError { + #[error("Only named fields are allowed")] + InvalidFieldType } pub struct ContextMacroArgs(Option); -pub struct ContextMacroStruct { - is_pub: bool, - ident: String, - fields: Vec, -} - -pub struct ContextMacroPathField { - is_pub: bool, +pub struct ContextMacroField { path: TranslationPath, - ident: String, + is_pub: Visibility, + ident: Ident, + ty: Type } -impl Parse for ContextMacroPathField { - fn parse(input: ParseStream) -> SynResult { - let mut is_pub = false; - - if input.peek(Token![pub]) { - input.parse::()?; - is_pub = true; - } - - let ident = input - .parse::()? - .to_string(); - - input.parse::()?; - - let path = input.parse::()?; - - Ok(Self { is_pub, path, ident }) - } -} - -impl Parse for ContextMacroStruct { - fn parse(input: ParseStream) -> SynResult { - let mut is_pub = false; - - if input.peek(Token![pub]) { - input.parse::()?; - is_pub = true; - } - - if !input.peek(Token![struct]) { - let dummy_ident = Ident::new("_", Span::call_site()); - return Err(ContextMacroArgsError::NotAStruct.to_syn_error(dummy_ident)); - } - - input.parse::()?; - - let ident = input - .parse::()? - .to_string(); - - let content; - braced!(content in input); - - let fields = content - .parse_terminated(ContextMacroPathField::parse, Token![,])? - .into_iter() - .collect::>(); - - Ok(Self { is_pub, ident, fields }) - } +pub struct ContextMacroStruct { + is_pub: Visibility, + ident: Ident, + fields: Vec } impl Parse for ContextMacroArgs { fn parse(input: ParseStream) -> SynResult { - let path = input.parse::() - .ok(); - - Ok(ContextMacroArgs(path)) + Ok(Self( + if !input.is_empty() { + Some(input.parse::()?) + } else { + None + } + )) } } -impl ContextMacroStruct { - #[inline] - #[allow(unused)] - pub fn is_pub(&self) -> bool { - self.is_pub - } - - #[inline] - #[allow(unused)] - pub fn ident(&self) -> &str { - &self.ident - } - - #[inline] - #[allow(unused)] - pub fn fields(&self) -> &[ContextMacroPathField] { - &self.fields +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()? + .unwrap_or_else(|| TranslationPath::default()); + + 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, + is_pub, + ident, + ty + }) } } -impl ContextMacroPathField { - #[inline] - #[allow(unused)] - pub fn is_pub(&self) -> bool { - self.is_pub - } +impl Parse for ContextMacroStruct { + fn parse(input: ParseStream) -> SynResult { + let structure = input.parse::()?; - #[inline] - #[allow(unused)] - pub fn path(&self) -> &TranslationPath { - &self.path - } + let is_pub = structure.vis; + let ident = structure.ident; - #[inline] - #[allow(unused)] - pub fn ident(&self) -> &str { - &self.ident + let fields = structure + .fields + .into_iter() + .map(|field| ContextMacroField::try_from(field)) + .collect::, _>>()?; + + Ok(Self { + is_pub, + ident, + fields + }) } } diff --git a/translatable_proc/src/macro_input/translation.rs b/translatable_proc/src/macro_input/translation.rs index 9bb86b4..a6293db 100644 --- a/translatable_proc/src/macro_input/translation.rs +++ b/translatable_proc/src/macro_input/translation.rs @@ -26,7 +26,7 @@ use super::utils::translation_path::TranslationPath; /// macro input. This error is only used while parsing compile-time input, /// as runtime input is validated in runtime. #[derive(Error, Debug)] -enum TranslationMacroArgsError { +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")] @@ -81,7 +81,7 @@ impl Parse for TranslationMacroArgs { { Ok(language) => InputType::Static(language), - Err(_) => Err(TranslationMacroArgsError::InvalidIsoLiteral(literal.value()) + Err(_) => Err(MacroArgsError::InvalidIsoLiteral(literal.value()) .to_syn_error(literal))?, } }, diff --git a/translatable_proc/src/macro_input/utils/translation_path.rs b/translatable_proc/src/macro_input/utils/translation_path.rs index 5424170..8ea993f 100644 --- a/translatable_proc/src/macro_input/utils/translation_path.rs +++ b/translatable_proc/src/macro_input/utils/translation_path.rs @@ -32,6 +32,15 @@ impl Parse for TranslationPath { } } +impl Default for TranslationPath { + fn default() -> Self { + Self { + segments: Vec::new(), + span: Span::call_site() + } + } +} + impl TranslationPath { #[inline] #[allow(unused)] From c89320be903613cef79d3ab529f4b416050ac517 Mon Sep 17 00:00:00 2001 From: stifskere Date: Mon, 28 Apr 2025 03:40:40 +0200 Subject: [PATCH 188/228] feat(context): add getters and merge translation paths --- .../integration/context/context_macro.rs | 2 +- .../src/macro_generation/context.rs | 26 ++--- translatable_proc/src/macro_input/context.rs | 94 +++++++++++++------ .../src/macro_input/translation.rs | 31 +++--- .../src/macro_input/utils/translation_path.rs | 11 ++- 5 files changed, 98 insertions(+), 66 deletions(-) diff --git a/translatable/tests/integration/context/context_macro.rs b/translatable/tests/integration/context/context_macro.rs index 16965e7..11fdae1 100644 --- a/translatable/tests/integration/context/context_macro.rs +++ b/translatable/tests/integration/context/context_macro.rs @@ -1,4 +1,4 @@ -use translatable::{translation_context, Language}; +use translatable::{Language, translation_context}; #[translation_context(base::path)] pub struct TestContext { diff --git a/translatable_proc/src/macro_generation/context.rs b/translatable_proc/src/macro_generation/context.rs index be12a34..93f4746 100644 --- a/translatable_proc/src/macro_generation/context.rs +++ b/translatable_proc/src/macro_generation/context.rs @@ -9,16 +9,14 @@ use crate::data::translations::load_translations; use crate::macro_input::context::{ContextMacroArgs, ContextMacroStruct}; #[derive(Error, Debug)] -enum MacroCompileError { - -} +enum MacroCompileError {} macro_rules! pub_token { ($input:expr) => { if $input { quote! { pub } } else { - quote! { } + quote! {} } }; } @@ -37,10 +35,8 @@ pub fn context_macro(base_path: ContextMacroArgs, macro_input: ContextMacroStruc let translations = macro_input .fields() .iter() - .map(|field| ( - field.is_pub(), - field.ident(), - { + .map(|field| { + (field.is_pub(), field.ident(), { let path_segments = field .path() .segments() @@ -52,8 +48,8 @@ pub fn context_macro(base_path: ContextMacroArgs, macro_input: ContextMacroStruc .collect(); translations.find_path(&path) - } - )) + }) + }) .collect::>(); let struct_fields = macro_input @@ -62,10 +58,7 @@ pub fn context_macro(base_path: ContextMacroArgs, macro_input: ContextMacroStruc .map(|field| { let pub_token = pub_token!(field.is_pub()); - let ident = Ident::new( - field.ident(), - Span::call_site() - ); + let ident = Ident::new(field.ident(), Span::call_site()); quote! { #pub_token #ident: String } }); @@ -75,10 +68,7 @@ pub fn context_macro(base_path: ContextMacroArgs, macro_input: ContextMacroStruc .map(|(is_pub, ident, translation)| { let pub_token = pub_token!(*is_pub); - let ident = Ident::new( - ident, - Span::call_site() - ); + let ident = Ident::new(ident, Span::call_site()); let templated_ident = format_ident!("templated_{ident}"); diff --git a/translatable_proc/src/macro_input/context.rs b/translatable_proc/src/macro_input/context.rs index f467f14..076a10b 100644 --- a/translatable_proc/src/macro_input/context.rs +++ b/translatable_proc/src/macro_input/context.rs @@ -1,4 +1,5 @@ -use syn::{parse::{Parse, ParseStream}, Field, Ident, ItemStruct, Result as SynResult, Type, Visibility, Error as SynError}; +use syn::parse::{Parse, ParseStream}; +use syn::{Error as SynError, Field, Ident, ItemStruct, Result as SynResult, Type, Visibility}; use thiserror::Error; use translatable_shared::macros::errors::IntoCompileError; @@ -7,7 +8,7 @@ use super::utils::translation_path::TranslationPath; #[derive(Error, Debug)] enum MacroArgsError { #[error("Only named fields are allowed")] - InvalidFieldType + InvalidFieldType, } pub struct ContextMacroArgs(Option); @@ -16,24 +17,44 @@ pub struct ContextMacroField { path: TranslationPath, is_pub: Visibility, ident: Ident, - ty: Type + ty: Type, } pub struct ContextMacroStruct { is_pub: Visibility, ident: Ident, - fields: Vec + fields: Vec, } impl Parse for ContextMacroArgs { fn parse(input: ParseStream) -> SynResult { - Ok(Self( - if !input.is_empty() { - Some(input.parse::()?) - } else { - None - } - )) + Ok(Self(if !input.is_empty() { Some(input.parse::()?) } else { None })) + } +} + +impl ContextMacroField { + #[inline] + #[allow(unused)] + pub fn path(&self) -> &TranslationPath { + &self.path + } + + #[inline] + #[allow(unused)] + pub fn is_pub(&self) -> &Visibility { + &self.is_pub + } + + #[inline] + #[allow(unused)] + pub fn ident(&self) -> &Ident { + &self.ident + } + + #[inline] + #[allow(unused)] + pub fn ty(&self) -> &Type { + &self.ty } } @@ -44,7 +65,11 @@ impl TryFrom for ContextMacroField { let path = field .attrs .iter() - .find(|field| field.path().is_ident("path")) + .find(|field| { + field + .path() + .is_ident("path") + }) .map(|field| field.parse_args::()) .transpose()? .unwrap_or_else(|| TranslationPath::default()); @@ -56,20 +81,31 @@ impl TryFrom for ContextMacroField { let ident = field .ident .clone() - .ok_or( - MacroArgsError::InvalidFieldType - .to_syn_error(&field) - )?; - - let ty = field - .ty; - - Ok(Self { - path, - is_pub, - ident, - ty - }) + .ok_or(MacroArgsError::InvalidFieldType.to_syn_error(&field))?; + + let ty = field.ty; + + Ok(Self { path, is_pub, ident, ty }) + } +} + +impl ContextMacroStruct { + #[inline] + #[allow(unused)] + pub fn is_pub(&self) -> &Visibility { + &self.is_pub + } + + #[inline] + #[allow(unused)] + pub fn ident(&self) -> &Ident { + &self.ident + } + + #[inline] + #[allow(unused)] + pub fn fields(&self) -> &[ContextMacroField] { + &self.fields } } @@ -86,10 +122,6 @@ impl Parse for ContextMacroStruct { .map(|field| ContextMacroField::try_from(field)) .collect::, _>>()?; - Ok(Self { - is_pub, - ident, - fields - }) + Ok(Self { is_pub, ident, fields }) } } diff --git a/translatable_proc/src/macro_input/translation.rs b/translatable_proc/src/macro_input/translation.rs index a6293db..86de4c1 100644 --- a/translatable_proc/src/macro_input/translation.rs +++ b/translatable_proc/src/macro_input/translation.rs @@ -73,21 +73,22 @@ pub struct TranslationMacroArgs { /// 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()), - }; + 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::()?; diff --git a/translatable_proc/src/macro_input/utils/translation_path.rs b/translatable_proc/src/macro_input/utils/translation_path.rs index 8ea993f..bdeeb86 100644 --- a/translatable_proc/src/macro_input/utils/translation_path.rs +++ b/translatable_proc/src/macro_input/utils/translation_path.rs @@ -36,12 +36,21 @@ impl Default for TranslationPath { fn default() -> Self { Self { segments: Vec::new(), - span: Span::call_site() + span: Span::call_site(), } } } impl TranslationPath { + // TODO: merge spans (not yet in #19) + pub fn merge(&self, other: &TranslationPath) -> Vec { + [ + self.segments().to_vec(), + other.segments().to_vec() + ] + .concat() + } + #[inline] #[allow(unused)] pub fn segments(&self) -> &Vec { From c605b52e4f48975d4b6cbe2a4fa834650ccb5e3d Mon Sep 17 00:00:00 2001 From: stifskere Date: Tue, 29 Apr 2025 05:09:25 +0200 Subject: [PATCH 189/228] feat(context): parse and generate whole macro --- .../integration/context/context_macro.rs | 21 ++- translatable/tests/unitary/templating.rs | 10 +- .../tests/unitary/translation_collection.rs | 2 +- .../src/macro_generation/context.rs | 131 ++++++++---------- .../src/macro_generation/translation.rs | 4 +- translatable_proc/src/macro_input/context.rs | 22 +-- translatable_shared/src/misc/templating.rs | 6 +- 7 files changed, 100 insertions(+), 96 deletions(-) diff --git a/translatable/tests/integration/context/context_macro.rs b/translatable/tests/integration/context/context_macro.rs index 11fdae1..73f4024 100644 --- a/translatable/tests/integration/context/context_macro.rs +++ b/translatable/tests/integration/context/context_macro.rs @@ -1,12 +1,25 @@ +use std::collections::HashMap; + use translatable::{Language, translation_context}; -#[translation_context(base::path)] +#[translation_context(greetings)] pub struct TestContext { - pub xd: path::to::translation, - lol: path::to::other_translation, + #[path(formal)] + pub formal: String, + #[path(informal)] + informal: String, } #[test] fn test() { - TestContext::lol(&Language::ES); + let translations = TestContext::load_translations( + Language::ES, + &HashMap::from([ + ("user", "John") + ]) + ) + .expect("Translations should be able to load"); + + assert_eq!(translations.informal, "Hey John, todo bien?"); + assert_eq!(translations.formal, "Bueno conocerte.") } diff --git a/translatable/tests/unitary/templating.rs b/translatable/tests/unitary/templating.rs index 921a80a..2b7a673 100644 --- a/translatable/tests/unitary/templating.rs +++ b/translatable/tests/unitary/templating.rs @@ -7,7 +7,7 @@ use translatable_shared::misc::templating::FormatString; pub fn does_not_replace_not_found() { let result = FormatString::from_str("Hello {name}") .expect("Format string to be valid.") - .replace_with(HashMap::new()); + .replace_with(&HashMap::new()); assert_eq!(result, "Hello {name}"); } @@ -16,7 +16,7 @@ pub fn does_not_replace_not_found() { 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())])); + .replace_with(&HashMap::from([("name".into(), "Josh".into())])); assert_eq!(result, "Hello Josh"); } @@ -25,7 +25,7 @@ pub fn replaces_single_template() { 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([ + .replace_with(&HashMap::from([ ("name".into(), "Josh".into()), ("day".into(), "today".into()), ])); @@ -37,7 +37,7 @@ pub fn replaces_multiple_templates() { 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())])); + .replace_with(&HashMap::from([("name".into(), "Josh".into())])); assert_eq!(result, "Hello Josh how are you doing {day}?"); } @@ -53,7 +53,7 @@ pub fn fails_unclosed_template() { 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())])); + .replace_with(&HashMap::from([("this".into(), "not replaced".into())])); assert_eq!(result, "You write escaped templates like {{ this }}.") } diff --git a/translatable/tests/unitary/translation_collection.rs b/translatable/tests/unitary/translation_collection.rs index 5aaa1d2..4bb06ae 100644 --- a/translatable/tests/unitary/translation_collection.rs +++ b/translatable/tests/unitary/translation_collection.rs @@ -49,7 +49,7 @@ pub fn loads_and_finds_collection() { .expect("Translation to be found.") .get(&Language::ES) .expect("Language to be available.") - .replace_with(HashMap::new()); + .replace_with(&HashMap::new()); assert_eq!(translation, "Hola"); } diff --git a/translatable_proc/src/macro_generation/context.rs b/translatable_proc/src/macro_generation/context.rs index 93f4746..b1fe32d 100644 --- a/translatable_proc/src/macro_generation/context.rs +++ b/translatable_proc/src/macro_generation/context.rs @@ -1,104 +1,85 @@ -use proc_macro2::{Span, TokenStream as TokenStream2}; -use quote::{format_ident, quote}; -use syn::Ident; +use proc_macro2::TokenStream as TokenStream2; +use quote::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}; +use crate::macro_input::utils::translation_path::TranslationPath; #[derive(Error, Debug)] -enum MacroCompileError {} - -macro_rules! pub_token { - ($input:expr) => { - if $input { - quote! { pub } - } else { - quote! {} - } - }; +enum MacroCompileError { + #[error("A translation with the path '{0}' could not be found.")] + TranslationNotFound(String) } pub fn context_macro(base_path: ContextMacroArgs, macro_input: ContextMacroStruct) -> TokenStream2 { let translations = handle_macro_result!(load_translations()); + let base_path = base_path.into_inner().unwrap_or_else(|| TranslationPath::default()); - let pub_token = pub_token!(macro_input.is_pub()); - let struct_ident = Ident::new(macro_input.ident(), Span::call_site()); - - let base_path = base_path - .or_empty() - .segments() - .to_vec(); - - let translations = macro_input - .fields() - .iter() - .map(|field| { - (field.is_pub(), field.ident(), { - let path_segments = field - .path() - .segments() - .to_vec(); - - let path = base_path - .iter() - .chain(&path_segments) - .collect(); - - translations.find_path(&path) - }) - }) - .collect::>(); + let struct_pub = macro_input.pub_state(); + let struct_ident = macro_input.ident(); let struct_fields = macro_input .fields() .iter() .map(|field| { - let pub_token = pub_token!(field.is_pub()); - - let ident = Ident::new(field.ident(), Span::call_site()); - - quote! { #pub_token #ident: String } + let field_ident = field.ident(); + quote! { #field_ident: String } }); - let field_impls = translations - .iter() - .map(|(is_pub, ident, translation)| { - let pub_token = pub_token!(*is_pub); - - let ident = Ident::new(ident, Span::call_site()); - - let templated_ident = format_ident!("templated_{ident}"); - - let translation = translation - .map(|translation| map_to_tokens(translation)) - .ok_or(); - - quote! { - #[inline] - #pub_token fn #templated_ident(language: &translatable::Language) - -> Option { - #translation - .remove(language) - } - - #[inline] - #pub_token fn #ident(language: &translatable::Language) -> Option { - Self::#templated_ident(language) - .map(|lang| lang.replace_with(std::collections::HashMap::new())) - } - } - }); + let loadable_translations = handle_macro_result!( + macro_input + .fields() + .iter() + .map(|field| { + let path_segments = base_path + .merge(field.path()); + + let path_segments_display = path_segments + .join("::"); + + let translation = map_to_tokens( + translations + .find_path(&path_segments) + .ok_or(MacroCompileError::TranslationNotFound(path_segments.join("::")))?, + ); + + let ident = field.ident(); + + Ok(quote! { + #ident: #translation + .get(&language) + .ok_or_else(|| translatable::Error::LanguageNotAvailable( + language.clone(), + #path_segments_display.to_string() + ))? + .replace_with(&replacements) + }) + }) + .collect::, MacroCompileError>>() + ); quote! { - #pub_token struct #struct_ident { + #struct_pub struct #struct_ident { #(#struct_fields),* } impl #struct_ident { - #(#field_impls)* + #struct_pub fn load_translations( + language: translatable::Language, + replacements: &std::collections::HashMap + ) -> Result { + let replacements = replacements + .iter() + .map(|(key, value)| (key.to_string(), value.to_string())) + .collect::>(); + + Ok(Self { + #(#loadable_translations),* + }) + } } } } diff --git a/translatable_proc/src/macro_generation/translation.rs b/translatable_proc/src/macro_generation/translation.rs index d01af68..a4e4a15 100644 --- a/translatable_proc/src/macro_generation/translation.rs +++ b/translatable_proc/src/macro_generation/translation.rs @@ -96,7 +96,7 @@ pub fn translation_macro(input: TranslationMacroArgs) -> TokenStream2 { return quote! { #translation - .replace_with(#template_replacements) + .replace_with(&#template_replacements) }; } } @@ -152,7 +152,7 @@ pub fn translation_macro(input: TranslationMacroArgs) -> TokenStream2 { #translation_object .get(&language) .ok_or_else(|| translatable::Error::LanguageNotAvailable(language, path.join("::")))? - .replace_with(#template_replacements) + .replace_with(&#template_replacements) }) })() } diff --git a/translatable_proc/src/macro_input/context.rs b/translatable_proc/src/macro_input/context.rs index 076a10b..9893066 100644 --- a/translatable_proc/src/macro_input/context.rs +++ b/translatable_proc/src/macro_input/context.rs @@ -15,17 +15,23 @@ pub struct ContextMacroArgs(Option); pub struct ContextMacroField { path: TranslationPath, - is_pub: Visibility, + pub_state: Visibility, ident: Ident, ty: Type, } pub struct ContextMacroStruct { - is_pub: Visibility, + pub_state: Visibility, ident: Ident, fields: Vec, } +impl ContextMacroArgs { + pub fn into_inner(self) -> Option { + self.0 + } +} + impl Parse for ContextMacroArgs { fn parse(input: ParseStream) -> SynResult { Ok(Self(if !input.is_empty() { Some(input.parse::()?) } else { None })) @@ -41,8 +47,8 @@ impl ContextMacroField { #[inline] #[allow(unused)] - pub fn is_pub(&self) -> &Visibility { - &self.is_pub + pub fn pub_state(&self) -> &Visibility { + &self.pub_state } #[inline] @@ -85,15 +91,15 @@ impl TryFrom for ContextMacroField { let ty = field.ty; - Ok(Self { path, is_pub, ident, ty }) + Ok(Self { path, pub_state: is_pub, ident, ty }) } } impl ContextMacroStruct { #[inline] #[allow(unused)] - pub fn is_pub(&self) -> &Visibility { - &self.is_pub + pub fn pub_state(&self) -> &Visibility { + &self.pub_state } #[inline] @@ -122,6 +128,6 @@ impl Parse for ContextMacroStruct { .map(|field| ContextMacroField::try_from(field)) .collect::, _>>()?; - Ok(Self { is_pub, ident, fields }) + Ok(Self { pub_state: is_pub, ident, fields }) } } diff --git a/translatable_shared/src/misc/templating.rs b/translatable_shared/src/misc/templating.rs index b66183d..70b5cb4 100644 --- a/translatable_shared/src/misc/templating.rs +++ b/translatable_shared/src/misc/templating.rs @@ -104,7 +104,7 @@ impl FormatString { /// /// **Returns** /// A copy of the original string with it's templates replaced. - pub fn replace_with(&self, values: HashMap) -> String { + pub fn replace_with(&self, values: &HashMap) -> String { let mut original = self .original .clone(); @@ -129,6 +129,10 @@ impl FormatString { original } + + pub fn original(&self) -> &str { + &self.original + } } /// Parse method implementation. From c797123ffbe84ce2cb2cd1178b62a86796452d69 Mon Sep 17 00:00:00 2001 From: stifskere Date: Tue, 29 Apr 2025 19:11:33 +0200 Subject: [PATCH 190/228] chore: sync --- .../integration/context/context_macro.rs | 1 - .../src/macro_generation/context.rs | 2 +- translatable_proc/src/macro_input/context.rs | 30 +++++++++++++++---- .../src/macro_input/utils/translation_path.rs | 5 ++++ 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/translatable/tests/integration/context/context_macro.rs b/translatable/tests/integration/context/context_macro.rs index 73f4024..292449f 100644 --- a/translatable/tests/integration/context/context_macro.rs +++ b/translatable/tests/integration/context/context_macro.rs @@ -6,7 +6,6 @@ use translatable::{Language, translation_context}; pub struct TestContext { #[path(formal)] pub formal: String, - #[path(informal)] informal: String, } diff --git a/translatable_proc/src/macro_generation/context.rs b/translatable_proc/src/macro_generation/context.rs index b1fe32d..3268abd 100644 --- a/translatable_proc/src/macro_generation/context.rs +++ b/translatable_proc/src/macro_generation/context.rs @@ -35,7 +35,7 @@ pub fn context_macro(base_path: ContextMacroArgs, macro_input: ContextMacroStruc .iter() .map(|field| { let path_segments = base_path - .merge(field.path()); + .merge(&field.path()); let path_segments_display = path_segments .join("::"); diff --git a/translatable_proc/src/macro_input/context.rs b/translatable_proc/src/macro_input/context.rs index 9893066..dd0c957 100644 --- a/translatable_proc/src/macro_input/context.rs +++ b/translatable_proc/src/macro_input/context.rs @@ -1,3 +1,5 @@ +use std::ops::Not; + use syn::parse::{Parse, ParseStream}; use syn::{Error as SynError, Field, Ident, ItemStruct, Result as SynResult, Type, Visibility}; use thiserror::Error; @@ -14,7 +16,7 @@ enum MacroArgsError { pub struct ContextMacroArgs(Option); pub struct ContextMacroField { - path: TranslationPath, + path: Option, pub_state: Visibility, ident: Ident, ty: Type, @@ -34,15 +36,32 @@ impl ContextMacroArgs { impl Parse for ContextMacroArgs { fn parse(input: ParseStream) -> SynResult { - Ok(Self(if !input.is_empty() { Some(input.parse::()?) } else { None })) + Ok(Self( + input + .is_empty() + .not() + .then(|| input.parse()) + .transpose()? + )) } } impl ContextMacroField { #[inline] #[allow(unused)] - pub fn path(&self) -> &TranslationPath { - &self.path + pub fn path(&self) -> TranslationPath { + self.path + .clone() + .unwrap_or_else(|| + TranslationPath::new( + vec![ + self.ident + .to_string() + ], + self.ident + .span() + ) + ) } #[inline] @@ -77,8 +96,7 @@ impl TryFrom for ContextMacroField { .is_ident("path") }) .map(|field| field.parse_args::()) - .transpose()? - .unwrap_or_else(|| TranslationPath::default()); + .transpose()?; let is_pub = field .vis diff --git a/translatable_proc/src/macro_input/utils/translation_path.rs b/translatable_proc/src/macro_input/utils/translation_path.rs index bdeeb86..ce83d08 100644 --- a/translatable_proc/src/macro_input/utils/translation_path.rs +++ b/translatable_proc/src/macro_input/utils/translation_path.rs @@ -3,6 +3,7 @@ use syn::parse::{Parse, ParseStream}; use syn::spanned::Spanned; use syn::{Error as SynError, Path, PathArguments, Result as SynResult}; +#[derive(Clone)] pub struct TranslationPath { segments: Vec, span: Span, @@ -42,6 +43,10 @@ impl Default for TranslationPath { } impl TranslationPath { + pub fn new(segments: Vec, span: Span) -> Self { + Self { segments, span } + } + // TODO: merge spans (not yet in #19) pub fn merge(&self, other: &TranslationPath) -> Vec { [ From bed4693a6f7879e5c3047787b6a18b77609e4ff1 Mon Sep 17 00:00:00 2001 From: stifskere Date: Tue, 29 Apr 2025 19:19:38 +0200 Subject: [PATCH 191/228] chore(meta): change code of conduct and add discord link in readme --- .github/ISSUE_TEMPLATE/BUG_REPORT.yml | 2 +- .github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml | 2 +- .github/ISSUE_TEMPLATE/config.yml | 10 ++++------ .github/pull_request_template.md | 8 ++++++-- README.md | 1 + 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml index 8cdcd53..ecd0b1d 100644 --- a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml @@ -9,7 +9,7 @@ body: options: - label: I've checked existing issues and pull requests required: true - - label: I've read the [Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct) + - label: I've read the [Code of Conduct](https://github.com/FlakySL/translatable/blob/main/CODE_OF_CONDUCT.md) required: true - label: Are you using the latest translatable version? required: true diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml index 687f118..8f32766 100644 --- a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml +++ b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml @@ -9,7 +9,7 @@ body: options: - label: I've checked existing issues and pull requests required: true - - label: I've read the [Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct) + - label: I've read the [Code of Conduct](https://github.com/FlakySL/translatable/blob/main/CODE_OF_CONDUCT.md) required: true - type: textarea diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 8588987..3c25e40 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -11,18 +11,16 @@ contact_links: *Please check existing issues first!* - name: "βš–οΈ Code of Conduct" - url: https://www.rust-lang.org/policies/code-of-conduct + url: https://github.com/FlakySL/translatable/blob/main/CODE_OF_CONDUCT.md about: | - All community interactions must follow: - - Rust's Code of Conduct - - Project-specific guidelines + All community interactions must follow the project + code of conduct, which is based on the contributor covenant. *Required reading before participating* - name: "🚨 Moderation Contact" - url: mailto:esteve@memw.es + url: mailto:moderation@flaky.es about: | For urgent moderation issues: - Code of Conduct violations - Community safety concerns - Escalation requests - *Do NOT contact Rust moderation team* diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index ae8eb47..9c1d668 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,11 +1,15 @@ + ## Pre-submission Checklist + - [ ] I've checked existing issues and pull requests -- [ ] I've read the [Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct) -- [ ] I've tested my changes +- [ ] 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 - [ ] I've listed all my changes in the `Changes` section ## Changes + - ## Linked Issues + - fixes # diff --git a/README.md b/README.md index 35f31f6..1224bf9 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ [![Downloads](https://badges.ws/crates/dt/translatable)](https://docs.rs/translatable) [![Codecov](https://img.shields.io/codecov/c/github/FlakySL/translatable)](https://app.codecov.io/gh/FlakySL/translatable) ![tests](https://github.com/FlakySL/translatable/actions/workflows/overall-coverage.yml/badge.svg) +[![discord](https://badges.ws/discord/online/793890238267260958)](https://discord.gg/AJWFyps23a) A robust internationalization solution for Rust featuring compile-time validation, ISO 639-1 compliance, and TOML-based translation management. From 8ac5b45ad9861d364674c034310b9b3e36ca77f0 Mon Sep 17 00:00:00 2001 From: stifskere Date: Tue, 29 Apr 2025 19:20:28 +0200 Subject: [PATCH 192/228] fix(meta): change server id --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1224bf9..a06b12a 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Downloads](https://badges.ws/crates/dt/translatable)](https://docs.rs/translatable) [![Codecov](https://img.shields.io/codecov/c/github/FlakySL/translatable)](https://app.codecov.io/gh/FlakySL/translatable) ![tests](https://github.com/FlakySL/translatable/actions/workflows/overall-coverage.yml/badge.svg) -[![discord](https://badges.ws/discord/online/793890238267260958)](https://discord.gg/AJWFyps23a) +[![discord](https://badges.ws/discord/online/1344769456731197450)](https://discord.gg/AJWFyps23a) A robust internationalization solution for Rust featuring compile-time validation, ISO 639-1 compliance, and TOML-based translation management. From 0e2b0d79f1ae9be46100cb53e3d875ee8ee8d961 Mon Sep 17 00:00:00 2001 From: stifskere Date: Wed, 30 Apr 2025 06:55:29 +0200 Subject: [PATCH 193/228] feat(context): parse context parameters --- .../integration/context/context_macro.rs | 2 +- .../src/macro_generation/context.rs | 7 +- translatable_proc/src/macro_input/context.rs | 80 ++++++++++++++++--- 3 files changed, 75 insertions(+), 14 deletions(-) diff --git a/translatable/tests/integration/context/context_macro.rs b/translatable/tests/integration/context/context_macro.rs index 292449f..08b53fe 100644 --- a/translatable/tests/integration/context/context_macro.rs +++ b/translatable/tests/integration/context/context_macro.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use translatable::{Language, translation_context}; -#[translation_context(greetings)] +#[translation_context(base_path = greetings, fallback_language = "en")] pub struct TestContext { #[path(formal)] pub formal: String, diff --git a/translatable_proc/src/macro_generation/context.rs b/translatable_proc/src/macro_generation/context.rs index 3268abd..3e1e4f5 100644 --- a/translatable_proc/src/macro_generation/context.rs +++ b/translatable_proc/src/macro_generation/context.rs @@ -14,9 +14,12 @@ enum MacroCompileError { TranslationNotFound(String) } -pub fn context_macro(base_path: ContextMacroArgs, macro_input: ContextMacroStruct) -> TokenStream2 { +pub fn context_macro(macro_args: ContextMacroArgs, macro_input: ContextMacroStruct) -> TokenStream2 { let translations = handle_macro_result!(load_translations()); - let base_path = base_path.into_inner().unwrap_or_else(|| TranslationPath::default()); + let base_path = macro_args + .base_path() + .cloned() + .unwrap_or_else(|| TranslationPath::default()); let struct_pub = macro_input.pub_state(); let struct_ident = macro_input.ident(); diff --git a/translatable_proc/src/macro_input/context.rs b/translatable_proc/src/macro_input/context.rs index dd0c957..348bccb 100644 --- a/translatable_proc/src/macro_input/context.rs +++ b/translatable_proc/src/macro_input/context.rs @@ -1,9 +1,12 @@ use std::ops::Not; +use std::str::FromStr; +use quote::ToTokens; use syn::parse::{Parse, ParseStream}; -use syn::{Error as SynError, Field, Ident, ItemStruct, Result as SynResult, Type, Visibility}; +use syn::{parse2, Error as SynError, Expr, ExprLit, Field, Ident, ItemStruct, Lit, MetaNameValue, Result as SynResult, Token, Type, Visibility}; use thiserror::Error; use translatable_shared::macros::errors::IntoCompileError; +use translatable_shared::misc::language::Language; use super::utils::translation_path::TranslationPath; @@ -11,9 +14,21 @@ use super::utils::translation_path::TranslationPath; enum MacroArgsError { #[error("Only named fields are allowed")] InvalidFieldType, + + #[error("Only a language literal is allowed")] + OnlyLangLiteralAllowed, + + #[error("Invalid language literal '{0}' is not a valid ISO-639-1 language")] + InvalidLanguageLiteral(String), + + #[error("Unknown key '{0}', allowed keys are 'fallback_language' and 'base_path'")] + UnknownKey(String) } -pub struct ContextMacroArgs(Option); +pub struct ContextMacroArgs{ + base_path: Option, + fallback_language: Option +} pub struct ContextMacroField { path: Option, @@ -29,20 +44,63 @@ pub struct ContextMacroStruct { } impl ContextMacroArgs { - pub fn into_inner(self) -> Option { - self.0 + pub fn base_path(&self) -> Option<&TranslationPath> { + self.base_path.as_ref() + } + + pub fn fallback_language(&self) -> Option<&Language> { + self.fallback_language.as_ref() } } impl Parse for ContextMacroArgs { fn parse(input: ParseStream) -> SynResult { - Ok(Self( - input - .is_empty() - .not() - .then(|| input.parse()) - .transpose()? - )) + 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) + ); + } + } + } + + Ok(Self { + base_path, + fallback_language + }) } } From 11084004f8661ad41a4530d71176ef96400b96a4 Mon Sep 17 00:00:00 2001 From: stifskere Date: Wed, 30 Apr 2025 10:59:37 +0200 Subject: [PATCH 194/228] feat(context): implement fallback_language --- .../integration/context/context_macro.rs | 7 +- .../src/macro_generation/context.rs | 73 ++++++++++++++----- translatable_proc/src/macro_input/context.rs | 16 ++-- translatable_shared/src/macros/errors.rs | 21 +++++- 4 files changed, 86 insertions(+), 31 deletions(-) diff --git a/translatable/tests/integration/context/context_macro.rs b/translatable/tests/integration/context/context_macro.rs index 08b53fe..0846d42 100644 --- a/translatable/tests/integration/context/context_macro.rs +++ b/translatable/tests/integration/context/context_macro.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use translatable::{Language, translation_context}; -#[translation_context(base_path = greetings, fallback_language = "en")] +#[translation_context(base_path = greetings, fallback_language = "es")] pub struct TestContext { #[path(formal)] pub formal: String, @@ -12,12 +12,11 @@ pub struct TestContext { #[test] fn test() { let translations = TestContext::load_translations( - Language::ES, + Language::AA, &HashMap::from([ ("user", "John") ]) - ) - .expect("Translations should be able to load"); + ); assert_eq!(translations.informal, "Hey John, todo bien?"); assert_eq!(translations.formal, "Bueno conocerte.") diff --git a/translatable_proc/src/macro_generation/context.rs b/translatable_proc/src/macro_generation/context.rs index 3e1e4f5..c54dcde 100644 --- a/translatable_proc/src/macro_generation/context.rs +++ b/translatable_proc/src/macro_generation/context.rs @@ -6,20 +6,20 @@ use translatable_shared::macros::collections::map_to_tokens; use crate::data::translations::load_translations; use crate::macro_input::context::{ContextMacroArgs, ContextMacroStruct}; -use crate::macro_input::utils::translation_path::TranslationPath; #[derive(Error, Debug)] enum MacroCompileError { - #[error("A translation with the path '{0}' could not be found.")] - TranslationNotFound(String) + #[error("A translation with the path '{0}' could not be found")] + TranslationNotFound(String), + + #[error("One of the translations doesn't have the fallback language available")] + FallbackNotAvailable } pub fn context_macro(macro_args: ContextMacroArgs, macro_input: ContextMacroStruct) -> TokenStream2 { - let translations = handle_macro_result!(load_translations()); + let translations = handle_macro_result!(out load_translations()); let base_path = macro_args - .base_path() - .cloned() - .unwrap_or_else(|| TranslationPath::default()); + .base_path(); let struct_pub = macro_input.pub_state(); let struct_ident = macro_input.ident(); @@ -32,7 +32,7 @@ pub fn context_macro(macro_args: ContextMacroArgs, macro_input: ContextMacroStru quote! { #field_ident: String } }); - let loadable_translations = handle_macro_result!( + let loadable_translations = handle_macro_result!(out macro_input .fields() .iter() @@ -43,27 +43,62 @@ pub fn context_macro(macro_args: ContextMacroArgs, macro_input: ContextMacroStru let path_segments_display = path_segments .join("::"); - let translation = map_to_tokens( - translations - .find_path(&path_segments) - .ok_or(MacroCompileError::TranslationNotFound(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(); - Ok(quote! { - #ident: #translation - .get(&language) + 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),* @@ -73,15 +108,13 @@ pub fn context_macro(macro_args: ContextMacroArgs, macro_input: ContextMacroStru #struct_pub fn load_translations( language: translatable::Language, replacements: &std::collections::HashMap - ) -> Result { + ) -> #load_ret_ty { let replacements = replacements .iter() .map(|(key, value)| (key.to_string(), value.to_string())) .collect::>(); - Ok(Self { - #(#loadable_translations),* - }) + #load_ret_stmnt } } } diff --git a/translatable_proc/src/macro_input/context.rs b/translatable_proc/src/macro_input/context.rs index 348bccb..ffe9340 100644 --- a/translatable_proc/src/macro_input/context.rs +++ b/translatable_proc/src/macro_input/context.rs @@ -1,4 +1,3 @@ -use std::ops::Not; use std::str::FromStr; use quote::ToTokens; @@ -44,12 +43,19 @@ pub struct ContextMacroStruct { } impl ContextMacroArgs { - pub fn base_path(&self) -> Option<&TranslationPath> { - self.base_path.as_ref() + #[inline] + #[allow(unused)] + pub fn base_path(&self) -> TranslationPath { + self.base_path + .clone() + .unwrap_or_else(|| TranslationPath::default()) } - pub fn fallback_language(&self) -> Option<&Language> { - self.fallback_language.as_ref() + #[inline] + #[allow(unused)] + pub fn fallback_language(&self) -> Option { + self.fallback_language + .clone() } } diff --git a/translatable_shared/src/macros/errors.rs b/translatable_shared/src/macros/errors.rs index dc63564..088d267 100644 --- a/translatable_shared/src/macros/errors.rs +++ b/translatable_shared/src/macros/errors.rs @@ -27,6 +27,10 @@ where /// 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] @@ -35,6 +39,11 @@ where quote! { std::compile_error!(#message) } } + 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 @@ -69,12 +78,20 @@ impl IntoCompileError for T {} /// [`to_compile_error`]: IntoCompileError::to_compile_error #[macro_export] macro_rules! handle_macro_result { - ($val:expr) => {{ + ($method:ident; $val:expr) => {{ use $crate::macros::errors::IntoCompileError; match $val { std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => return error.to_compile_error(), + 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) + }; } From 10f8e4d5868a17ab6b23c17984b29c64e0feb8a0 Mon Sep 17 00:00:00 2001 From: stifskere Date: Wed, 30 Apr 2025 11:07:59 +0200 Subject: [PATCH 195/228] feat(coverage): set 80% --- codecov.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 codecov.yml diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..0cd8828 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,8 @@ +coverage: + status: + project: + default: + target: 80% + threshold: 1% + if_not_found: success + informational: false From cfaa19fff38b1dc7a8a17423cbb487b63521a6ab Mon Sep 17 00:00:00 2001 From: stifskere Date: Wed, 30 Apr 2025 11:11:21 +0200 Subject: [PATCH 196/228] feat(coverage): remove config --- codecov.yml | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 codecov.yml diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index 0cd8828..0000000 --- a/codecov.yml +++ /dev/null @@ -1,8 +0,0 @@ -coverage: - status: - project: - default: - target: 80% - threshold: 1% - if_not_found: success - informational: false From 3007becbf7db087f1b39d2a99bade4be401799d3 Mon Sep 17 00:00:00 2001 From: stifskere Date: Wed, 30 Apr 2025 11:11:47 +0200 Subject: [PATCH 197/228] feat(coverage): set 80% --- codecov.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 codecov.yml diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..0cd8828 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,8 @@ +coverage: + status: + project: + default: + target: 80% + threshold: 1% + if_not_found: success + informational: false From ac7913e8ba4e0d22d854be2f191b28c63d625d3b Mon Sep 17 00:00:00 2001 From: stifskere Date: Wed, 30 Apr 2025 11:19:30 +0200 Subject: [PATCH 198/228] feat(coverage): set 80% to both patch and project --- codecov.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/codecov.yml b/codecov.yml index 0cd8828..df894dd 100644 --- a/codecov.yml +++ b/codecov.yml @@ -3,6 +3,6 @@ coverage: project: default: target: 80% - threshold: 1% - if_not_found: success - informational: false + patch: + default: + target: 80% From e370996b781ff42be575c1648d84e2064a042046 Mon Sep 17 00:00:00 2001 From: stifskere Date: Fri, 2 May 2025 11:15:08 +0200 Subject: [PATCH 199/228] docs(context): document translation path abstraction --- .../src/macro_input/utils/translation_path.rs | 83 ++++++++++++++++++- .../src/translations/collection.rs | 8 +- 2 files changed, 85 insertions(+), 6 deletions(-) diff --git a/translatable_proc/src/macro_input/utils/translation_path.rs b/translatable_proc/src/macro_input/utils/translation_path.rs index ce83d08..c66a335 100644 --- a/translatable_proc/src/macro_input/utils/translation_path.rs +++ b/translatable_proc/src/macro_input/utils/translation_path.rs @@ -1,14 +1,49 @@ +//! [`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::()?; @@ -33,6 +68,13 @@ impl Parse for TranslationPath { } } +/// 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 { @@ -43,12 +85,40 @@ impl Default for TranslationPath { } 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 } } - // TODO: merge spans (not yet in #19) - pub fn merge(&self, other: &TranslationPath) -> Vec { + /// 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() @@ -56,15 +126,24 @@ impl TranslationPath { .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_shared/src/translations/collection.rs b/translatable_shared/src/translations/collection.rs index f05b665..852be95 100644 --- a/translatable_shared/src/translations/collection.rs +++ b/translatable_shared/src/translations/collection.rs @@ -47,10 +47,10 @@ impl TranslationNodeCollection { /// independently, if you are looking for an independent /// translation you may want to call find_path instead. /// - /// # Arguments + /// **Arguments** /// * `path` - The OS path where the file was originally found. /// - /// # Returns + /// **Returns** /// A top level translation node, containing all the translations /// in that specific file. #[allow(unused)] @@ -66,11 +66,11 @@ impl TranslationNodeCollection { /// of the necessary TOML object path to reach a specific /// translation object. /// - /// # Arguments + /// **Arguments** /// * `path` - The sections of the TOML path in order to access /// the desired translation object. /// - /// # Returns + /// **Returns** /// A translation object containing a specific translation /// in all it's available languages. pub fn find_path(&self, path: &Vec) -> Option<&TranslationObject> { From 904d55210ea57beb294b2db4c216b6df6a9dd822 Mon Sep 17 00:00:00 2001 From: stifskere Date: Fri, 2 May 2025 11:15:47 +0200 Subject: [PATCH 200/228] chore: apply cargo fmt --- .../integration/context/context_macro.rs | 8 +- .../src/macro_generation/context.rs | 14 ++-- translatable_proc/src/macro_input/context.rs | 74 +++++++++++-------- .../src/macro_input/utils/translation_path.rs | 9 ++- 4 files changed, 59 insertions(+), 46 deletions(-) diff --git a/translatable/tests/integration/context/context_macro.rs b/translatable/tests/integration/context/context_macro.rs index 0846d42..e7ea8f8 100644 --- a/translatable/tests/integration/context/context_macro.rs +++ b/translatable/tests/integration/context/context_macro.rs @@ -11,12 +11,8 @@ pub struct TestContext { #[test] fn test() { - let translations = TestContext::load_translations( - Language::AA, - &HashMap::from([ - ("user", "John") - ]) - ); + let translations = + TestContext::load_translations(Language::AA, &HashMap::from([("user", "John")])); assert_eq!(translations.informal, "Hey John, todo bien?"); assert_eq!(translations.formal, "Bueno conocerte.") diff --git a/translatable_proc/src/macro_generation/context.rs b/translatable_proc/src/macro_generation/context.rs index c54dcde..857f841 100644 --- a/translatable_proc/src/macro_generation/context.rs +++ b/translatable_proc/src/macro_generation/context.rs @@ -13,13 +13,15 @@ enum MacroCompileError { TranslationNotFound(String), #[error("One of the translations doesn't have the fallback language available")] - FallbackNotAvailable + FallbackNotAvailable, } -pub fn context_macro(macro_args: ContextMacroArgs, macro_input: ContextMacroStruct) -> TokenStream2 { +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 base_path = macro_args.base_path(); let struct_pub = macro_input.pub_state(); let struct_ident = macro_input.ident(); @@ -77,7 +79,9 @@ pub fn context_macro(macro_args: ContextMacroArgs, macro_input: ContextMacroStru .collect::, MacroCompileError>>() ); - let is_lang_some = macro_args.fallback_language().is_some(); + let is_lang_some = macro_args + .fallback_language() + .is_some(); let load_ret_ty = if is_lang_some { quote! { Self } diff --git a/translatable_proc/src/macro_input/context.rs b/translatable_proc/src/macro_input/context.rs index ffe9340..220b3c8 100644 --- a/translatable_proc/src/macro_input/context.rs +++ b/translatable_proc/src/macro_input/context.rs @@ -2,7 +2,21 @@ use std::str::FromStr; use quote::ToTokens; use syn::parse::{Parse, ParseStream}; -use syn::{parse2, Error as SynError, Expr, ExprLit, Field, Ident, ItemStruct, Lit, MetaNameValue, Result as SynResult, Token, Type, Visibility}; +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; @@ -21,12 +35,12 @@ enum MacroArgsError { InvalidLanguageLiteral(String), #[error("Unknown key '{0}', allowed keys are 'fallback_language' and 'base_path'")] - UnknownKey(String) + UnknownKey(String), } -pub struct ContextMacroArgs{ +pub struct ContextMacroArgs { base_path: Option, - fallback_language: Option + fallback_language: Option, } pub struct ContextMacroField { @@ -66,47 +80,43 @@ impl Parse for ContextMacroArgs { let mut fallback_language = None; for kvp in values { - let key = kvp.path + let key = kvp + .path .to_token_stream() .to_string(); match key.as_str() { "base_path" => { - base_path = Some( - parse2::(kvp.value.to_token_stream())? - ); - } + 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) - )? + 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) - ); + return Err(MacroArgsError::OnlyLangLiteralAllowed.to_syn_error(kvp.value)); } - } + }, key => { - return Err( - MacroArgsError::UnknownKey(key.to_string()) - .to_syn_error(kvp.path) - ); - } + return Err(MacroArgsError::UnknownKey(key.to_string()).to_syn_error(kvp.path)); + }, } } - Ok(Self { - base_path, - fallback_language - }) + Ok(Self { base_path, fallback_language }) } } @@ -116,16 +126,16 @@ impl ContextMacroField { pub fn path(&self) -> TranslationPath { self.path .clone() - .unwrap_or_else(|| + .unwrap_or_else(|| { TranslationPath::new( vec![ self.ident - .to_string() + .to_string(), ], self.ident - .span() + .span(), ) - ) + }) } #[inline] diff --git a/translatable_proc/src/macro_input/utils/translation_path.rs b/translatable_proc/src/macro_input/utils/translation_path.rs index c66a335..560ca30 100644 --- a/translatable_proc/src/macro_input/utils/translation_path.rs +++ b/translatable_proc/src/macro_input/utils/translation_path.rs @@ -120,10 +120,13 @@ impl TranslationPath { pub fn merge(&self, other: &Self) -> Vec { // TODO: merge spans (not yet in #19) [ - self.segments().to_vec(), - other.segments().to_vec() + self.segments() + .to_vec(), + other + .segments() + .to_vec(), ] - .concat() + .concat() } /// Internal segments getter. From 2dd2756fea3e35959a3ab5e53e595ebef9e46608 Mon Sep 17 00:00:00 2001 From: stifskere Date: Fri, 2 May 2025 12:07:33 +0200 Subject: [PATCH 201/228] feat(context): limit macro allowed types --- .../src/macro_generation/context.rs | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/translatable_proc/src/macro_generation/context.rs b/translatable_proc/src/macro_generation/context.rs index 857f841..e3f28ff 100644 --- a/translatable_proc/src/macro_generation/context.rs +++ b/translatable_proc/src/macro_generation/context.rs @@ -1,5 +1,5 @@ use proc_macro2::TokenStream as TokenStream2; -use quote::quote; +use quote::{quote, ToTokens}; use thiserror::Error; use translatable_shared::handle_macro_result; use translatable_shared::macros::collections::map_to_tokens; @@ -14,6 +14,9 @@ enum MacroCompileError { #[error("One of the translations doesn't have the fallback language available")] FallbackNotAvailable, + + #[error("Only '_', 'String' and '&str' is allowed for translation contexts")] + TypeNotAllowed } pub fn context_macro( @@ -26,12 +29,27 @@ pub fn context_macro( let struct_pub = macro_input.pub_state(); let struct_ident = macro_input.ident(); - let struct_fields = macro_input - .fields() + 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 struct_fields = struct_fields .iter() .map(|field| { let field_ident = field.ident(); - quote! { #field_ident: String } + let field_pub_state = field.pub_state(); + quote! { #field_pub_state #field_ident: String } }); let loadable_translations = handle_macro_result!(out From d37addae5a78c394e0cd2438fb5d4a7d10ed22ca Mon Sep 17 00:00:00 2001 From: stifskere Date: Fri, 2 May 2025 12:07:52 +0200 Subject: [PATCH 202/228] chore: apply cargo fmt --- .../src/macro_generation/context.rs | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/translatable_proc/src/macro_generation/context.rs b/translatable_proc/src/macro_generation/context.rs index e3f28ff..bfd720b 100644 --- a/translatable_proc/src/macro_generation/context.rs +++ b/translatable_proc/src/macro_generation/context.rs @@ -1,5 +1,5 @@ use proc_macro2::TokenStream as TokenStream2; -use quote::{quote, ToTokens}; +use quote::{ToTokens, quote}; use thiserror::Error; use translatable_shared::handle_macro_result; use translatable_shared::macros::collections::map_to_tokens; @@ -16,7 +16,7 @@ enum MacroCompileError { FallbackNotAvailable, #[error("Only '_', 'String' and '&str' is allowed for translation contexts")] - TypeNotAllowed + TypeNotAllowed, } pub fn context_macro( @@ -30,19 +30,19 @@ pub fn context_macro( 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::, _>>() - ); + 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 struct_fields = struct_fields .iter() From d0d557ffeab6456d577a3be9a47b819923cc0ddb Mon Sep 17 00:00:00 2001 From: stifskere Date: Fri, 2 May 2025 12:13:04 +0200 Subject: [PATCH 203/228] feat(context): abstracted the field generation with ToTokens implementation --- translatable_proc/src/macro_generation/context.rs | 8 -------- translatable_proc/src/macro_input/context.rs | 15 ++++++++++++++- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/translatable_proc/src/macro_generation/context.rs b/translatable_proc/src/macro_generation/context.rs index bfd720b..94c59df 100644 --- a/translatable_proc/src/macro_generation/context.rs +++ b/translatable_proc/src/macro_generation/context.rs @@ -44,14 +44,6 @@ pub fn context_macro( .collect::, _>>() ); - let struct_fields = struct_fields - .iter() - .map(|field| { - let field_ident = field.ident(); - let field_pub_state = field.pub_state(); - quote! { #field_pub_state #field_ident: String } - }); - let loadable_translations = handle_macro_result!(out macro_input .fields() diff --git a/translatable_proc/src/macro_input/context.rs b/translatable_proc/src/macro_input/context.rs index 220b3c8..da08cbf 100644 --- a/translatable_proc/src/macro_input/context.rs +++ b/translatable_proc/src/macro_input/context.rs @@ -1,6 +1,7 @@ use std::str::FromStr; -use quote::ToTokens; +use proc_macro2::TokenStream; +use quote::{quote, ToTokens, TokenStreamExt}; use syn::parse::{Parse, ParseStream}; use syn::{ Error as SynError, @@ -157,6 +158,18 @@ impl ContextMacroField { } } +impl ToTokens for ContextMacroField { + fn to_tokens(&self, tokens: &mut TokenStream) { + let pub_state = self.pub_state(); + let ident = self.ident(); + let ty = self.ty(); + + tokens.append_all(quote! { + #pub_state #ident: #ty + }); + } +} + impl TryFrom for ContextMacroField { type Error = SynError; From 956c347ba9748d908f9fc6bae6f1a5df3f5ae422 Mon Sep 17 00:00:00 2001 From: stifskere Date: Fri, 2 May 2025 15:08:34 +0200 Subject: [PATCH 204/228] fix(context): disallow use of _ as it's damned useless --- .../src/macro_generation/context.rs | 26 +++++++++---------- translatable_proc/src/macro_input/context.rs | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/translatable_proc/src/macro_generation/context.rs b/translatable_proc/src/macro_generation/context.rs index 94c59df..fc57ad4 100644 --- a/translatable_proc/src/macro_generation/context.rs +++ b/translatable_proc/src/macro_generation/context.rs @@ -15,7 +15,7 @@ enum MacroCompileError { #[error("One of the translations doesn't have the fallback language available")] FallbackNotAvailable, - #[error("Only '_', 'String' and '&str' is allowed for translation contexts")] + #[error("Only String' and '&str' is allowed for translation contexts")] TypeNotAllowed, } @@ -30,18 +30,18 @@ pub fn context_macro( 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::, _>>() + 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 diff --git a/translatable_proc/src/macro_input/context.rs b/translatable_proc/src/macro_input/context.rs index da08cbf..28486ef 100644 --- a/translatable_proc/src/macro_input/context.rs +++ b/translatable_proc/src/macro_input/context.rs @@ -1,7 +1,7 @@ use std::str::FromStr; use proc_macro2::TokenStream; -use quote::{quote, ToTokens, TokenStreamExt}; +use quote::{ToTokens, TokenStreamExt, quote}; use syn::parse::{Parse, ParseStream}; use syn::{ Error as SynError, From 2168888f47ffc421795d8e409e2574c69039c345 Mon Sep 17 00:00:00 2001 From: chikof Date: Sat, 3 May 2025 12:08:10 +0100 Subject: [PATCH 205/228] feat: remove serde Switch from toml to toml_edit, this change removes the overhead of using serde as a deserializer since toml_edit does not necessarily use serde under the hood for this. --- Cargo.lock | 14 ++-- translatable/Cargo.toml | 2 +- .../tests/unitary/translation_collection.rs | 6 +- translatable_proc/Cargo.toml | 2 +- translatable_proc/src/data/config.rs | 5 +- translatable_proc/src/data/translations.rs | 5 +- translatable_shared/Cargo.toml | 2 +- translatable_shared/src/translations/node.rs | 69 ++++++++++++++++--- 8 files changed, 76 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 71117ee..64a5d7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -210,9 +210,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.25" +version = "0.22.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10558ed0bd2a1562e630926a2d1f0b98c827da99fabd3fe20920a59642504485" +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" dependencies = [ "indexmap", "serde", @@ -224,9 +224,9 @@ dependencies = [ [[package]] name = "toml_write" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28391a4201ba7eb1984cfeb6862c0b3ea2cfe23332298967c749dddc0d6cd976" +checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" [[package]] name = "translatable" @@ -234,7 +234,7 @@ version = "1.0.0" dependencies = [ "quote", "thiserror", - "toml", + "toml_edit", "translatable_proc", "translatable_shared", "trybuild", @@ -249,7 +249,7 @@ dependencies = [ "strum", "syn", "thiserror", - "toml", + "toml_edit", "translatable_shared", ] @@ -262,7 +262,7 @@ dependencies = [ "strum", "syn", "thiserror", - "toml", + "toml_edit", ] [[package]] diff --git a/translatable/Cargo.toml b/translatable/Cargo.toml index 861ae8f..d146e2f 100644 --- a/translatable/Cargo.toml +++ b/translatable/Cargo.toml @@ -22,5 +22,5 @@ translatable_shared = { version = "1", path = "../translatable_shared/" } [dev-dependencies] quote = "1.0.40" -toml = "0.8.21" +toml_edit = "0.22.26" trybuild = "1.0.104" diff --git a/translatable/tests/unitary/translation_collection.rs b/translatable/tests/unitary/translation_collection.rs index 5aaa1d2..93b0d27 100644 --- a/translatable/tests/unitary/translation_collection.rs +++ b/translatable/tests/unitary/translation_collection.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use toml::Table; +use toml_edit::DocumentMut; use translatable::Language; use translatable_shared::translations::collection::TranslationNodeCollection; use translatable_shared::translations::node::TranslationNode; @@ -24,7 +24,7 @@ pub fn loads_and_finds_collection() { "a".into(), TranslationNode::try_from( FILE_1 - .parse::
() + .parse::() .expect("TOML to be parsed correctly."), ) .expect("TOML to follow the translation rules."), @@ -33,7 +33,7 @@ pub fn loads_and_finds_collection() { "b".into(), TranslationNode::try_from( FILE_2 - .parse::
() + .parse::() .expect("TOML to be parsed correctly."), ) .expect("TOML to follow the translation rules."), diff --git a/translatable_proc/Cargo.toml b/translatable_proc/Cargo.toml index 4734fe4..350a23b 100644 --- a/translatable_proc/Cargo.toml +++ b/translatable_proc/Cargo.toml @@ -17,5 +17,5 @@ quote = "1.0.38" strum = { version = "0.27.1", features = ["derive"] } syn = { version = "2.0.98", features = ["full"] } thiserror = "2.0.11" -toml = "0.8.20" +toml_edit = "0.22.26" translatable_shared = { version = "1", path = "../translatable_shared/" } diff --git a/translatable_proc/src/data/config.rs b/translatable_proc/src/data/config.rs index 479d891..c11b367 100644 --- a/translatable_proc/src/data/config.rs +++ b/translatable_proc/src/data/config.rs @@ -11,8 +11,7 @@ use std::sync::OnceLock; use strum::EnumString; use thiserror::Error; -use toml::Table; -use toml::de::Error as TomlError; +use toml_edit::{DocumentMut, TomlError}; /// Configuration error enum. /// @@ -213,7 +212,7 @@ pub fn load_config() -> Result<&'static MacroConfig, ConfigError> { let toml_content = read_to_string("./translatable.toml") .unwrap_or_default() - .parse::
()?; + .parse::()?; macro_rules! config_value { ($env_var:expr, $key:expr, $default:expr) => { diff --git a/translatable_proc/src/data/translations.rs b/translatable_proc/src/data/translations.rs index 2334aba..ac934ec 100644 --- a/translatable_proc/src/data/translations.rs +++ b/translatable_proc/src/data/translations.rs @@ -13,8 +13,7 @@ use std::io::Error as IoError; use std::sync::OnceLock; use thiserror::Error; -use toml::Table; -use toml::de::Error as TomlError; +use toml_edit::{DocumentMut, TomlError}; use translatable_shared::translations::collection::TranslationNodeCollection; use translatable_shared::translations::node::{TranslationNode, TranslationNodeError}; @@ -209,7 +208,7 @@ pub fn load_translations() -> Result<&'static TranslationNodeCollection, Transla .iter() .map(|path| { let table = read_to_string(path)? - .parse::
() + .parse::() .map_err(|err| TranslationDataError::ParseToml(err, path.clone()))?; Ok((path.clone(), TranslationNode::try_from(table)?)) diff --git a/translatable_shared/Cargo.toml b/translatable_shared/Cargo.toml index 36102f9..de7b9b5 100644 --- a/translatable_shared/Cargo.toml +++ b/translatable_shared/Cargo.toml @@ -14,4 +14,4 @@ quote = "1.0.40" strum = { version = "0.27.1", features = ["derive", "strum_macros"] } syn = { version = "2.0.100", features = ["full"] } thiserror = "2.0.12" -toml = "0.8.20" +toml_edit = "0.22.26" diff --git a/translatable_shared/src/translations/node.rs b/translatable_shared/src/translations/node.rs index 8809b7f..e8832fb 100644 --- a/translatable_shared/src/translations/node.rs +++ b/translatable_shared/src/translations/node.rs @@ -11,7 +11,7 @@ use proc_macro2::TokenStream as TokenStream2; use quote::{ToTokens, TokenStreamExt, quote}; use strum::ParseError; use thiserror::Error; -use toml::{Table, Value}; +use toml_edit::{DocumentMut, Item, Table, Value}; use crate::macros::collections::{map_to_tokens, map_transform_to_tokens}; use crate::misc::language::Language; @@ -172,32 +172,50 @@ impl ToTokens for TranslationNode { impl TryFrom
for TranslationNode { type Error = TranslationNodeError; - // The top level can only contain objects is never enforced. + /// Attempts to convert a `Table` into a `TranslationNode`. + /// + /// This function iterates over the key-value pairs in the given `Table`. + /// Depending on the type of each value, it either adds a translation to + /// a `TranslationObject` or a nested `TranslationNode` to a `TranslationNesting`. + /// If the conversion fails due to mixed values or invalid nesting, it returns + /// a `TranslationNodeError`. + /// + /// # Arguments + /// + /// * `value` - A TOML table to be converted. + /// + /// # Returns + /// + /// A `Result` containing a `TranslationNode` if successful, or a + /// `TranslationNodeError` if the conversion fails. fn try_from(value: Table) -> Result { let mut result = None; - for (key, value) in value { + for (key, value) in value.iter() { match value { - Value::String(translation_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.parse()?); + translation.insert( + key.parse()?, + translation_value + .clone() + .into_value() + .parse()?, + ); }, - Self::Nesting(_) => return Err(TranslationNodeError::MixedValues), } }, - Value::Table(nesting_value) => { + Item::Table(nesting_value) => { match result.get_or_insert_with(|| Self::Nesting(HashMap::new())) { Self::Nesting(nesting) => { - nesting.insert(key, Self::try_from(nesting_value)?); + nesting.insert(key.to_string(), Self::try_from(nesting_value.clone())?); }, - Self::Translation(_) => return Err(TranslationNodeError::MixedValues), } }, - _ => return Err(TranslationNodeError::InvalidNesting), } } @@ -205,3 +223,34 @@ impl TryFrom
for TranslationNode { result.ok_or(TranslationNodeError::EmptyTable) } } + +/// TOML table parsing. +/// +/// This implementation parses a TOML DocumentMut struct +/// into a [`TranslationNode`] for validation and +/// seeking the translations according to the rules. +impl TryFrom for TranslationNode { + type Error = TranslationNodeError; + + /// Attempts to convert a `DocumentMut` into a `TranslationNode`. + /// + /// This function tries to parse the given `DocumentMut` as a TOML table + /// and convert it into a `TranslationNode`. If the conversion fails, + /// it returns a `TranslationNodeError`. + /// + /// # Arguments + /// + /// * `value` - A mutable TOML document to be converted. + /// + /// # Returns + /// + /// A `Result` containing a `TranslationNode` if successful, or a + /// `TranslationNodeError` if the conversion fails. + fn try_from(value: DocumentMut) -> Result { + Self::try_from( + value + .as_table() + .clone(), + ) + } +} From 2930ca81f54838b158a9bb21b60baddfea8fd4d6 Mon Sep 17 00:00:00 2001 From: chikof Date: Sat, 3 May 2025 12:31:37 +0100 Subject: [PATCH 206/228] fix: use immutable documents instead --- .../tests/unitary/translation_collection.rs | 6 +++--- translatable_proc/src/data/config.rs | 4 ++-- translatable_proc/src/data/translations.rs | 4 ++-- translatable_shared/src/translations/node.rs | 15 +++++++-------- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/translatable/tests/unitary/translation_collection.rs b/translatable/tests/unitary/translation_collection.rs index 93b0d27..cdc4c25 100644 --- a/translatable/tests/unitary/translation_collection.rs +++ b/translatable/tests/unitary/translation_collection.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use toml_edit::DocumentMut; +use toml_edit::ImDocument; use translatable::Language; use translatable_shared::translations::collection::TranslationNodeCollection; use translatable_shared::translations::node::TranslationNode; @@ -24,7 +24,7 @@ pub fn loads_and_finds_collection() { "a".into(), TranslationNode::try_from( FILE_1 - .parse::() + .parse::>() .expect("TOML to be parsed correctly."), ) .expect("TOML to follow the translation rules."), @@ -33,7 +33,7 @@ pub fn loads_and_finds_collection() { "b".into(), TranslationNode::try_from( FILE_2 - .parse::() + .parse::>() .expect("TOML to be parsed correctly."), ) .expect("TOML to follow the translation rules."), diff --git a/translatable_proc/src/data/config.rs b/translatable_proc/src/data/config.rs index c11b367..498f32b 100644 --- a/translatable_proc/src/data/config.rs +++ b/translatable_proc/src/data/config.rs @@ -11,7 +11,7 @@ use std::sync::OnceLock; use strum::EnumString; use thiserror::Error; -use toml_edit::{DocumentMut, TomlError}; +use toml_edit::{ImDocument, TomlError}; /// Configuration error enum. /// @@ -212,7 +212,7 @@ pub fn load_config() -> Result<&'static MacroConfig, ConfigError> { let toml_content = read_to_string("./translatable.toml") .unwrap_or_default() - .parse::()?; + .parse::>()?; macro_rules! config_value { ($env_var:expr, $key:expr, $default:expr) => { diff --git a/translatable_proc/src/data/translations.rs b/translatable_proc/src/data/translations.rs index ac934ec..2f0b0d3 100644 --- a/translatable_proc/src/data/translations.rs +++ b/translatable_proc/src/data/translations.rs @@ -13,7 +13,7 @@ use std::io::Error as IoError; use std::sync::OnceLock; use thiserror::Error; -use toml_edit::{DocumentMut, TomlError}; +use toml_edit::{ImDocument, TomlError}; use translatable_shared::translations::collection::TranslationNodeCollection; use translatable_shared::translations::node::{TranslationNode, TranslationNodeError}; @@ -208,7 +208,7 @@ pub fn load_translations() -> Result<&'static TranslationNodeCollection, Transla .iter() .map(|path| { let table = read_to_string(path)? - .parse::() + .parse::>() .map_err(|err| TranslationDataError::ParseToml(err, path.clone()))?; Ok((path.clone(), TranslationNode::try_from(table)?)) diff --git a/translatable_shared/src/translations/node.rs b/translatable_shared/src/translations/node.rs index e8832fb..7ea8031 100644 --- a/translatable_shared/src/translations/node.rs +++ b/translatable_shared/src/translations/node.rs @@ -11,7 +11,7 @@ use proc_macro2::TokenStream as TokenStream2; use quote::{ToTokens, TokenStreamExt, quote}; use strum::ParseError; use thiserror::Error; -use toml_edit::{DocumentMut, Item, Table, Value}; +use toml_edit::{ImDocument, Item, Table, Value}; use crate::macros::collections::{map_to_tokens, map_transform_to_tokens}; use crate::misc::language::Language; @@ -191,7 +191,7 @@ impl TryFrom
for TranslationNode { fn try_from(value: Table) -> Result { let mut result = None; - for (key, value) in value.iter() { + for (key, value) in value { match value { Item::Value(Value::String(translation_value)) => { match result.get_or_insert_with(|| Self::Translation(HashMap::new())) { @@ -199,7 +199,6 @@ impl TryFrom
for TranslationNode { translation.insert( key.parse()?, translation_value - .clone() .into_value() .parse()?, ); @@ -226,15 +225,15 @@ impl TryFrom
for TranslationNode { /// TOML table parsing. /// -/// This implementation parses a TOML DocumentMut struct +/// This implementation parses a TOML ImDocument struct /// into a [`TranslationNode`] for validation and /// seeking the translations according to the rules. -impl TryFrom for TranslationNode { +impl TryFrom> for TranslationNode { type Error = TranslationNodeError; - /// Attempts to convert a `DocumentMut` into a `TranslationNode`. + /// Attempts to convert a `ImDocument` into a `TranslationNode`. /// - /// This function tries to parse the given `DocumentMut` as a TOML table + /// This function tries to parse the given `ImDocument` as a TOML table /// and convert it into a `TranslationNode`. If the conversion fails, /// it returns a `TranslationNodeError`. /// @@ -246,7 +245,7 @@ impl TryFrom for TranslationNode { /// /// A `Result` containing a `TranslationNode` if successful, or a /// `TranslationNodeError` if the conversion fails. - fn try_from(value: DocumentMut) -> Result { + fn try_from(value: ImDocument) -> Result { Self::try_from( value .as_table() From 8ab7d672e1ca6adce06ba34d53be0cb1884b0893 Mon Sep 17 00:00:00 2001 From: chikof Date: Sat, 3 May 2025 12:35:31 +0100 Subject: [PATCH 207/228] fix: remove unnecessary clone --- translatable_shared/src/translations/node.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/translatable_shared/src/translations/node.rs b/translatable_shared/src/translations/node.rs index 7ea8031..2d70741 100644 --- a/translatable_shared/src/translations/node.rs +++ b/translatable_shared/src/translations/node.rs @@ -210,7 +210,7 @@ impl TryFrom
for TranslationNode { 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.clone())?); + nesting.insert(key.to_string(), Self::try_from(nesting_value)?); }, Self::Translation(_) => return Err(TranslationNodeError::MixedValues), } From 5c76575a8b7585af6a1371d0736a5fa4d0456a89 Mon Sep 17 00:00:00 2001 From: chikof Date: Sat, 3 May 2025 12:36:48 +0100 Subject: [PATCH 208/228] fix: remove unnecessary documentation for impl traits --- translatable_shared/src/translations/node.rs | 31 +------------------- 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/translatable_shared/src/translations/node.rs b/translatable_shared/src/translations/node.rs index 2d70741..05ba6bc 100644 --- a/translatable_shared/src/translations/node.rs +++ b/translatable_shared/src/translations/node.rs @@ -172,22 +172,7 @@ impl ToTokens for TranslationNode { impl TryFrom
for TranslationNode { type Error = TranslationNodeError; - /// Attempts to convert a `Table` into a `TranslationNode`. - /// - /// This function iterates over the key-value pairs in the given `Table`. - /// Depending on the type of each value, it either adds a translation to - /// a `TranslationObject` or a nested `TranslationNode` to a `TranslationNesting`. - /// If the conversion fails due to mixed values or invalid nesting, it returns - /// a `TranslationNodeError`. - /// - /// # Arguments - /// - /// * `value` - A TOML table to be converted. - /// - /// # Returns - /// - /// A `Result` containing a `TranslationNode` if successful, or a - /// `TranslationNodeError` if the conversion fails. + // The top level can only contain objects is never enforced. fn try_from(value: Table) -> Result { let mut result = None; @@ -231,20 +216,6 @@ impl TryFrom
for TranslationNode { impl TryFrom> for TranslationNode { type Error = TranslationNodeError; - /// Attempts to convert a `ImDocument` into a `TranslationNode`. - /// - /// This function tries to parse the given `ImDocument` as a TOML table - /// and convert it into a `TranslationNode`. If the conversion fails, - /// it returns a `TranslationNodeError`. - /// - /// # Arguments - /// - /// * `value` - A mutable TOML document to be converted. - /// - /// # Returns - /// - /// A `Result` containing a `TranslationNode` if successful, or a - /// `TranslationNodeError` if the conversion fails. fn try_from(value: ImDocument) -> Result { Self::try_from( value From 595850ff620bfcd529e2e611e01c84764f76528e Mon Sep 17 00:00:00 2001 From: stifskere Date: Sat, 3 May 2025 21:12:06 +0200 Subject: [PATCH 209/228] docs(context): document macro_generation/context.rs --- .../src/macro_generation/context.rs | 41 +++++++++++++++++++ .../src/macro_generation/translation.rs | 12 ++++-- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/translatable_proc/src/macro_generation/context.rs b/translatable_proc/src/macro_generation/context.rs index fc57ad4..bc4c106 100644 --- a/translatable_proc/src/macro_generation/context.rs +++ b/translatable_proc/src/macro_generation/context.rs @@ -1,3 +1,11 @@ +//! [`#\[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; @@ -7,18 +15,51 @@ 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, diff --git a/translatable_proc/src/macro_generation/translation.rs b/translatable_proc/src/macro_generation/translation.rs index a4e4a15..21263a5 100644 --- a/translatable_proc/src/macro_generation/translation.rs +++ b/translatable_proc/src/macro_generation/translation.rs @@ -1,10 +1,11 @@ //! [`translation!()`] macro output module. //! //! This module contains the required for -//! the generation of the `translation!()` macro tokens -//! with intrinsics from `macro_input::translation.rs`. +//! 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}; @@ -46,7 +47,7 @@ enum MacroCompileError { LanguageNotAvailable(Language, String), } -/// `translation!()` macro output generation. +/// [`translation!()`] macro output generation. /// /// Expands into code that resolves a translation string based on the input /// language and translation path, performing placeholder substitutions @@ -61,11 +62,14 @@ enum MacroCompileError { /// /// **Arguments** /// * `input` β€” Structured arguments defining the translation path, language, -/// and any placeholder replacements obtained from `macro_input::translation`. +/// 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()); From b6d1a9834773cb04dd18c7aa19cf1545eccd7c72 Mon Sep 17 00:00:00 2001 From: stifskere Date: Sat, 3 May 2025 21:16:25 +0200 Subject: [PATCH 210/228] chore(docs): add missing docs as a warning in all crate levels --- translatable/src/lib.rs | 2 ++ translatable_proc/src/lib.rs | 2 ++ translatable_shared/src/lib.rs | 2 ++ 3 files changed, 6 insertions(+) diff --git a/translatable/src/lib.rs b/translatable/src/lib.rs index 68d9039..76397e7 100644 --- a/translatable/src/lib.rs +++ b/translatable/src/lib.rs @@ -5,6 +5,8 @@ //! ISO 639-1 compliance, and TOML-based //! translation management. +#![warn(missing_docs)] + mod error; /// Runtime error re-export. diff --git a/translatable_proc/src/lib.rs b/translatable_proc/src/lib.rs index 8a4e818..09feaa6 100644 --- a/translatable_proc/src/lib.rs +++ b/translatable_proc/src/lib.rs @@ -7,6 +7,8 @@ //! The `translatable` library re-exports the macros //! declared in this crate. +#![warn(missing_docs)] + use macro_generation::context::context_macro; use macro_input::context::{ContextMacroArgs, ContextMacroStruct}; use proc_macro::TokenStream; diff --git a/translatable_shared/src/lib.rs b/translatable_shared/src/lib.rs index 9e3a099..ff03a65 100644 --- a/translatable_shared/src/lib.rs +++ b/translatable_shared/src/lib.rs @@ -8,6 +8,8 @@ //! declared in this crate and exposes the necessary //! ones. +#![warn(missing_docs)] + pub mod macros; pub mod misc; pub mod translations; From 7b9360805eb7a41d1185cffc6d8a960dec30e827 Mon Sep 17 00:00:00 2001 From: stifskere Date: Sat, 3 May 2025 21:24:41 +0200 Subject: [PATCH 211/228] fix(ci): remove pr-converage.yml --- .github/workflows/pr-coverage.yml | 44 ------------------------------- 1 file changed, 44 deletions(-) delete mode 100644 .github/workflows/pr-coverage.yml diff --git a/.github/workflows/pr-coverage.yml b/.github/workflows/pr-coverage.yml deleted file mode 100644 index 581cfcf..0000000 --- a/.github/workflows/pr-coverage.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: pr coverage - -on: - pull_request: - branches: [main] - -jobs: - coverage: - runs-on: ubuntu-latest - permissions: - actions: read - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set-up Rust - uses: dtolnay/rust-toolchain@stable - with: - toolchain: stable - components: llvm-tools-preview - - - name: Install cargo-binstall@latest - uses: cargo-bins/cargo-binstall@main - - - name: Install cargo-llvm-cov - run: | - cargo binstall cargo-llvm-cov - - - name: Check coverage percentage - id: coverage - run: | - coverage=$(make cov | grep 'Total Coverage: ' | awk '{print $3}') - echo "coverage_percentage=${coverage%\%}" >> $GITHUB_OUTPUT - echo "Detected coverage: ${coverage}" - - - name: Fail if overall coverage is below 80% - run: | - if (( $(echo "${{ steps.coverage.outputs.coverage_percentage }} < 80" | bc -l) )); then - echo "❌ Coverage is below 80% (${{ steps.coverage.outputs.coverage_percentage }}%)" - exit 1 - else - echo "βœ… Coverage meets requirement (${{ steps.coverage.outputs.coverage_percentage }}%)" - fi From cfe3d05f8e7a34eb7925af65a88a1d55f90c9089 Mon Sep 17 00:00:00 2001 From: Chiko <53100578+chikof@users.noreply.github.com> Date: Sun, 4 May 2025 03:33:16 +0100 Subject: [PATCH 212/228] fix: switch back to mutable document This commit switches back to using a **mutable** `DocumentMut` everywhere. Instead of calling `as_table().clone()` on an immutable document, what is done now is: 1. Parse directly into a `DocumentMut`. 2. Call `as_table_mut()` to get a `&mut Table`. 3. Use `std::mem::take` to move the entire `Table` out in O(1) time (no deep clone). --- .../tests/unitary/translation_collection.rs | 6 +++--- translatable_proc/src/data/config.rs | 4 ++-- translatable_proc/src/data/translations.rs | 4 ++-- translatable_shared/src/translations/node.rs | 19 +++++++++---------- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/translatable/tests/unitary/translation_collection.rs b/translatable/tests/unitary/translation_collection.rs index cdc4c25..93b0d27 100644 --- a/translatable/tests/unitary/translation_collection.rs +++ b/translatable/tests/unitary/translation_collection.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use toml_edit::ImDocument; +use toml_edit::DocumentMut; use translatable::Language; use translatable_shared::translations::collection::TranslationNodeCollection; use translatable_shared::translations::node::TranslationNode; @@ -24,7 +24,7 @@ pub fn loads_and_finds_collection() { "a".into(), TranslationNode::try_from( FILE_1 - .parse::>() + .parse::() .expect("TOML to be parsed correctly."), ) .expect("TOML to follow the translation rules."), @@ -33,7 +33,7 @@ pub fn loads_and_finds_collection() { "b".into(), TranslationNode::try_from( FILE_2 - .parse::>() + .parse::() .expect("TOML to be parsed correctly."), ) .expect("TOML to follow the translation rules."), diff --git a/translatable_proc/src/data/config.rs b/translatable_proc/src/data/config.rs index 498f32b..c11b367 100644 --- a/translatable_proc/src/data/config.rs +++ b/translatable_proc/src/data/config.rs @@ -11,7 +11,7 @@ use std::sync::OnceLock; use strum::EnumString; use thiserror::Error; -use toml_edit::{ImDocument, TomlError}; +use toml_edit::{DocumentMut, TomlError}; /// Configuration error enum. /// @@ -212,7 +212,7 @@ pub fn load_config() -> Result<&'static MacroConfig, ConfigError> { let toml_content = read_to_string("./translatable.toml") .unwrap_or_default() - .parse::>()?; + .parse::()?; macro_rules! config_value { ($env_var:expr, $key:expr, $default:expr) => { diff --git a/translatable_proc/src/data/translations.rs b/translatable_proc/src/data/translations.rs index 2f0b0d3..ac934ec 100644 --- a/translatable_proc/src/data/translations.rs +++ b/translatable_proc/src/data/translations.rs @@ -13,7 +13,7 @@ use std::io::Error as IoError; use std::sync::OnceLock; use thiserror::Error; -use toml_edit::{ImDocument, TomlError}; +use toml_edit::{DocumentMut, TomlError}; use translatable_shared::translations::collection::TranslationNodeCollection; use translatable_shared::translations::node::{TranslationNode, TranslationNodeError}; @@ -208,7 +208,7 @@ pub fn load_translations() -> Result<&'static TranslationNodeCollection, Transla .iter() .map(|path| { let table = read_to_string(path)? - .parse::>() + .parse::() .map_err(|err| TranslationDataError::ParseToml(err, path.clone()))?; Ok((path.clone(), TranslationNode::try_from(table)?)) diff --git a/translatable_shared/src/translations/node.rs b/translatable_shared/src/translations/node.rs index 05ba6bc..8b43067 100644 --- a/translatable_shared/src/translations/node.rs +++ b/translatable_shared/src/translations/node.rs @@ -6,12 +6,13 @@ //! lead to translation objects or other paths. use std::collections::HashMap; +use std::mem::take; use proc_macro2::TokenStream as TokenStream2; use quote::{ToTokens, TokenStreamExt, quote}; use strum::ParseError; use thiserror::Error; -use toml_edit::{ImDocument, Item, Table, Value}; +use toml_edit::{DocumentMut, Item, Table, Value}; use crate::macros::collections::{map_to_tokens, map_transform_to_tokens}; use crate::misc::language::Language; @@ -184,7 +185,7 @@ impl TryFrom
for TranslationNode { translation.insert( key.parse()?, translation_value - .into_value() + .value() .parse()?, ); }, @@ -210,17 +211,15 @@ impl TryFrom
for TranslationNode { /// TOML table parsing. /// -/// This implementation parses a TOML ImDocument struct +/// This implementation parses a TOML DocumentMut struct /// into a [`TranslationNode`] for validation and /// seeking the translations according to the rules. -impl TryFrom> for TranslationNode { +impl TryFrom for TranslationNode { type Error = TranslationNodeError; - fn try_from(value: ImDocument) -> Result { - Self::try_from( - value - .as_table() - .clone(), - ) + fn try_from(value: DocumentMut) -> Result { + let mut doc = value; + let table = take(doc.as_table_mut()); + Self::try_from(table) } } From 2a28e67811b0d5863c66e039fee324221c6ea4c3 Mon Sep 17 00:00:00 2001 From: stifskere Date: Sun, 4 May 2025 20:10:44 +0200 Subject: [PATCH 213/228] docs(context): macro_input/translation.rs --- .../src/macro_generation/context.rs | 2 +- translatable_proc/src/macro_input/context.rs | 186 ++++++++++++++++-- .../src/macro_input/translation.rs | 6 +- 3 files changed, 176 insertions(+), 18 deletions(-) diff --git a/translatable_proc/src/macro_generation/context.rs b/translatable_proc/src/macro_generation/context.rs index bc4c106..8f3ae00 100644 --- a/translatable_proc/src/macro_generation/context.rs +++ b/translatable_proc/src/macro_generation/context.rs @@ -67,7 +67,7 @@ pub fn context_macro( let translations = handle_macro_result!(out load_translations()); let base_path = macro_args.base_path(); - let struct_pub = macro_input.pub_state(); + let struct_pub = macro_input.visibility(); let struct_ident = macro_input.ident(); let struct_fields = handle_macro_result!(out diff --git a/translatable_proc/src/macro_input/context.rs b/translatable_proc/src/macro_input/context.rs index 28486ef..139400c 100644 --- a/translatable_proc/src/macro_input/context.rs +++ b/translatable_proc/src/macro_input/context.rs @@ -1,3 +1,11 @@ +//! [`#\[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; @@ -24,48 +32,139 @@ 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 { - base_path: Option, + /// Field base path. + /// + /// A base path to be pre-appended 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, - pub_state: Visibility, + + /// 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 { - pub_state: Visibility, + /// 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 - .clone() - .unwrap_or_else(|| TranslationPath::default()) + 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 { @@ -74,6 +173,12 @@ impl ContextMacroArgs { } } +/// [`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![,])?; @@ -117,11 +222,23 @@ impl Parse for ContextMacroArgs { } } + 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 { @@ -139,18 +256,30 @@ impl ContextMacroField { }) } + /// Visibility getter. + /// + /// **Returns** + /// A reference to this field's visibility. #[inline] #[allow(unused)] - pub fn pub_state(&self) -> &Visibility { - &self.pub_state + 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 { @@ -158,18 +287,27 @@ impl ContextMacroField { } } +/// [`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 pub_state = self.pub_state(); + let visibility = self.visibility(); let ident = self.ident(); let ty = self.ty(); tokens.append_all(quote! { - #pub_state #ident: #ty + #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; @@ -196,23 +334,35 @@ impl TryFrom for ContextMacroField { let ty = field.ty; - Ok(Self { path, pub_state: is_pub, ident, 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 pub_state(&self) -> &Visibility { - &self.pub_state + 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] { @@ -220,6 +370,12 @@ impl ContextMacroStruct { } } +/// [`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::()?; @@ -233,6 +389,6 @@ impl Parse for ContextMacroStruct { .map(|field| ContextMacroField::try_from(field)) .collect::, _>>()?; - Ok(Self { pub_state: is_pub, ident, fields }) + Ok(Self { visibility: is_pub, ident, fields }) } } diff --git a/translatable_proc/src/macro_input/translation.rs b/translatable_proc/src/macro_input/translation.rs index 86de4c1..7b93b6c 100644 --- a/translatable_proc/src/macro_input/translation.rs +++ b/translatable_proc/src/macro_input/translation.rs @@ -1,7 +1,7 @@ -//! [`translation!()`] output generation module. +//! [`translation!()`] input parsing module. //! //! This module declares a structure that implements -//! [`Parse`] for it to be used with [`parse_macro_input`] +//! [`Parse`] for it to be used with [`parse_macro_input`]. //! //! [`translation!()`]: crate::translation //! [`parse_macro_input`]: syn::parse_macro_input @@ -25,6 +25,8 @@ use super::utils::translation_path::TranslationPath; /// 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 From 8c85ba37e6c23c941b7dfb5eb740c61badeefbe4 Mon Sep 17 00:00:00 2001 From: stifskere Date: Sun, 4 May 2025 20:38:47 +0200 Subject: [PATCH 214/228] docs(context): translation_context macro --- translatable_proc/src/lib.rs | 33 +++- translatable_shared/src/misc/language.rs | 183 +++++++++++++++++++++ translatable_shared/src/misc/templating.rs | 4 + 3 files changed, 218 insertions(+), 2 deletions(-) diff --git a/translatable_proc/src/lib.rs b/translatable_proc/src/lib.rs index 09feaa6..52c166a 100644 --- a/translatable_proc/src/lib.rs +++ b/translatable_proc/src/lib.rs @@ -21,7 +21,7 @@ mod data; mod macro_generation; mod macro_input; -/// **translation obtention macro.** +/// # Translation obtention macro. /// /// This macro generates the way to obtain a translation /// from the translation files in the directory defined @@ -68,11 +68,40 @@ pub fn translation(input: TokenStream) -> TokenStream { translation_macro(parse_macro_input!(input as TranslationMacroArgs).into()).into() } +/// # 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 pre-appended 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() + .into() } diff --git a/translatable_shared/src/misc/language.rs b/translatable_shared/src/misc/language.rs index b61ecc5..d7fc925 100644 --- a/translatable_shared/src/misc/language.rs +++ b/translatable_shared/src/misc/language.rs @@ -31,370 +31,553 @@ impl ToTokens for Language { #[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/templating.rs b/translatable_shared/src/misc/templating.rs index 70b5cb4..08c6bee 100644 --- a/translatable_shared/src/misc/templating.rs +++ b/translatable_shared/src/misc/templating.rs @@ -130,6 +130,10 @@ impl FormatString { original } + /// Original string getter. + /// + /// **Returns** + /// A shared slice to the original string. pub fn original(&self) -> &str { &self.original } From bb58d585ed3f054607f8ed571b4875112a5c1bb5 Mon Sep 17 00:00:00 2001 From: stifskere Date: Sun, 4 May 2025 20:43:24 +0200 Subject: [PATCH 215/228] docs(context): errors --- translatable_proc/src/lib.rs | 2 +- translatable_proc/src/macro_input/context.rs | 2 +- translatable_shared/src/macros/errors.rs | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/translatable_proc/src/lib.rs b/translatable_proc/src/lib.rs index 52c166a..d85e76e 100644 --- a/translatable_proc/src/lib.rs +++ b/translatable_proc/src/lib.rs @@ -80,7 +80,7 @@ pub fn translation(input: TokenStream) -> TokenStream { /// /// You can configure some parameters as a punctuated [`MetaNameValue`], /// these are -/// - `base_path`: A path that gets pre-appended to all fields. +/// - `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. /// diff --git a/translatable_proc/src/macro_input/context.rs b/translatable_proc/src/macro_input/context.rs index 139400c..ac586b9 100644 --- a/translatable_proc/src/macro_input/context.rs +++ b/translatable_proc/src/macro_input/context.rs @@ -85,7 +85,7 @@ enum MacroArgsError { pub struct ContextMacroArgs { /// Field base path. /// - /// A base path to be pre-appended to all + /// A base path to be prepended to all /// field paths. base_path: TranslationPath, diff --git a/translatable_shared/src/macros/errors.rs b/translatable_shared/src/macros/errors.rs index 088d267..73be469 100644 --- a/translatable_shared/src/macros/errors.rs +++ b/translatable_shared/src/macros/errors.rs @@ -39,6 +39,16 @@ where 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 } } @@ -75,7 +85,11 @@ impl IntoCompileError for T {} /// 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) => {{ From 7f7a10a08239d2d0ba7735f3fd30cc7b023968c0 Mon Sep 17 00:00:00 2001 From: stifskere Date: Mon, 5 May 2025 11:44:32 +0200 Subject: [PATCH 216/228] chore: sync --- translatable_proc/src/lib.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/translatable_proc/src/lib.rs b/translatable_proc/src/lib.rs index d85e76e..ec96762 100644 --- a/translatable_proc/src/lib.rs +++ b/translatable_proc/src/lib.rs @@ -10,13 +10,12 @@ #![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; -use crate::macro_generation::translation::translation_macro; -use crate::macro_input::translation::TranslationMacroArgs; - mod data; mod macro_generation; mod macro_input; From 0359f954d62fb9a8a5660686a1369a23133256f1 Mon Sep 17 00:00:00 2001 From: stifskere Date: Mon, 5 May 2025 11:47:11 +0200 Subject: [PATCH 217/228] chore(tests): reorder integration tests --- .../integration/context/context_macro.rs | 19 ------------------- translatable/tests/integration/context/mod.rs | 1 - .../language/fail_static_invalid.rs | 0 .../language/fail_static_invalid.stderr | 0 .../{ => translation}/language/mod.rs | 0 .../language/pass_dynamic_enum.rs | 0 .../language/pass_dynamic_expr.rs | 0 .../language/pass_dynamic_invalid_runtime.rs | 0 .../language/pass_static_lowercase.rs | 0 .../language/pass_static_uppercase.rs | 0 .../path/fail_static_nonexistent.rs | 0 .../path/fail_static_nonexistent.stderr | 0 .../integration/{ => translation}/path/mod.rs | 0 .../path/pass_dynamic_expr.rs | 0 .../path/pass_static_existing.rs | 0 .../templates/fail_invalid_ident.rs | 0 .../templates/fail_invalid_ident.stderr | 0 .../templates/fail_not_display.rs | 0 .../templates/fail_not_display.stderr | 0 .../templates/fail_value_not_found.rs | 0 .../templates/fail_value_not_found.stderr | 0 .../{ => translation}/templates/mod.rs | 0 .../templates/pass_ident_ref.rs | 0 .../templates/pass_multiple_templates.rs | 0 .../templates/pass_trailing_comma.rs | 0 .../templates/pass_trailing_comma_no_args.rs | 0 26 files changed, 20 deletions(-) delete mode 100644 translatable/tests/integration/context/context_macro.rs delete mode 100644 translatable/tests/integration/context/mod.rs rename translatable/tests/integration/{ => translation}/language/fail_static_invalid.rs (100%) rename translatable/tests/integration/{ => translation}/language/fail_static_invalid.stderr (100%) rename translatable/tests/integration/{ => translation}/language/mod.rs (100%) rename translatable/tests/integration/{ => translation}/language/pass_dynamic_enum.rs (100%) rename translatable/tests/integration/{ => translation}/language/pass_dynamic_expr.rs (100%) rename translatable/tests/integration/{ => translation}/language/pass_dynamic_invalid_runtime.rs (100%) rename translatable/tests/integration/{ => translation}/language/pass_static_lowercase.rs (100%) rename translatable/tests/integration/{ => translation}/language/pass_static_uppercase.rs (100%) rename translatable/tests/integration/{ => translation}/path/fail_static_nonexistent.rs (100%) rename translatable/tests/integration/{ => translation}/path/fail_static_nonexistent.stderr (100%) rename translatable/tests/integration/{ => translation}/path/mod.rs (100%) rename translatable/tests/integration/{ => translation}/path/pass_dynamic_expr.rs (100%) rename translatable/tests/integration/{ => translation}/path/pass_static_existing.rs (100%) rename translatable/tests/integration/{ => translation}/templates/fail_invalid_ident.rs (100%) rename translatable/tests/integration/{ => translation}/templates/fail_invalid_ident.stderr (100%) rename translatable/tests/integration/{ => translation}/templates/fail_not_display.rs (100%) rename translatable/tests/integration/{ => translation}/templates/fail_not_display.stderr (100%) rename translatable/tests/integration/{ => translation}/templates/fail_value_not_found.rs (100%) rename translatable/tests/integration/{ => translation}/templates/fail_value_not_found.stderr (100%) rename translatable/tests/integration/{ => translation}/templates/mod.rs (100%) rename translatable/tests/integration/{ => translation}/templates/pass_ident_ref.rs (100%) rename translatable/tests/integration/{ => translation}/templates/pass_multiple_templates.rs (100%) rename translatable/tests/integration/{ => translation}/templates/pass_trailing_comma.rs (100%) rename translatable/tests/integration/{ => translation}/templates/pass_trailing_comma_no_args.rs (100%) diff --git a/translatable/tests/integration/context/context_macro.rs b/translatable/tests/integration/context/context_macro.rs deleted file mode 100644 index e7ea8f8..0000000 --- a/translatable/tests/integration/context/context_macro.rs +++ /dev/null @@ -1,19 +0,0 @@ -use std::collections::HashMap; - -use translatable::{Language, translation_context}; - -#[translation_context(base_path = greetings, fallback_language = "es")] -pub struct TestContext { - #[path(formal)] - pub formal: String, - informal: String, -} - -#[test] -fn test() { - let translations = - TestContext::load_translations(Language::AA, &HashMap::from([("user", "John")])); - - assert_eq!(translations.informal, "Hey John, todo bien?"); - assert_eq!(translations.formal, "Bueno conocerte.") -} diff --git a/translatable/tests/integration/context/mod.rs b/translatable/tests/integration/context/mod.rs deleted file mode 100644 index 9169472..0000000 --- a/translatable/tests/integration/context/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod context_macro; diff --git a/translatable/tests/integration/language/fail_static_invalid.rs b/translatable/tests/integration/translation/language/fail_static_invalid.rs similarity index 100% rename from translatable/tests/integration/language/fail_static_invalid.rs rename to translatable/tests/integration/translation/language/fail_static_invalid.rs diff --git a/translatable/tests/integration/language/fail_static_invalid.stderr b/translatable/tests/integration/translation/language/fail_static_invalid.stderr similarity index 100% rename from translatable/tests/integration/language/fail_static_invalid.stderr rename to translatable/tests/integration/translation/language/fail_static_invalid.stderr diff --git a/translatable/tests/integration/language/mod.rs b/translatable/tests/integration/translation/language/mod.rs similarity index 100% rename from translatable/tests/integration/language/mod.rs rename to translatable/tests/integration/translation/language/mod.rs diff --git a/translatable/tests/integration/language/pass_dynamic_enum.rs b/translatable/tests/integration/translation/language/pass_dynamic_enum.rs similarity index 100% rename from translatable/tests/integration/language/pass_dynamic_enum.rs rename to translatable/tests/integration/translation/language/pass_dynamic_enum.rs diff --git a/translatable/tests/integration/language/pass_dynamic_expr.rs b/translatable/tests/integration/translation/language/pass_dynamic_expr.rs similarity index 100% rename from translatable/tests/integration/language/pass_dynamic_expr.rs rename to translatable/tests/integration/translation/language/pass_dynamic_expr.rs diff --git a/translatable/tests/integration/language/pass_dynamic_invalid_runtime.rs b/translatable/tests/integration/translation/language/pass_dynamic_invalid_runtime.rs similarity index 100% rename from translatable/tests/integration/language/pass_dynamic_invalid_runtime.rs rename to translatable/tests/integration/translation/language/pass_dynamic_invalid_runtime.rs diff --git a/translatable/tests/integration/language/pass_static_lowercase.rs b/translatable/tests/integration/translation/language/pass_static_lowercase.rs similarity index 100% rename from translatable/tests/integration/language/pass_static_lowercase.rs rename to translatable/tests/integration/translation/language/pass_static_lowercase.rs diff --git a/translatable/tests/integration/language/pass_static_uppercase.rs b/translatable/tests/integration/translation/language/pass_static_uppercase.rs similarity index 100% rename from translatable/tests/integration/language/pass_static_uppercase.rs rename to translatable/tests/integration/translation/language/pass_static_uppercase.rs diff --git a/translatable/tests/integration/path/fail_static_nonexistent.rs b/translatable/tests/integration/translation/path/fail_static_nonexistent.rs similarity index 100% rename from translatable/tests/integration/path/fail_static_nonexistent.rs rename to translatable/tests/integration/translation/path/fail_static_nonexistent.rs diff --git a/translatable/tests/integration/path/fail_static_nonexistent.stderr b/translatable/tests/integration/translation/path/fail_static_nonexistent.stderr similarity index 100% rename from translatable/tests/integration/path/fail_static_nonexistent.stderr rename to translatable/tests/integration/translation/path/fail_static_nonexistent.stderr diff --git a/translatable/tests/integration/path/mod.rs b/translatable/tests/integration/translation/path/mod.rs similarity index 100% rename from translatable/tests/integration/path/mod.rs rename to translatable/tests/integration/translation/path/mod.rs diff --git a/translatable/tests/integration/path/pass_dynamic_expr.rs b/translatable/tests/integration/translation/path/pass_dynamic_expr.rs similarity index 100% rename from translatable/tests/integration/path/pass_dynamic_expr.rs rename to translatable/tests/integration/translation/path/pass_dynamic_expr.rs diff --git a/translatable/tests/integration/path/pass_static_existing.rs b/translatable/tests/integration/translation/path/pass_static_existing.rs similarity index 100% rename from translatable/tests/integration/path/pass_static_existing.rs rename to translatable/tests/integration/translation/path/pass_static_existing.rs diff --git a/translatable/tests/integration/templates/fail_invalid_ident.rs b/translatable/tests/integration/translation/templates/fail_invalid_ident.rs similarity index 100% rename from translatable/tests/integration/templates/fail_invalid_ident.rs rename to translatable/tests/integration/translation/templates/fail_invalid_ident.rs diff --git a/translatable/tests/integration/templates/fail_invalid_ident.stderr b/translatable/tests/integration/translation/templates/fail_invalid_ident.stderr similarity index 100% rename from translatable/tests/integration/templates/fail_invalid_ident.stderr rename to translatable/tests/integration/translation/templates/fail_invalid_ident.stderr diff --git a/translatable/tests/integration/templates/fail_not_display.rs b/translatable/tests/integration/translation/templates/fail_not_display.rs similarity index 100% rename from translatable/tests/integration/templates/fail_not_display.rs rename to translatable/tests/integration/translation/templates/fail_not_display.rs diff --git a/translatable/tests/integration/templates/fail_not_display.stderr b/translatable/tests/integration/translation/templates/fail_not_display.stderr similarity index 100% rename from translatable/tests/integration/templates/fail_not_display.stderr rename to translatable/tests/integration/translation/templates/fail_not_display.stderr diff --git a/translatable/tests/integration/templates/fail_value_not_found.rs b/translatable/tests/integration/translation/templates/fail_value_not_found.rs similarity index 100% rename from translatable/tests/integration/templates/fail_value_not_found.rs rename to translatable/tests/integration/translation/templates/fail_value_not_found.rs diff --git a/translatable/tests/integration/templates/fail_value_not_found.stderr b/translatable/tests/integration/translation/templates/fail_value_not_found.stderr similarity index 100% rename from translatable/tests/integration/templates/fail_value_not_found.stderr rename to translatable/tests/integration/translation/templates/fail_value_not_found.stderr diff --git a/translatable/tests/integration/templates/mod.rs b/translatable/tests/integration/translation/templates/mod.rs similarity index 100% rename from translatable/tests/integration/templates/mod.rs rename to translatable/tests/integration/translation/templates/mod.rs diff --git a/translatable/tests/integration/templates/pass_ident_ref.rs b/translatable/tests/integration/translation/templates/pass_ident_ref.rs similarity index 100% rename from translatable/tests/integration/templates/pass_ident_ref.rs rename to translatable/tests/integration/translation/templates/pass_ident_ref.rs diff --git a/translatable/tests/integration/templates/pass_multiple_templates.rs b/translatable/tests/integration/translation/templates/pass_multiple_templates.rs similarity index 100% rename from translatable/tests/integration/templates/pass_multiple_templates.rs rename to translatable/tests/integration/translation/templates/pass_multiple_templates.rs diff --git a/translatable/tests/integration/templates/pass_trailing_comma.rs b/translatable/tests/integration/translation/templates/pass_trailing_comma.rs similarity index 100% rename from translatable/tests/integration/templates/pass_trailing_comma.rs rename to translatable/tests/integration/translation/templates/pass_trailing_comma.rs diff --git a/translatable/tests/integration/templates/pass_trailing_comma_no_args.rs b/translatable/tests/integration/translation/templates/pass_trailing_comma_no_args.rs similarity index 100% rename from translatable/tests/integration/templates/pass_trailing_comma_no_args.rs rename to translatable/tests/integration/translation/templates/pass_trailing_comma_no_args.rs From 1c6e8ada9c323a7cdbc450e17b3dfc8fb830a622 Mon Sep 17 00:00:00 2001 From: stifskere Date: Mon, 5 May 2025 18:38:36 +0200 Subject: [PATCH 218/228] feat(tests): add integration tests for context --- .../context/fail_fallback_is_raw.rs | 12 +++++++++ .../context/fail_invalid_base_path.rs | 9 +++++++ .../context/fail_invalid_fallback.rs | 9 +++++++ .../context/fail_no_fallback_is_result.rs | 12 +++++++++ translatable/tests/integration/context/mod.rs | 3 +++ .../context/pass_invalid_runtime_language.rs | 26 +++++++++++++++++++ .../context/pass_without_params.rs | 16 ++++++++++++ translatable/tests/integration/mod.rs | 4 +-- .../language/fail_static_invalid.stderr | 2 +- .../tests/integration/translation/mod.rs | 4 +++ .../path/fail_static_nonexistent.stderr | 2 +- .../templates/fail_invalid_ident.stderr | 2 +- .../templates/fail_not_display.stderr | 2 +- .../templates/fail_value_not_found.stderr | 2 +- translatable/tests/integration_tests.rs | 12 ++++----- 15 files changed, 103 insertions(+), 14 deletions(-) create mode 100644 translatable/tests/integration/context/fail_fallback_is_raw.rs create mode 100644 translatable/tests/integration/context/fail_invalid_base_path.rs create mode 100644 translatable/tests/integration/context/fail_invalid_fallback.rs create mode 100644 translatable/tests/integration/context/fail_no_fallback_is_result.rs create mode 100644 translatable/tests/integration/context/mod.rs create mode 100644 translatable/tests/integration/context/pass_invalid_runtime_language.rs create mode 100644 translatable/tests/integration/context/pass_without_params.rs create mode 100644 translatable/tests/integration/translation/mod.rs diff --git a/translatable/tests/integration/context/fail_fallback_is_raw.rs b/translatable/tests/integration/context/fail_fallback_is_raw.rs new file mode 100644 index 0000000..7bae437 --- /dev/null +++ b/translatable/tests/integration/context/fail_fallback_is_raw.rs @@ -0,0 +1,12 @@ +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_invalid_base_path.rs b/translatable/tests/integration/context/fail_invalid_base_path.rs new file mode 100644 index 0000000..e91dfd4 --- /dev/null +++ b/translatable/tests/integration/context/fail_invalid_base_path.rs @@ -0,0 +1,9 @@ +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_fallback.rs b/translatable/tests/integration/context/fail_invalid_fallback.rs new file mode 100644 index 0000000..5aa1353 --- /dev/null +++ b/translatable/tests/integration/context/fail_invalid_fallback.rs @@ -0,0 +1,9 @@ +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_no_fallback_is_result.rs b/translatable/tests/integration/context/fail_no_fallback_is_result.rs new file mode 100644 index 0000000..d1d191c --- /dev/null +++ b/translatable/tests/integration/context/fail_no_fallback_is_result.rs @@ -0,0 +1,12 @@ +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/mod.rs b/translatable/tests/integration/context/mod.rs new file mode 100644 index 0000000..e7934a3 --- /dev/null +++ b/translatable/tests/integration/context/mod.rs @@ -0,0 +1,3 @@ + +pub mod pass_without_params; +pub mod pass_invalid_runtime_language; diff --git a/translatable/tests/integration/context/pass_invalid_runtime_language.rs b/translatable/tests/integration/context/pass_invalid_runtime_language.rs new file mode 100644 index 0000000..d3eece0 --- /dev/null +++ b/translatable/tests/integration/context/pass_invalid_runtime_language.rs @@ -0,0 +1,26 @@ +#![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 new file mode 100644 index 0000000..6970aef --- /dev/null +++ b/translatable/tests/integration/context/pass_without_params.rs @@ -0,0 +1,16 @@ +#[allow(unused_imports)] // trybuild +use translatable::translation_context; + +#[translation_context(base_path = greetings)] +struct Context { + formal: String, + informal: String +} + +#[test] +fn pass_without_params() { + +} + +#[allow(unused)] +fn main() {} // trybuild diff --git a/translatable/tests/integration/mod.rs b/translatable/tests/integration/mod.rs index a75dfd7..762b824 100644 --- a/translatable/tests/integration/mod.rs +++ b/translatable/tests/integration/mod.rs @@ -1,4 +1,2 @@ +pub mod translation; pub mod context; -pub mod language; -pub mod path; -pub mod templates; diff --git a/translatable/tests/integration/translation/language/fail_static_invalid.stderr b/translatable/tests/integration/translation/language/fail_static_invalid.stderr index 78bc53b..9f5a77f 100644 --- a/translatable/tests/integration/translation/language/fail_static_invalid.stderr +++ b/translatable/tests/integration/translation/language/fail_static_invalid.stderr @@ -1,5 +1,5 @@ error: The literal 'xx' is an invalid ISO 639-1 string, and cannot be parsed - --> tests/integration/language/fail_static_invalid.rs:5:18 + --> tests/integration/translation/language/fail_static_invalid.rs:5:18 | 5 | translation!("xx", static greetings::formal); | ^^^^ diff --git a/translatable/tests/integration/translation/mod.rs b/translatable/tests/integration/translation/mod.rs new file mode 100644 index 0000000..17edfb7 --- /dev/null +++ b/translatable/tests/integration/translation/mod.rs @@ -0,0 +1,4 @@ + +pub mod language; +pub mod path; +pub mod templates; diff --git a/translatable/tests/integration/translation/path/fail_static_nonexistent.stderr b/translatable/tests/integration/translation/path/fail_static_nonexistent.stderr index 946b269..128ba6f 100644 --- a/translatable/tests/integration/translation/path/fail_static_nonexistent.stderr +++ b/translatable/tests/integration/translation/path/fail_static_nonexistent.stderr @@ -1,5 +1,5 @@ error: The path 'non::existing::path' could not be found - --> tests/integration/path/fail_static_nonexistent.rs:5:5 + --> tests/integration/translation/path/fail_static_nonexistent.rs:5:5 | 5 | translation!("es", static non::existing::path); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/translatable/tests/integration/translation/templates/fail_invalid_ident.stderr b/translatable/tests/integration/translation/templates/fail_invalid_ident.stderr index 9fab06b..dd964ea 100644 --- a/translatable/tests/integration/translation/templates/fail_invalid_ident.stderr +++ b/translatable/tests/integration/translation/templates/fail_invalid_ident.stderr @@ -1,5 +1,5 @@ error: expected identifier - --> tests/integration/templates/fail_invalid_ident.rs:5:52 + --> 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.stderr b/translatable/tests/integration/translation/templates/fail_not_display.stderr index 22d079f..0a78c8d 100644 --- a/translatable/tests/integration/translation/templates/fail_not_display.stderr +++ b/translatable/tests/integration/translation/templates/fail_not_display.stderr @@ -1,5 +1,5 @@ error[E0599]: `NotDisplay` doesn't implement `std::fmt::Display` - --> tests/integration/templates/fail_not_display.rs:7:5 + --> 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` diff --git a/translatable/tests/integration/translation/templates/fail_value_not_found.stderr b/translatable/tests/integration/translation/templates/fail_value_not_found.stderr index 9ad6009..1ed020e 100644 --- a/translatable/tests/integration/translation/templates/fail_value_not_found.stderr +++ b/translatable/tests/integration/translation/templates/fail_value_not_found.stderr @@ -1,5 +1,5 @@ error[E0425]: cannot find value `user` in this scope - --> tests/integration/templates/fail_value_not_found.rs:7:52 + --> 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_tests.rs b/translatable/tests/integration_tests.rs index 3078c91..bd395fd 100644 --- a/translatable/tests/integration_tests.rs +++ b/translatable/tests/integration_tests.rs @@ -45,14 +45,14 @@ fn valid_environment() { set_default_env(); set_locales_env("everything_valid"); - t.pass("./tests/integration/language/pass*.rs"); - t.compile_fail("./tests/integration/language/fail*.rs"); + t.pass("./tests/integration/translation/language/pass*.rs"); + t.compile_fail("./tests/integration/translation/language/fail*.rs"); - t.pass("./tests/integration/path/pass*.rs"); - t.compile_fail("./tests/integration/path/fail*.rs"); + t.pass("./tests/integration/translation/path/pass*.rs"); + t.compile_fail("./tests/tranlsation/integration/path/fail*.rs"); - t.pass("./tests/integration/templates/pass*.rs"); - t.compile_fail("./tests/integration/templates/fail*.rs"); + t.pass("./tests/integration/translation/templates/pass*.rs"); + t.compile_fail("./tests/integration/translation/templates/fail*.rs"); } } From 8fd9b427df2b60c17c0fe34233caa44b6ec6e39e Mon Sep 17 00:00:00 2001 From: stifskere Date: Mon, 5 May 2025 18:39:09 +0200 Subject: [PATCH 219/228] chore: apply cargo fmt --- translatable/tests/integration/context/mod.rs | 3 +-- .../context/pass_invalid_runtime_language.rs | 13 ++++--------- .../integration/context/pass_without_params.rs | 6 ++---- translatable/tests/integration/mod.rs | 2 +- translatable/tests/integration/translation/mod.rs | 1 - translatable_proc/src/lib.rs | 7 ++++--- translatable_proc/src/macro_generation/context.rs | 10 ++++++---- translatable_proc/src/macro_input/context.rs | 9 ++++----- 8 files changed, 22 insertions(+), 29 deletions(-) diff --git a/translatable/tests/integration/context/mod.rs b/translatable/tests/integration/context/mod.rs index e7934a3..994e8bc 100644 --- a/translatable/tests/integration/context/mod.rs +++ b/translatable/tests/integration/context/mod.rs @@ -1,3 +1,2 @@ - -pub mod pass_without_params; pub mod pass_invalid_runtime_language; +pub mod pass_without_params; diff --git a/translatable/tests/integration/context/pass_invalid_runtime_language.rs b/translatable/tests/integration/context/pass_invalid_runtime_language.rs index d3eece0..85acd33 100644 --- a/translatable/tests/integration/context/pass_invalid_runtime_language.rs +++ b/translatable/tests/integration/context/pass_invalid_runtime_language.rs @@ -1,23 +1,18 @@ #![allow(dead_code)] #[allow(unused_imports)] // trybuild -use ::{ - std::collections::HashMap, - translatable::translation_context -}; +use ::{std::collections::HashMap, translatable::translation_context}; #[translation_context(base_path = greetings)] struct Context { formal: String, - informal: String + informal: String, } #[test] fn pass_invalid_runtime_language() { - let translations = Context::load_translations( - translatable::Language::AA, - &HashMap::::new() - ); + let translations = + Context::load_translations(translatable::Language::AA, &HashMap::::new()); assert!(translations.is_err()); } diff --git a/translatable/tests/integration/context/pass_without_params.rs b/translatable/tests/integration/context/pass_without_params.rs index 6970aef..40c7e0e 100644 --- a/translatable/tests/integration/context/pass_without_params.rs +++ b/translatable/tests/integration/context/pass_without_params.rs @@ -4,13 +4,11 @@ use translatable::translation_context; #[translation_context(base_path = greetings)] struct Context { formal: String, - informal: String + informal: String, } #[test] -fn pass_without_params() { - -} +fn pass_without_params() {} #[allow(unused)] fn main() {} // trybuild diff --git a/translatable/tests/integration/mod.rs b/translatable/tests/integration/mod.rs index 762b824..f374719 100644 --- a/translatable/tests/integration/mod.rs +++ b/translatable/tests/integration/mod.rs @@ -1,2 +1,2 @@ -pub mod translation; pub mod context; +pub mod translation; diff --git a/translatable/tests/integration/translation/mod.rs b/translatable/tests/integration/translation/mod.rs index 17edfb7..b600327 100644 --- a/translatable/tests/integration/translation/mod.rs +++ b/translatable/tests/integration/translation/mod.rs @@ -1,4 +1,3 @@ - pub mod language; pub mod path; pub mod templates; diff --git a/translatable_proc/src/lib.rs b/translatable_proc/src/lib.rs index ec96762..547b82f 100644 --- a/translatable_proc/src/lib.rs +++ b/translatable_proc/src/lib.rs @@ -91,8 +91,9 @@ pub fn translation(input: TokenStream) -> TokenStream { /// 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. +/// 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 @@ -102,5 +103,5 @@ pub fn translation_context(attr: TokenStream, item: TokenStream) -> TokenStream parse_macro_input!(attr as ContextMacroArgs), parse_macro_input!(item as ContextMacroStruct), ) - .into() + .into() } diff --git a/translatable_proc/src/macro_generation/context.rs b/translatable_proc/src/macro_generation/context.rs index 8f3ae00..d78e6f6 100644 --- a/translatable_proc/src/macro_generation/context.rs +++ b/translatable_proc/src/macro_generation/context.rs @@ -17,12 +17,14 @@ 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. +/// 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. +/// for immediate feedback while invoking the [`#\[translation_context\]`] +/// macro. /// /// [`#\[translation_context\]`]: crate::translation_context #[derive(Error, Debug)] diff --git a/translatable_proc/src/macro_input/context.rs b/translatable_proc/src/macro_input/context.rs index ac586b9..f36e6a2 100644 --- a/translatable_proc/src/macro_input/context.rs +++ b/translatable_proc/src/macro_input/context.rs @@ -34,9 +34,9 @@ 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. +/// 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)] @@ -222,8 +222,7 @@ impl Parse for ContextMacroArgs { } } - let base_path = base_path - .unwrap_or_else(|| TranslationPath::default()); + let base_path = base_path.unwrap_or_else(|| TranslationPath::default()); Ok(Self { base_path, fallback_language }) } From 48c60f61d437156db2d378b68efa50611fe7794d Mon Sep 17 00:00:00 2001 From: stifskere Date: Mon, 5 May 2025 18:49:47 +0200 Subject: [PATCH 220/228] chore(tests): run integration --- .../integration/context/fail_disallowed_type.rs | 12 ++++++++++++ .../integration/context/fail_disallowed_type.stderr | 7 +++++++ .../integration/context/fail_fallback_is_raw.rs | 1 + .../integration/context/fail_fallback_is_raw.stderr | 8 ++++++++ .../context/fail_invalid_base_path.stderr | 7 +++++++ .../integration/context/fail_invalid_fallback.stderr | 5 +++++ .../context/fail_no_fallback_is_result.rs | 1 + .../context/fail_no_fallback_is_result.stderr | 10 ++++++++++ translatable/tests/integration_tests.rs | 3 +++ 9 files changed, 54 insertions(+) create mode 100644 translatable/tests/integration/context/fail_disallowed_type.rs create mode 100644 translatable/tests/integration/context/fail_disallowed_type.stderr create mode 100644 translatable/tests/integration/context/fail_fallback_is_raw.stderr create mode 100644 translatable/tests/integration/context/fail_invalid_base_path.stderr create mode 100644 translatable/tests/integration/context/fail_invalid_fallback.stderr create mode 100644 translatable/tests/integration/context/fail_no_fallback_is_result.stderr diff --git a/translatable/tests/integration/context/fail_disallowed_type.rs b/translatable/tests/integration/context/fail_disallowed_type.rs new file mode 100644 index 0000000..dc80d25 --- /dev/null +++ b/translatable/tests/integration/context/fail_disallowed_type.rs @@ -0,0 +1,12 @@ +#[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 new file mode 100644 index 0000000..d0af50b --- /dev/null +++ b/translatable/tests/integration/context/fail_disallowed_type.stderr @@ -0,0 +1,7 @@ +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 index 7bae437..6a422ff 100644 --- a/translatable/tests/integration/context/fail_fallback_is_raw.rs +++ b/translatable/tests/integration/context/fail_fallback_is_raw.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use translatable::{translation_context, Language}; #[translation_context(base_path = greetings, fallback_language = "en")] diff --git a/translatable/tests/integration/context/fail_fallback_is_raw.stderr b/translatable/tests/integration/context/fail_fallback_is_raw.stderr new file mode 100644 index 0000000..0126b0e --- /dev/null +++ b/translatable/tests/integration/context/fail_fallback_is_raw.stderr @@ -0,0 +1,8 @@ +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.stderr b/translatable/tests/integration/context/fail_invalid_base_path.stderr new file mode 100644 index 0000000..aedd58d --- /dev/null +++ b/translatable/tests/integration/context/fail_invalid_base_path.stderr @@ -0,0 +1,7 @@ +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.stderr b/translatable/tests/integration/context/fail_invalid_fallback.stderr new file mode 100644 index 0000000..98180be --- /dev/null +++ b/translatable/tests/integration/context/fail_invalid_fallback.stderr @@ -0,0 +1,5 @@ +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 index d1d191c..5abd09f 100644 --- a/translatable/tests/integration/context/fail_no_fallback_is_result.rs +++ b/translatable/tests/integration/context/fail_no_fallback_is_result.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use translatable::{translation_context, Language}; #[translation_context(base_path = greetings)] diff --git a/translatable/tests/integration/context/fail_no_fallback_is_result.stderr b/translatable/tests/integration/context/fail_no_fallback_is_result.stderr new file mode 100644 index 0000000..be42190 --- /dev/null +++ b/translatable/tests/integration/context/fail_no_fallback_is_result.stderr @@ -0,0 +1,10 @@ +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_tests.rs b/translatable/tests/integration_tests.rs index bd395fd..2967aee 100644 --- a/translatable/tests/integration_tests.rs +++ b/translatable/tests/integration_tests.rs @@ -53,6 +53,9 @@ fn valid_environment() { 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"); } } From 2d3a7e13d08b27f82b6f29029b409d27fd3bf683 Mon Sep 17 00:00:00 2001 From: stifskere Date: Mon, 5 May 2025 18:57:55 +0200 Subject: [PATCH 221/228] chore(tests): add original string test --- translatable/tests/unitary/templating.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/translatable/tests/unitary/templating.rs b/translatable/tests/unitary/templating.rs index 2b7a673..4c7e1a1 100644 --- a/translatable/tests/unitary/templating.rs +++ b/translatable/tests/unitary/templating.rs @@ -57,3 +57,12 @@ pub fn escapes_templates() { 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.") + .original(); + + assert_eq!(result, "Hello {name} how are you doing {day}?"); +} From c82a1a0ddd02b8e7b7146e44a4a1916a2b95926e Mon Sep 17 00:00:00 2001 From: stifskere Date: Mon, 5 May 2025 19:04:37 +0200 Subject: [PATCH 222/228] fix(tests): variable can't hold a dropped reference --- translatable/tests/unitary/templating.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/translatable/tests/unitary/templating.rs b/translatable/tests/unitary/templating.rs index 4c7e1a1..b633ce8 100644 --- a/translatable/tests/unitary/templating.rs +++ b/translatable/tests/unitary/templating.rs @@ -61,8 +61,7 @@ pub fn escapes_templates() { #[test] pub fn gives_original_string() { let result = FormatString::from_str("Hello {name} how are you doing {day}?") - .expect("Format string to be valid.") - .original(); + .expect("Format string to be valid."); - assert_eq!(result, "Hello {name} how are you doing {day}?"); + assert_eq!(result.original(), "Hello {name} how are you doing {day}?"); } From 5ef66c588fe6f9047c9da908076e2a9f35be2705 Mon Sep 17 00:00:00 2001 From: stifskere Date: Mon, 5 May 2025 19:10:50 +0200 Subject: [PATCH 223/228] fix(ci): set pipefail --- .github/workflows/overall-coverage.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/overall-coverage.yml b/.github/workflows/overall-coverage.yml index a471b5e..3251bf5 100644 --- a/.github/workflows/overall-coverage.yml +++ b/.github/workflows/overall-coverage.yml @@ -31,6 +31,7 @@ jobs: - name: Generate coverage and get percentage id: coverage run: | + set -o pipefail make cov export-lcov=1 | tee output.log coverage=$(grep 'Total Coverage: ' output.log | awk '{print $3}') echo "coverage_percentage=${coverage%\%}" >> $GITHUB_OUTPUT From fb95e8fe38fc727ed34a209c3ab50d380d1afa88 Mon Sep 17 00:00:00 2001 From: stifskere Date: Mon, 5 May 2025 19:24:45 +0200 Subject: [PATCH 224/228] chore(tests): fail generic arguments --- translatable/tests/integration/context/mod.rs | 1 + .../context/pass_fallback_catch.rs | 22 +++++++++++++++++++ .../context/pass_without_params.rs | 8 +++++-- .../path/fail_generic_arguments.rs | 6 +++++ .../path/fail_generic_arguments.stderr | 5 +++++ translatable/tests/integration_tests.rs | 2 +- 6 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 translatable/tests/integration/context/pass_fallback_catch.rs create mode 100644 translatable/tests/integration/translation/path/fail_generic_arguments.rs create mode 100644 translatable/tests/integration/translation/path/fail_generic_arguments.stderr diff --git a/translatable/tests/integration/context/mod.rs b/translatable/tests/integration/context/mod.rs index 994e8bc..acef2bf 100644 --- a/translatable/tests/integration/context/mod.rs +++ b/translatable/tests/integration/context/mod.rs @@ -1,2 +1,3 @@ +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 new file mode 100644 index 0000000..a220a1a --- /dev/null +++ b/translatable/tests/integration/context/pass_fallback_catch.rs @@ -0,0 +1,22 @@ +#[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_without_params.rs b/translatable/tests/integration/context/pass_without_params.rs index 40c7e0e..cbf2f50 100644 --- a/translatable/tests/integration/context/pass_without_params.rs +++ b/translatable/tests/integration/context/pass_without_params.rs @@ -1,14 +1,18 @@ #[allow(unused_imports)] // trybuild use translatable::translation_context; -#[translation_context(base_path = greetings)] +#[translation_context] struct Context { + #[path(greetings::formal)] formal: String, + #[path(greetings::informal)] informal: String, } #[test] -fn pass_without_params() {} +fn pass_without_params() { + +} #[allow(unused)] fn main() {} // trybuild diff --git a/translatable/tests/integration/translation/path/fail_generic_arguments.rs b/translatable/tests/integration/translation/path/fail_generic_arguments.rs new file mode 100644 index 0000000..4562b19 --- /dev/null +++ b/translatable/tests/integration/translation/path/fail_generic_arguments.rs @@ -0,0 +1,6 @@ +#[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 new file mode 100644 index 0000000..7a83806 --- /dev/null +++ b/translatable/tests/integration/translation/path/fail_generic_arguments.stderr @@ -0,0 +1,5 @@ +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_tests.rs b/translatable/tests/integration_tests.rs index 2967aee..7b0fc89 100644 --- a/translatable/tests/integration_tests.rs +++ b/translatable/tests/integration_tests.rs @@ -49,7 +49,7 @@ fn valid_environment() { t.compile_fail("./tests/integration/translation/language/fail*.rs"); t.pass("./tests/integration/translation/path/pass*.rs"); - t.compile_fail("./tests/tranlsation/integration/path/fail*.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"); From 3c7cc3fb56195bc0e4e9420fae74f52076b4126c Mon Sep 17 00:00:00 2001 From: stifskere Date: Mon, 5 May 2025 19:43:50 +0200 Subject: [PATCH 225/228] chore(tests): display to error tokens --- .../tests/unitary/display_to_error_tokens.rs | 15 +++++++++++++++ translatable/tests/unitary/mod.rs | 1 + 2 files changed, 16 insertions(+) create mode 100644 translatable/tests/unitary/display_to_error_tokens.rs diff --git a/translatable/tests/unitary/display_to_error_tokens.rs b/translatable/tests/unitary/display_to_error_tokens.rs new file mode 100644 index 0000000..a6f4000 --- /dev/null +++ b/translatable/tests/unitary/display_to_error_tokens.rs @@ -0,0 +1,15 @@ +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/mod.rs b/translatable/tests/unitary/mod.rs index c80cded..336c0f5 100644 --- a/translatable/tests/unitary/mod.rs +++ b/translatable/tests/unitary/mod.rs @@ -3,3 +3,4 @@ pub mod language_enum; pub mod runtime_error; pub mod templating; pub mod translation_collection; +pub mod display_to_error_tokens; From eb1ad4aefed81d30cea7db224b6fd63ca9f4fa6a Mon Sep 17 00:00:00 2001 From: stifskere Date: Mon, 5 May 2025 23:24:58 +0200 Subject: [PATCH 226/228] fix: parse table refernces instead of DocumentMut --- .../tests/unitary/translation_collection.rs | 6 +++-- translatable_proc/src/data/translations.rs | 2 +- translatable_shared/src/translations/node.rs | 23 ++++--------------- 3 files changed, 9 insertions(+), 22 deletions(-) diff --git a/translatable/tests/unitary/translation_collection.rs b/translatable/tests/unitary/translation_collection.rs index 93b0d27..2c91ea6 100644 --- a/translatable/tests/unitary/translation_collection.rs +++ b/translatable/tests/unitary/translation_collection.rs @@ -25,7 +25,8 @@ pub fn loads_and_finds_collection() { TranslationNode::try_from( FILE_1 .parse::() - .expect("TOML to be parsed correctly."), + .expect("TOML to be parsed correctly.") + .as_table(), ) .expect("TOML to follow the translation rules."), ), @@ -34,7 +35,8 @@ pub fn loads_and_finds_collection() { TranslationNode::try_from( FILE_2 .parse::() - .expect("TOML to be parsed correctly."), + .expect("TOML to be parsed correctly.") + .as_table(), ) .expect("TOML to follow the translation rules."), ), diff --git a/translatable_proc/src/data/translations.rs b/translatable_proc/src/data/translations.rs index ac934ec..6998af4 100644 --- a/translatable_proc/src/data/translations.rs +++ b/translatable_proc/src/data/translations.rs @@ -211,7 +211,7 @@ pub fn load_translations() -> Result<&'static TranslationNodeCollection, Transla .parse::() .map_err(|err| TranslationDataError::ParseToml(err, path.clone()))?; - Ok((path.clone(), TranslationNode::try_from(table)?)) + Ok((path.clone(), TranslationNode::try_from(table.as_table())?)) }) .collect::>()?; diff --git a/translatable_shared/src/translations/node.rs b/translatable_shared/src/translations/node.rs index 8b43067..32ae9b6 100644 --- a/translatable_shared/src/translations/node.rs +++ b/translatable_shared/src/translations/node.rs @@ -6,13 +6,12 @@ //! lead to translation objects or other paths. use std::collections::HashMap; -use std::mem::take; use proc_macro2::TokenStream as TokenStream2; use quote::{ToTokens, TokenStreamExt, quote}; use strum::ParseError; use thiserror::Error; -use toml_edit::{DocumentMut, Item, Table, Value}; +use toml_edit::{Item, Table, Value}; use crate::macros::collections::{map_to_tokens, map_transform_to_tokens}; use crate::misc::language::Language; @@ -168,13 +167,14 @@ impl ToTokens for TranslationNode { /// 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
for TranslationNode { +impl TryFrom<&Table> for TranslationNode { type Error = TranslationNodeError; // The top level can only contain objects is never enforced. - fn try_from(value: Table) -> Result { + fn try_from(value: &Table) -> Result { let mut result = None; for (key, value) in value { @@ -208,18 +208,3 @@ impl TryFrom
for TranslationNode { result.ok_or(TranslationNodeError::EmptyTable) } } - -/// TOML table parsing. -/// -/// This implementation parses a TOML DocumentMut struct -/// into a [`TranslationNode`] for validation and -/// seeking the translations according to the rules. -impl TryFrom for TranslationNode { - type Error = TranslationNodeError; - - fn try_from(value: DocumentMut) -> Result { - let mut doc = value; - let table = take(doc.as_table_mut()); - Self::try_from(table) - } -} From ad70eb3a3cc954098325a5cc175539a6e138cb86 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 13:16:11 +0000 Subject: [PATCH 227/228] chore(deps): bump trybuild from 1.0.104 to 1.0.105 Bumps [trybuild](https://github.com/dtolnay/trybuild) from 1.0.104 to 1.0.105. - [Release notes](https://github.com/dtolnay/trybuild/releases) - [Commits](https://github.com/dtolnay/trybuild/compare/1.0.104...1.0.105) --- updated-dependencies: - dependency-name: trybuild dependency-version: 1.0.105 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- translatable/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 64a5d7b..70b3b6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -267,9 +267,9 @@ dependencies = [ [[package]] name = "trybuild" -version = "1.0.104" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ae08be68c056db96f0e6c6dd820727cca756ced9e1f4cc7fdd20e2a55e23898" +checksum = "1c9bf9513a2f4aeef5fdac8677d7d349c79fdbcc03b9c86da6e9d254f1e43be2" dependencies = [ "glob", "serde", diff --git a/translatable/Cargo.toml b/translatable/Cargo.toml index d146e2f..14fcd1b 100644 --- a/translatable/Cargo.toml +++ b/translatable/Cargo.toml @@ -23,4 +23,4 @@ translatable_shared = { version = "1", path = "../translatable_shared/" } [dev-dependencies] quote = "1.0.40" toml_edit = "0.22.26" -trybuild = "1.0.104" +trybuild = "1.0.105" From 475b4af5b585f70cb0d83772dcbed68fecc1d9d3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 16:03:06 +0000 Subject: [PATCH 228/228] chore(deps): bump toml_edit from 0.22.26 to 0.23.1 --- updated-dependencies: - dependency-name: toml_edit dependency-version: 0.23.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Cargo.lock | 53 +++++++++++++++++++++++++++++----- translatable/Cargo.toml | 2 +- translatable_proc/Cargo.toml | 2 +- translatable_shared/Cargo.toml | 2 +- 4 files changed, 48 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 70b3b6d..19d1486 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -195,8 +195,8 @@ checksum = "900f6c86a685850b1bc9f6223b20125115ee3f31e01207d81655bbcc0aea9231" dependencies = [ "serde", "serde_spanned", - "toml_datetime", - "toml_edit", + "toml_datetime 0.6.9", + "toml_edit 0.22.26", ] [[package]] @@ -208,6 +208,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" +dependencies = [ + "serde", +] + [[package]] name = "toml_edit" version = "0.22.26" @@ -217,24 +226,52 @@ dependencies = [ "indexmap", "serde", "serde_spanned", - "toml_datetime", + "toml_datetime 0.6.9", "toml_write", "winnow", ] +[[package]] +name = "toml_edit" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f23a5f4511b296579b6c83e437fe85fa7ece22e3ec44e45ddb975bcf57c3dd" +dependencies = [ + "indexmap", + "toml_datetime 0.7.0", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30" +dependencies = [ + "winnow", +] + [[package]] name = "toml_write" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" +[[package]] +name = "toml_writer" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" + [[package]] name = "translatable" version = "1.0.0" dependencies = [ "quote", "thiserror", - "toml_edit", + "toml_edit 0.23.1", "translatable_proc", "translatable_shared", "trybuild", @@ -249,7 +286,7 @@ dependencies = [ "strum", "syn", "thiserror", - "toml_edit", + "toml_edit 0.23.1", "translatable_shared", ] @@ -262,7 +299,7 @@ dependencies = [ "strum", "syn", "thiserror", - "toml_edit", + "toml_edit 0.23.1", ] [[package]] @@ -370,9 +407,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/translatable/Cargo.toml b/translatable/Cargo.toml index 14fcd1b..a54d232 100644 --- a/translatable/Cargo.toml +++ b/translatable/Cargo.toml @@ -22,5 +22,5 @@ translatable_shared = { version = "1", path = "../translatable_shared/" } [dev-dependencies] quote = "1.0.40" -toml_edit = "0.22.26" +toml_edit = "0.23.1" trybuild = "1.0.105" diff --git a/translatable_proc/Cargo.toml b/translatable_proc/Cargo.toml index 350a23b..4b8bb26 100644 --- a/translatable_proc/Cargo.toml +++ b/translatable_proc/Cargo.toml @@ -17,5 +17,5 @@ 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" +toml_edit = "0.23.1" translatable_shared = { version = "1", path = "../translatable_shared/" } diff --git a/translatable_shared/Cargo.toml b/translatable_shared/Cargo.toml index de7b9b5..4af74fa 100644 --- a/translatable_shared/Cargo.toml +++ b/translatable_shared/Cargo.toml @@ -14,4 +14,4 @@ quote = "1.0.40" strum = { version = "0.27.1", features = ["derive", "strum_macros"] } syn = { version = "2.0.100", features = ["full"] } thiserror = "2.0.12" -toml_edit = "0.22.26" +toml_edit = "0.23.1"