diff options
Diffstat (limited to 'derive')
| -rw-r--r-- | derive/Cargo.toml | 19 | ||||
| -rw-r--r-- | derive/README.md | 133 | ||||
| -rw-r--r-- | derive/src/lib.rs | 957 |
3 files changed, 1109 insertions, 0 deletions
diff --git a/derive/Cargo.toml b/derive/Cargo.toml new file mode 100644 index 0000000..3e30184 --- /dev/null +++ b/derive/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "static-l10n" +version = "0.0.1" +edition = "2024" +authors = ["copi143 <copi143@outlook.com>"] +license = "CC0-1.0" +description = "Static localization for Rust using procedural macros." +homepage = "https://github.com/copi143/rust-static-l10n" +repository = "https://github.com/copi143/rust-static-l10n" +readme = "README.md" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0.105" +quote = "1.0.43" +syn = { version = "2.0.114", features = ["full"] } +toml = "0.9.11" diff --git a/derive/README.md b/derive/README.md new file mode 100644 index 0000000..dc2709d --- /dev/null +++ b/derive/README.md @@ -0,0 +1,133 @@ +# static-l10n + +Procedural macros for `static-l10n`. + +## Macros + +- `main!()` initializes the language state with the default language. +- `lang!("xx")` switches the current language. +- `l10n!("key")` returns a localized string for `key`. +- `f16n!("fmt: {}", arg)` returns a localized formatted string. +- `l10n_print!("key")` prints a localized string. +- `l10n_println!("key")` prints a localized string with newline. +- `l10n_eprint!("key")` prints a localized string to stderr. +- `l10n_eprintln!("key")` prints a localized string to stderr with newline. +- `l10n_write!(writer, "key")` writes a localized string to a writer. +- `l10n_writeln!(writer, "key")` writes a localized string to a writer with newline. +- `l10n_panic!("key")` panics with a localized string. +- `l10n_args!("key")` returns `std::fmt::Arguments` for formatting APIs. +- `l10n_assert!(cond, "key")` asserts a condition with a localized message. +- `l10n_assert_eq!(left, right, "key")` asserts equality with a localized message. +- `l10n_assert_ne!(left, right, "key")` asserts inequality with a localized message. +- `l10n_debug_assert!(cond, "key")` debug-asserts a condition with a localized message. +- `l10n_debug_assert_eq!(left, right, "key")` debug-asserts equality with a localized message. +- `l10n_debug_assert_ne!(left, right, "key")` debug-asserts inequality with a localized message. +- `f16n_print!("fmt: {}", arg)` prints a localized formatted string. +- `f16n_println!("fmt: {}", arg)` prints a localized formatted string with newline. +- `f16n_eprint!("fmt: {}", arg)` prints a localized formatted string to stderr. +- `f16n_eprintln!("fmt: {}", arg)` prints a localized formatted string to stderr with newline. +- `f16n_write!(writer, "fmt: {}", arg)` writes a localized formatted string to a writer. +- `f16n_writeln!(writer, "fmt: {}", arg)` writes a localized formatted string to a writer with newline. +- `f16n_panic!("fmt: {}", arg)` panics with a localized formatted string. +- `f16n_args!("fmt: {}", arg)` returns `std::fmt::Arguments` for formatting APIs. +- `f16n_assert!(cond, "fmt: {}", arg)` asserts a condition with a localized formatted message. +- `f16n_assert_eq!(left, right, "fmt: {}", arg)` asserts equality with a localized formatted message. +- `f16n_assert_ne!(left, right, "fmt: {}", arg)` asserts inequality with a localized formatted message. +- `f16n_debug_assert!(cond, "fmt: {}", arg)` debug-asserts a condition with a localized formatted message. +- `f16n_debug_assert_eq!(left, right, "fmt: {}", arg)` debug-asserts equality with a localized formatted message. +- `f16n_debug_assert_ne!(left, right, "fmt: {}", arg)` debug-asserts inequality with a localized formatted message. +- `debug_print_metadata!()` prints resolved metadata and loaded translations at compile time. + +## Usage + +Add metadata to your crate `Cargo.toml` (this is read at compile time): + +```toml +[package.metadata.static-l10n] +path = "i18n" +base = "en" +langs = ["en", { name = "zh", fallback = "en" }] +``` + +Create translation files under the `path` directory. Each file is TOML and can contain multiple keys: + +```toml +["Hello, world!"] +en = "Hello, world!" +zh = "你好,世界!" + +["Hello, {}!"] +en = "Hello, {}!" +zh = "你好,{}!" +``` + +You can split keys across multiple files and nested folders under `path`. Duplicate keys for the same language will cause a compile-time panic. + +Then use the macros in your crate: + +```rust +static_l10n::main!(); + +static_l10n::lang!("zh"); +let msg = static_l10n::l10n!("hello"); +let formatted = static_l10n::f16n!("count: {}", 3); +static_l10n::l10n_println!("hello"); +static_l10n::f16n_println!("count: {}", 3); +``` + +## How It Works + +- `main!()` defines a global mutex holding the current language. Call it once at crate root. +- `lang!("xx")` switches the current language for subsequent lookups. +- `l10n!` looks up a string key and returns a `&'static str` literal from the translations table. +- `f16n!` looks up a format string and returns a `String` built with `format!`. +- `*_args!` returns `std::fmt::Arguments` for zero-allocation formatting (use with `write!`, `format!`, etc.). + +## Common Patterns + +Print to stdout or stderr: + +```rust +static_l10n::l10n_print!("Hello, world!"); +static_l10n::l10n_eprintln!("Hello, world!"); +static_l10n::f16n_println!("Hello, {}!", "Rust"); +``` + +Write into a buffer or writer: + +```rust +let mut buf = String::new(); +static_l10n::l10n_write!(&mut buf, "Hello, world!"); +static_l10n::f16n_writeln!(&mut buf, "Hello, {}!", "Rust"); +``` + +Use `*_args!` with formatting APIs: + +```rust +use std::fmt::Write; + +let mut buf = String::new(); +let args = static_l10n::f16n_args!("Hello, {}!", "Rust"); +let _ = write!(&mut buf, "{}", args); +``` + +Localized assertions: + +```rust +static_l10n::l10n_assert!(1 + 1 == 2, "Hello, world!"); +static_l10n::f16n_assert_eq!(2 + 2, 4, "Hello, {}!", "Rust"); +static_l10n::l10n_debug_assert_ne!(3, 4, "Hello, world!"); +``` + +Localized panic: + +```rust +static_l10n::l10n_panic!("Hello, world!"); +static_l10n::f16n_panic!("Hello, {}!", "Rust"); +``` + +## Notes + +- All translations are loaded at compile time from `path`. +- Missing keys per language produce a compile-time error. +- `debug_print_metadata!()` can help inspect loaded metadata and translations during compilation. diff --git a/derive/src/lib.rs b/derive/src/lib.rs new file mode 100644 index 0000000..35c98ee --- /dev/null +++ b/derive/src/lib.rs @@ -0,0 +1,957 @@ +use proc_macro::TokenStream; +use quote::{quote, quote_spanned}; +use std::collections::HashMap; +use std::fs; +use std::path::Path; +use std::sync::LazyLock; +use syn::parse::{Parse, ParseStream}; +use syn::punctuated::Punctuated; +use syn::{Expr, LitStr, Token, parse_macro_input}; + +struct LangConfig { + name: String, + fallback: Option<String>, +} + +struct Config { + path: String, + base: String, + langs: Vec<LangConfig>, +} + +struct F16nInput { + fmt: LitStr, + args: Punctuated<Expr, Token![,]>, +} + +struct WriteL10nInput { + target: Expr, + key: LitStr, +} + +struct WriteF16nInput { + target: Expr, + fmt: LitStr, + args: Punctuated<Expr, Token![,]>, +} + +struct L10nAssertInput { + cond: Expr, + key: LitStr, +} + +struct L10nAssertEqInput { + left: Expr, + right: Expr, + key: LitStr, +} + +struct L10nAssertNeInput { + left: Expr, + right: Expr, + key: LitStr, +} + +struct F16nAssertInput { + cond: Expr, + fmt: LitStr, + args: Punctuated<Expr, Token![,]>, +} + +struct F16nAssertEqInput { + left: Expr, + right: Expr, + fmt: LitStr, + args: Punctuated<Expr, Token![,]>, +} + +struct F16nAssertNeInput { + left: Expr, + right: Expr, + fmt: LitStr, + args: Punctuated<Expr, Token![,]>, +} + +impl Parse for F16nInput { + fn parse(input: ParseStream) -> syn::Result<Self> { + let fmt: LitStr = input.parse()?; + let args = if input.is_empty() { + Punctuated::new() + } else { + input.parse::<Token![,]>()?; + Punctuated::parse_terminated(input)? + }; + Ok(F16nInput { fmt, args }) + } +} + +impl Parse for L10nAssertInput { + fn parse(input: ParseStream) -> syn::Result<Self> { + let cond: Expr = input.parse()?; + input.parse::<Token![,]>()?; + let key: LitStr = input.parse()?; + Ok(L10nAssertInput { cond, key }) + } +} + +impl Parse for L10nAssertEqInput { + fn parse(input: ParseStream) -> syn::Result<Self> { + let left: Expr = input.parse()?; + input.parse::<Token![,]>()?; + let right: Expr = input.parse()?; + input.parse::<Token![,]>()?; + let key: LitStr = input.parse()?; + Ok(L10nAssertEqInput { left, right, key }) + } +} + +impl Parse for L10nAssertNeInput { + fn parse(input: ParseStream) -> syn::Result<Self> { + let left: Expr = input.parse()?; + input.parse::<Token![,]>()?; + let right: Expr = input.parse()?; + input.parse::<Token![,]>()?; + let key: LitStr = input.parse()?; + Ok(L10nAssertNeInput { left, right, key }) + } +} + +impl Parse for F16nAssertInput { + fn parse(input: ParseStream) -> syn::Result<Self> { + let cond: Expr = input.parse()?; + input.parse::<Token![,]>()?; + let fmt: LitStr = input.parse()?; + let args = if input.is_empty() { + Punctuated::new() + } else { + input.parse::<Token![,]>()?; + Punctuated::parse_terminated(input)? + }; + Ok(F16nAssertInput { cond, fmt, args }) + } +} + +impl Parse for F16nAssertEqInput { + fn parse(input: ParseStream) -> syn::Result<Self> { + let left: Expr = input.parse()?; + input.parse::<Token![,]>()?; + let right: Expr = input.parse()?; + input.parse::<Token![,]>()?; + let fmt: LitStr = input.parse()?; + let args = if input.is_empty() { + Punctuated::new() + } else { + input.parse::<Token![,]>()?; + Punctuated::parse_terminated(input)? + }; + Ok(F16nAssertEqInput { + left, + right, + fmt, + args, + }) + } +} + +impl Parse for F16nAssertNeInput { + fn parse(input: ParseStream) -> syn::Result<Self> { + let left: Expr = input.parse()?; + input.parse::<Token![,]>()?; + let right: Expr = input.parse()?; + input.parse::<Token![,]>()?; + let fmt: LitStr = input.parse()?; + let args = if input.is_empty() { + Punctuated::new() + } else { + input.parse::<Token![,]>()?; + Punctuated::parse_terminated(input)? + }; + Ok(F16nAssertNeInput { + left, + right, + fmt, + args, + }) + } +} + +impl Parse for WriteL10nInput { + fn parse(input: ParseStream) -> syn::Result<Self> { + let target: Expr = input.parse()?; + input.parse::<Token![,]>()?; + let key: LitStr = input.parse()?; + Ok(WriteL10nInput { target, key }) + } +} + +impl Parse for WriteF16nInput { + fn parse(input: ParseStream) -> syn::Result<Self> { + let target: Expr = input.parse()?; + input.parse::<Token![,]>()?; + let fmt: LitStr = input.parse()?; + let args = if input.is_empty() { + Punctuated::new() + } else { + input.parse::<Token![,]>()?; + Punctuated::parse_terminated(input)? + }; + Ok(WriteF16nInput { target, fmt, args }) + } +} + +fn parse_metadata(cfg: &toml::Value) -> Option<Config> { + let path = cfg.get("path")?.as_str()?.to_string(); + let base = cfg.get("base")?.as_str()?.to_string(); + + let langs_value = cfg.get("langs")?; + let mut langs = Vec::new(); + + if let Some(array) = langs_value.as_array() { + for lang in array { + if let Some(name) = lang.as_str() { + langs.push(LangConfig { + name: name.to_string(), + fallback: None, + }); + } else if let Some(table) = lang.as_table() { + let name = table.get("name")?.as_str()?.to_string(); + let fallback = table + .get("fallback") + .and_then(|f| f.as_str()) + .map(|s| s.to_string()); + langs.push(LangConfig { name, fallback }); + } + } + } + + Some(Config { path, base, langs }) +} + +fn get_metadata() -> Config { + let manifest_path = Path::new(&*MANIFEST_DIR).join("Cargo.toml"); + let manifest_content = fs::read_to_string(manifest_path).unwrap(); + let manifest = toml::from_str::<toml::Value>(&manifest_content).unwrap(); + + let cfg = manifest + .get("package") + .and_then(|pkg| pkg.get("metadata")) + .and_then(|meta| meta.get("static-l10n")) + .expect("static-l10n metadata not found in Cargo.toml"); + + parse_metadata(cfg).expect("Invalid static-l10n metadata format") +} + +static MANIFEST_DIR: LazyLock<String> = + LazyLock::new(|| std::env::var("CARGO_MANIFEST_DIR").unwrap()); +static CONFIG: LazyLock<Config> = LazyLock::new(|| get_metadata()); +static TRANSLATION_PATH: LazyLock<String> = LazyLock::new(|| { + let base_path = Path::new(&*MANIFEST_DIR).join(&CONFIG.path); + base_path.to_string_lossy().to_string() +}); +static TRANSLATIONS: LazyLock<HashMap<(String, String), String>> = + LazyLock::new(|| parse_translations()); + +fn read_translate_files() -> Vec<String> { + let file_paths = list_translate_file_paths(); + let mut files = Vec::new(); + for path in file_paths { + if let Ok(content) = fs::read_to_string(&path) { + files.push(content); + } + } + files +} + +fn list_translate_file_paths() -> Vec<String> { + let mut files = Vec::new(); + + let base_path = Path::new(&*TRANSLATION_PATH).to_path_buf(); + if !base_path.exists() || !base_path.is_dir() { + return files; + } + + let mut stack = vec![base_path]; + while let Some(current_dir) = stack.pop() { + if let Ok(entries) = fs::read_dir(current_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + stack.push(path); + } else if path.is_file() { + files.push(path.to_string_lossy().to_string()); + } + } + } + } + + files.sort(); + files +} + +fn parse_translations() -> HashMap<(String, String), String> { + let files = read_translate_files(); + let mut translations = HashMap::new(); + + for content in files { + if let Ok(toml_value) = toml::from_str::<toml::Value>(&content) { + if let Some(table) = toml_value.as_table() { + for (key, value) in table { + let _ = translations + .insert((key.clone(), CONFIG.base.clone()), key.clone()) + .is_none_or(|_| { + panic!( + "Duplicate translation for key '{}' and lang '{}'", + key, CONFIG.base + ) + }); + if let Some(subtable) = value.as_table() { + for (lang, translation) in subtable { + if let Some(translation_str) = translation.as_str() { + let _ = translations + .insert( + (key.clone(), lang.clone()), + translation_str.to_string(), + ) + .is_none_or(|_| { + panic!( + "Duplicate translation for key '{}' and lang '{}'", + key, lang + ) + }); + } + } + } + } + } + } + } + + translations +} + +fn get_translations(key: &str) -> HashMap<String, Option<String>> { + let mut result = HashMap::new(); + for lang in &CONFIG.langs { + let mut fallback = lang.fallback.clone(); + let mut lang = lang.name.clone(); + let mut translation = TRANSLATIONS.get(&(key.to_string(), lang.clone())).cloned(); + while translation.is_none() { + let Some(ref fb) = fallback else { + break; + }; + let fallbacked = CONFIG.langs.iter().find(|l| &l.name == fb).unwrap(); + fallback = fallbacked.fallback.clone(); + lang = fallbacked.name.clone(); + translation = TRANSLATIONS.get(&(key.to_string(), lang.clone())).cloned(); + } + result.insert(lang.clone(), translation); + } + result +} + +#[proc_macro] +pub fn debug_print_metadata(item: TokenStream) -> TokenStream { + if !item.is_empty() { + return quote! { + compile_error!("debug_print_metadata does not take any arguments"); + } + .into(); + } + let span = proc_macro::Span::call_site(); + println!("Manifest dir: {}", *MANIFEST_DIR); + println!("Debug info from file: {}", span.file()); + println!("static-l10n metadata:"); + println!(" path: {}", CONFIG.path); + println!(" langs:"); + for lang in &CONFIG.langs { + if let Some(fallback) = &lang.fallback { + println!(" - name: {}, fallback: {}", lang.name, fallback); + } else { + println!(" - name: {}", lang.name); + } + } + println!("Loaded translations:"); + for ((key, lang), translation) in &*TRANSLATIONS { + println!( + " - key: {}, lang: {}, translation: {}", + key, lang, translation + ); + } + quote! {}.into() +} + +#[proc_macro] +pub fn main(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + if !item.is_empty() { + return quote! { + compile_error!("main does not take any arguments"); + } + .into(); + } + let include_paths = list_translate_file_paths(); + let include_files = include_paths + .iter() + .map(|path| LitStr::new(path, proc_macro2::Span::call_site())) + .map(|path| { + quote! { + const _: &str = include_str!(#path); + } + }); + let default_lang = &CONFIG.base; + quote! { + #(#include_files)* + static __STATIC_L10N_LANG__: ::std::sync::Mutex<&'static str> = + ::std::sync::Mutex::new(#default_lang); + } + .into() +} + +#[proc_macro] +pub fn lang(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let lang = syn::parse_macro_input!(item as syn::LitStr).value(); + quote! { + { + *crate::__STATIC_L10N_LANG__.lock().unwrap() = #lang; + } + } + .into() +} + +#[proc_macro] +pub fn l10n(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(item as LitStr); + expand_l10n_expr(input).into() +} + +// 格式化到 format 的数字翻译宏,支持 f16n!("xxx: {}", xxx) +#[proc_macro] +pub fn f16n(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(item as F16nInput); + expand_f16n_expr(input).into() +} + +#[proc_macro] +pub fn l10n_args(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(item as LitStr); + expand_l10n_args_expr(input).into() +} + +#[proc_macro] +pub fn f16n_args(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(item as F16nInput); + expand_f16n_args_expr(input).into() +} + +#[proc_macro] +pub fn l10n_print(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(item as LitStr); + expand_l10n_print_stmt(input, quote!(::std::print!)).into() +} + +#[proc_macro] +pub fn l10n_println(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(item as LitStr); + expand_l10n_print_stmt(input, quote!(::std::println!)).into() +} + +#[proc_macro] +pub fn l10n_eprint(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(item as LitStr); + expand_l10n_print_stmt(input, quote!(::std::eprint!)).into() +} + +#[proc_macro] +pub fn l10n_eprintln(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(item as LitStr); + expand_l10n_print_stmt(input, quote!(::std::eprintln!)).into() +} + +#[proc_macro] +pub fn l10n_write(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(item as WriteL10nInput); + expand_l10n_write_stmt(input, quote!(::std::write!)).into() +} + +#[proc_macro] +pub fn l10n_writeln(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(item as WriteL10nInput); + expand_l10n_write_stmt(input, quote!(::std::writeln!)).into() +} + +#[proc_macro] +pub fn l10n_panic(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(item as LitStr); + expand_l10n_panic_stmt(input).into() +} + +#[proc_macro] +pub fn l10n_assert(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(item as L10nAssertInput); + expand_l10n_assert_stmt(input).into() +} + +#[proc_macro] +pub fn l10n_assert_eq(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(item as L10nAssertEqInput); + expand_l10n_assert_eq_stmt(input).into() +} + +#[proc_macro] +pub fn l10n_assert_ne(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(item as L10nAssertNeInput); + expand_l10n_assert_ne_stmt(input).into() +} + +#[proc_macro] +pub fn l10n_debug_assert(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(item as L10nAssertInput); + expand_l10n_debug_assert_stmt(input).into() +} + +#[proc_macro] +pub fn l10n_debug_assert_eq(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(item as L10nAssertEqInput); + expand_l10n_debug_assert_eq_stmt(input).into() +} + +#[proc_macro] +pub fn l10n_debug_assert_ne(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(item as L10nAssertNeInput); + expand_l10n_debug_assert_ne_stmt(input).into() +} + +#[proc_macro] +pub fn f16n_print(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(item as F16nInput); + expand_f16n_print_stmt(input, quote!(::std::print!)).into() +} + +#[proc_macro] +pub fn f16n_println(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(item as F16nInput); + expand_f16n_print_stmt(input, quote!(::std::println!)).into() +} + +#[proc_macro] +pub fn f16n_eprint(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(item as F16nInput); + expand_f16n_print_stmt(input, quote!(::std::eprint!)).into() +} + +#[proc_macro] +pub fn f16n_eprintln(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(item as F16nInput); + expand_f16n_print_stmt(input, quote!(::std::eprintln!)).into() +} + +#[proc_macro] +pub fn f16n_write(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(item as WriteF16nInput); + expand_f16n_write_stmt(input, quote!(::std::write!)).into() +} + +#[proc_macro] +pub fn f16n_writeln(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(item as WriteF16nInput); + expand_f16n_write_stmt(input, quote!(::std::writeln!)).into() +} + +#[proc_macro] +pub fn f16n_panic(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(item as F16nInput); + expand_f16n_panic_stmt(input).into() +} + +#[proc_macro] +pub fn f16n_assert(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(item as F16nAssertInput); + expand_f16n_assert_stmt(input).into() +} + +#[proc_macro] +pub fn f16n_assert_eq(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(item as F16nAssertEqInput); + expand_f16n_assert_eq_stmt(input).into() +} + +#[proc_macro] +pub fn f16n_assert_ne(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(item as F16nAssertNeInput); + expand_f16n_assert_ne_stmt(input).into() +} + +#[proc_macro] +pub fn f16n_debug_assert(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(item as F16nAssertInput); + expand_f16n_debug_assert_stmt(input).into() +} + +#[proc_macro] +pub fn f16n_debug_assert_eq(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(item as F16nAssertEqInput); + expand_f16n_debug_assert_eq_stmt(input).into() +} + +#[proc_macro] +pub fn f16n_debug_assert_ne(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(item as F16nAssertNeInput); + expand_f16n_debug_assert_ne_stmt(input).into() +} + +fn expand_l10n_expr(input: LitStr) -> proc_macro2::TokenStream { + let key = input.value(); + let span = input.span(); + let translations = build_l10n_arms(&key, span, |lang, translation, span| { + quote_spanned! {span=> + #lang => #translation + } + }); + expand_match_expr(span, translations) +} + +fn expand_l10n_args_expr(input: LitStr) -> proc_macro2::TokenStream { + let key = input.value(); + let span = input.span(); + let translations = build_l10n_arms(&key, span, |lang, translation, span| { + quote_spanned! {span=> + #lang => ::std::format_args!(#translation) + } + }); + expand_match_expr(span, translations) +} + +fn expand_l10n_print_stmt( + input: LitStr, + printer: proc_macro2::TokenStream, +) -> proc_macro2::TokenStream { + let key = input.value(); + let span = input.span(); + let translations = build_l10n_arms(&key, span, |lang, translation, span| { + quote_spanned! {span=> + #lang => { #printer(#translation); } + } + }); + expand_match_expr(span, translations) +} + +fn expand_l10n_write_stmt( + input: WriteL10nInput, + writer: proc_macro2::TokenStream, +) -> proc_macro2::TokenStream { + let key = input.key; + let target = input.target; + let key_value = key.value(); + let span = key.span(); + let translations = build_l10n_arms(&key_value, span, |lang, translation, span| { + quote_spanned! {span=> + #lang => { let _ = #writer(#target, #translation); } + } + }); + expand_match_expr(span, translations) +} + +fn expand_l10n_panic_stmt(input: LitStr) -> proc_macro2::TokenStream { + let key = input.value(); + let span = input.span(); + let translations = build_l10n_arms(&key, span, |lang, translation, span| { + quote_spanned! {span=> + #lang => ::std::panic!(#translation) + } + }); + expand_match_expr(span, translations) +} + +fn expand_l10n_assert_stmt(input: L10nAssertInput) -> proc_macro2::TokenStream { + let cond = input.cond; + let key = input.key; + let span = key.span(); + let message = expand_l10n_expr(key); + quote_spanned! {span=> + ::std::assert!(#cond, "{}", #message); + } +} + +fn expand_l10n_assert_eq_stmt(input: L10nAssertEqInput) -> proc_macro2::TokenStream { + let left = input.left; + let right = input.right; + let key = input.key; + let span = key.span(); + let message = expand_l10n_expr(key); + quote_spanned! {span=> + ::std::assert_eq!(#left, #right, "{}", #message); + } +} + +fn expand_l10n_assert_ne_stmt(input: L10nAssertNeInput) -> proc_macro2::TokenStream { + let left = input.left; + let right = input.right; + let key = input.key; + let span = key.span(); + let message = expand_l10n_expr(key); + quote_spanned! {span=> + ::std::assert_ne!(#left, #right, "{}", #message); + } +} + +fn expand_l10n_debug_assert_stmt(input: L10nAssertInput) -> proc_macro2::TokenStream { + let cond = input.cond; + let key = input.key; + let span = key.span(); + let message = expand_l10n_expr(key); + quote_spanned! {span=> + ::std::debug_assert!(#cond, "{}", #message); + } +} + +fn expand_l10n_debug_assert_eq_stmt(input: L10nAssertEqInput) -> proc_macro2::TokenStream { + let left = input.left; + let right = input.right; + let key = input.key; + let span = key.span(); + let message = expand_l10n_expr(key); + quote_spanned! {span=> + ::std::debug_assert_eq!(#left, #right, "{}", #message); + } +} + +fn expand_l10n_debug_assert_ne_stmt(input: L10nAssertNeInput) -> proc_macro2::TokenStream { + let left = input.left; + let right = input.right; + let key = input.key; + let span = key.span(); + let message = expand_l10n_expr(key); + quote_spanned! {span=> + ::std::debug_assert_ne!(#left, #right, "{}", #message); + } +} + +fn expand_f16n_expr(input: F16nInput) -> proc_macro2::TokenStream { + let fmt_str_lit = input.fmt; + let fmt_str = fmt_str_lit.value(); + let span = fmt_str_lit.span(); + let translations = build_f16n_arms( + &fmt_str, + span, + &input.args, + |lang, translation, span, args| { + quote_spanned! {span=> + #lang => format!(#translation, #(#args),*) + } + }, + ); + expand_match_expr(span, translations) +} + +fn expand_f16n_args_expr(input: F16nInput) -> proc_macro2::TokenStream { + let fmt_str_lit = input.fmt; + let fmt_str = fmt_str_lit.value(); + let span = fmt_str_lit.span(); + let translations = build_f16n_arms( + &fmt_str, + span, + &input.args, + |lang, translation, span, args| { + quote_spanned! {span=> + #lang => ::std::format_args!(#translation, #(#args),*) + } + }, + ); + expand_match_expr(span, translations) +} + +fn expand_f16n_print_stmt( + input: F16nInput, + printer: proc_macro2::TokenStream, +) -> proc_macro2::TokenStream { + let fmt_str_lit = input.fmt; + let fmt_str = fmt_str_lit.value(); + let span = fmt_str_lit.span(); + let translations = build_f16n_arms( + &fmt_str, + span, + &input.args, + |lang, translation, span, args| { + quote_spanned! {span=> + #lang => { #printer(#translation, #(#args),*); } + } + }, + ); + expand_match_expr(span, translations) +} + +fn expand_f16n_write_stmt( + input: WriteF16nInput, + writer: proc_macro2::TokenStream, +) -> proc_macro2::TokenStream { + let fmt_str_lit = input.fmt; + let fmt_str = fmt_str_lit.value(); + let target = input.target; + let span = fmt_str_lit.span(); + let translations = build_f16n_arms( + &fmt_str, + span, + &input.args, + |lang, translation, span, args| { + quote_spanned! {span=> + #lang => { let _ = #writer(#target, #translation, #(#args),*); } + } + }, + ); + expand_match_expr(span, translations) +} + +fn expand_f16n_panic_stmt(input: F16nInput) -> proc_macro2::TokenStream { + let fmt_str_lit = input.fmt; + let fmt_str = fmt_str_lit.value(); + let span = fmt_str_lit.span(); + let translations = build_f16n_arms( + &fmt_str, + span, + &input.args, + |lang, translation, span, args| { + quote_spanned! {span=> + #lang => ::std::panic!(#translation, #(#args),*) + } + }, + ); + expand_match_expr(span, translations) +} + +fn expand_f16n_assert_stmt(input: F16nAssertInput) -> proc_macro2::TokenStream { + let cond = input.cond; + let fmt = input.fmt; + let span = fmt.span(); + let message = expand_f16n_args_expr(F16nInput { + fmt, + args: input.args, + }); + quote_spanned! {span=> + ::std::assert!(#cond, "{}", #message); + } +} + +fn expand_f16n_assert_eq_stmt(input: F16nAssertEqInput) -> proc_macro2::TokenStream { + let left = input.left; + let right = input.right; + let fmt = input.fmt; + let span = fmt.span(); + let message = expand_f16n_args_expr(F16nInput { + fmt, + args: input.args, + }); + quote_spanned! {span=> + ::std::assert_eq!(#left, #right, "{}", #message); + } +} + +fn expand_f16n_assert_ne_stmt(input: F16nAssertNeInput) -> proc_macro2::TokenStream { + let left = input.left; + let right = input.right; + let fmt = input.fmt; + let span = fmt.span(); + let message = expand_f16n_args_expr(F16nInput { + fmt, + args: input.args, + }); + quote_spanned! {span=> + ::std::assert_ne!(#left, #right, "{}", #message); + } +} + +fn expand_f16n_debug_assert_stmt(input: F16nAssertInput) -> proc_macro2::TokenStream { + let cond = input.cond; + let fmt = input.fmt; + let span = fmt.span(); + let message = expand_f16n_args_expr(F16nInput { + fmt, + args: input.args, + }); + quote_spanned! {span=> + ::std::debug_assert!(#cond, "{}", #message); + } +} + +fn expand_f16n_debug_assert_eq_stmt(input: F16nAssertEqInput) -> proc_macro2::TokenStream { + let left = input.left; + let right = input.right; + let fmt = input.fmt; + let span = fmt.span(); + let message = expand_f16n_args_expr(F16nInput { + fmt, + args: input.args, + }); + quote_spanned! {span=> + ::std::debug_assert_eq!(#left, #right, "{}", #message); + } +} + +fn expand_f16n_debug_assert_ne_stmt(input: F16nAssertNeInput) -> proc_macro2::TokenStream { + let left = input.left; + let right = input.right; + let fmt = input.fmt; + let span = fmt.span(); + let message = expand_f16n_args_expr(F16nInput { + fmt, + args: input.args, + }); + quote_spanned! {span=> + ::std::debug_assert_ne!(#left, #right, "{}", #message); + } +} + +fn expand_match_expr( + span: proc_macro2::Span, + translations: Vec<proc_macro2::TokenStream>, +) -> proc_macro2::TokenStream { + quote_spanned! {span=> + match crate::__STATIC_L10N_LANG__.lock().unwrap().as_ref() { + #(#translations,)* + other => panic!("Unsupported language: {}", other), + } + } +} + +fn build_l10n_arms( + key: &str, + span: proc_macro2::Span, + mut make_arm: impl FnMut(&str, &str, proc_macro2::Span) -> proc_macro2::TokenStream, +) -> Vec<proc_macro2::TokenStream> { + let translations = get_translations(key); + let mut arms = Vec::with_capacity(translations.len()); + for (lang, translation) in translations { + if let Some(translation) = translation { + arms.push(make_arm(&lang, &translation, span)); + } else { + let error_msg = format!("Missing translation for key '{}' in lang '{}'", key, lang); + arms.push(quote_spanned! {span=> + #lang => compile_error!(#error_msg) + }); + } + } + arms +} + +fn build_f16n_arms( + fmt_str: &str, + span: proc_macro2::Span, + args: &Punctuated<Expr, Token![,]>, + mut make_arm: impl FnMut(&str, &str, proc_macro2::Span, &[&Expr]) -> proc_macro2::TokenStream, +) -> Vec<proc_macro2::TokenStream> { + let translations = get_translations(fmt_str); + let args_vec: Vec<_> = args.iter().collect(); + let mut arms = Vec::with_capacity(translations.len()); + for (lang, translation) in translations { + if let Some(translation) = translation { + arms.push(make_arm(&lang, &translation, span, &args_vec)); + } else { + let error_msg = format!( + "Missing translation for key '{}' in lang '{}'", + fmt_str, lang + ); + arms.push(quote_spanned! {span=> + #lang => compile_error!(#error_msg) + }); + } + } + arms +} |
