From c38d117b5803c65ad57a4c4704ee0d897d9fb3cb Mon Sep 17 00:00:00 2001 From: copi143 Date: Sat, 17 Jan 2026 04:02:47 +0800 Subject: init --- .gitignore | 1 + Cargo.lock | 151 +++++++++ Cargo.toml | 3 + LICENSE | 121 +++++++ README.md | 66 ++++ derive/Cargo.toml | 19 ++ derive/README.md | 133 ++++++++ derive/src/lib.rs | 957 +++++++++++++++++++++++++++++++++++++++++++++++++++++ test/Cargo.toml | 13 + test/lang/src.toml | 14 + test/src/main.rs | 79 +++++ 11 files changed, 1557 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 derive/Cargo.toml create mode 100644 derive/README.md create mode 100644 derive/src/lib.rs create mode 100644 test/Cargo.toml create mode 100644 test/lang/src.toml create mode 100644 test/src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..3141dd1 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,151 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "proc-macro2" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + +[[package]] +name = "static-l10n" +version = "0.0.1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "toml", +] + +[[package]] +name = "static-l10n-test" +version = "0.0.1" +dependencies = [ + "static-l10n", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "toml" +version = "0.9.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..83c132c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +resolver = "3" +members = ["derive", "test"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8462883 --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +# rust-static-l10n + +Static localization for Rust using procedural macros. + +`static-l10n` loads translations at compile time and generates match-based +lookups, so missing translations fail the build rather than at runtime. + +## Workspace Layout + +- `derive/`: proc-macro crate that provides all macros. +- `test/`: example crate used for manual/testing. + +## Quick Start + +Add metadata in your crate's `Cargo.toml`: + +```toml +[package.metadata.static-l10n] +path = "i18n" +base = "en" +langs = ["en", { name = "zh", fallback = "en" }] +``` + +Put translations under the `path` directory (TOML): + +```toml +["Hello, world!"] +en = "Hello, world!" +zh = "你好,世界!" + +["Hello, {}!"] +en = "Hello, {}!" +zh = "你好,{}!" +``` + +Then use the macros in your crate: + +```rust +static_l10n::main!(); + +static_l10n::lang!("zh"); +let msg = static_l10n::l10n!("Hello, world!"); +let formatted = static_l10n::f16n!("Hello, {}!", "Rust"); +``` + +## Rebuild on Translation Changes + +`static_l10n::main!()` expands `include_str!` for every translation file, so +editing an existing file triggers a rebuild automatically. New files are not +picked up unless you rebuild or add a `build.rs` that watches the directory. + +## Docs + +- Macro reference and detailed usage: `derive/README.md` + +## Development + +Run tests in the example crate: + +```bash +cargo test -p static-l10n-test +``` + +## License + +See `LICENSE`. 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 "] +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, +} + +struct Config { + path: String, + base: String, + langs: Vec, +} + +struct F16nInput { + fmt: LitStr, + args: Punctuated, +} + +struct WriteL10nInput { + target: Expr, + key: LitStr, +} + +struct WriteF16nInput { + target: Expr, + fmt: LitStr, + args: Punctuated, +} + +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, +} + +struct F16nAssertEqInput { + left: Expr, + right: Expr, + fmt: LitStr, + args: Punctuated, +} + +struct F16nAssertNeInput { + left: Expr, + right: Expr, + fmt: LitStr, + args: Punctuated, +} + +impl Parse for F16nInput { + fn parse(input: ParseStream) -> syn::Result { + let fmt: LitStr = input.parse()?; + let args = if input.is_empty() { + Punctuated::new() + } else { + input.parse::()?; + Punctuated::parse_terminated(input)? + }; + Ok(F16nInput { fmt, args }) + } +} + +impl Parse for L10nAssertInput { + fn parse(input: ParseStream) -> syn::Result { + let cond: Expr = input.parse()?; + input.parse::()?; + let key: LitStr = input.parse()?; + Ok(L10nAssertInput { cond, key }) + } +} + +impl Parse for L10nAssertEqInput { + fn parse(input: ParseStream) -> syn::Result { + let left: Expr = input.parse()?; + input.parse::()?; + let right: Expr = input.parse()?; + input.parse::()?; + let key: LitStr = input.parse()?; + Ok(L10nAssertEqInput { left, right, key }) + } +} + +impl Parse for L10nAssertNeInput { + fn parse(input: ParseStream) -> syn::Result { + let left: Expr = input.parse()?; + input.parse::()?; + let right: Expr = input.parse()?; + input.parse::()?; + let key: LitStr = input.parse()?; + Ok(L10nAssertNeInput { left, right, key }) + } +} + +impl Parse for F16nAssertInput { + fn parse(input: ParseStream) -> syn::Result { + let cond: Expr = input.parse()?; + input.parse::()?; + let fmt: LitStr = input.parse()?; + let args = if input.is_empty() { + Punctuated::new() + } else { + input.parse::()?; + Punctuated::parse_terminated(input)? + }; + Ok(F16nAssertInput { cond, fmt, args }) + } +} + +impl Parse for F16nAssertEqInput { + fn parse(input: ParseStream) -> syn::Result { + let left: Expr = input.parse()?; + input.parse::()?; + let right: Expr = input.parse()?; + input.parse::()?; + let fmt: LitStr = input.parse()?; + let args = if input.is_empty() { + Punctuated::new() + } else { + input.parse::()?; + Punctuated::parse_terminated(input)? + }; + Ok(F16nAssertEqInput { + left, + right, + fmt, + args, + }) + } +} + +impl Parse for F16nAssertNeInput { + fn parse(input: ParseStream) -> syn::Result { + let left: Expr = input.parse()?; + input.parse::()?; + let right: Expr = input.parse()?; + input.parse::()?; + let fmt: LitStr = input.parse()?; + let args = if input.is_empty() { + Punctuated::new() + } else { + input.parse::()?; + Punctuated::parse_terminated(input)? + }; + Ok(F16nAssertNeInput { + left, + right, + fmt, + args, + }) + } +} + +impl Parse for WriteL10nInput { + fn parse(input: ParseStream) -> syn::Result { + let target: Expr = input.parse()?; + input.parse::()?; + let key: LitStr = input.parse()?; + Ok(WriteL10nInput { target, key }) + } +} + +impl Parse for WriteF16nInput { + fn parse(input: ParseStream) -> syn::Result { + let target: Expr = input.parse()?; + input.parse::()?; + let fmt: LitStr = input.parse()?; + let args = if input.is_empty() { + Punctuated::new() + } else { + input.parse::()?; + Punctuated::parse_terminated(input)? + }; + Ok(WriteF16nInput { target, fmt, args }) + } +} + +fn parse_metadata(cfg: &toml::Value) -> Option { + 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::(&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 = + LazyLock::new(|| std::env::var("CARGO_MANIFEST_DIR").unwrap()); +static CONFIG: LazyLock = LazyLock::new(|| get_metadata()); +static TRANSLATION_PATH: LazyLock = LazyLock::new(|| { + let base_path = Path::new(&*MANIFEST_DIR).join(&CONFIG.path); + base_path.to_string_lossy().to_string() +}); +static TRANSLATIONS: LazyLock> = + LazyLock::new(|| parse_translations()); + +fn read_translate_files() -> Vec { + 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 { + 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::(&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> { + 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 { + 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 { + 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, + mut make_arm: impl FnMut(&str, &str, proc_macro2::Span, &[&Expr]) -> proc_macro2::TokenStream, +) -> Vec { + 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 +} diff --git a/test/Cargo.toml b/test/Cargo.toml new file mode 100644 index 0000000..0735f93 --- /dev/null +++ b/test/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "static-l10n-test" +version = "0.0.1" +edition = "2024" +publish = false + +[package.metadata.static-l10n] +path = "lang" +base = "en" +langs = ["en", "es", { name = "fr", fallback = "en" }, "zh-cn"] + +[dependencies] +static-l10n = { path = "../derive" } diff --git a/test/lang/src.toml b/test/lang/src.toml new file mode 100644 index 0000000..dedc083 --- /dev/null +++ b/test/lang/src.toml @@ -0,0 +1,14 @@ +["Hello, world!"] +zh-cn = "你好,世界!" +es = "¡Hola, mundo!" +fr = "Bonjour le monde!" + +["Hello, {}!"] +zh-cn = "你好,{}!" +es = "¡Hola, {}!" +fr = "Bonjour, {}!" + +["Hello, {name}!"] +zh-cn = "你好,{name}!" +es = "¡Hola, {name}!" +fr = "Bonjour, {name}!" diff --git a/test/src/main.rs b/test/src/main.rs new file mode 100644 index 0000000..fa28d00 --- /dev/null +++ b/test/src/main.rs @@ -0,0 +1,79 @@ +use static_l10n::{f16n, l10n}; + +static_l10n::main!(); + +fn main() { + static_l10n::debug_print_metadata!(); + static_l10n::lang!("zh-cn"); + println!("{}", l10n!("Hello, world!")); + println!("{}", f16n!("Hello, {}!", "Rust")); + println!("{}", f16n!("Hello, {name}!", name = "World")); +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fmt::Write; + + #[test] + fn l10n_basic_lookup() { + static_l10n::lang!("es"); + assert_eq!(l10n!("Hello, world!"), "¡Hola, mundo!"); + static_l10n::lang!("fr"); + assert_eq!(l10n!("Hello, world!"), "Bonjour le monde!"); + } + + #[test] + fn f16n_formatting() { + static_l10n::lang!("zh-cn"); + assert_eq!(f16n!("Hello, {}!", "Rust"), "你好,Rust!"); + assert_eq!(f16n!("Hello, {name}!", name = "World"), "你好,World!"); + } + + #[test] + fn args_macros() { + static_l10n::lang!("en"); + let msg = format!("{}", static_l10n::l10n_args!("Hello, world!")); + assert_eq!(msg, "Hello, world!"); + let msg = format!("{}", static_l10n::f16n_args!("Hello, {}!", "Rust")); + assert_eq!(msg, "Hello, Rust!"); + } + + #[test] + fn write_macros() { + static_l10n::lang!("es"); + let mut buf = String::new(); + static_l10n::l10n_write!(&mut buf, "Hello, world!"); + assert_eq!(buf, "¡Hola, mundo!"); + + let mut buf = String::new(); + static_l10n::l10n_writeln!(&mut buf, "Hello, world!"); + assert_eq!(buf, "¡Hola, mundo!\n"); + + let mut buf = String::new(); + static_l10n::f16n_write!(&mut buf, "Hello, {}!", "Rust"); + assert_eq!(buf, "¡Hola, Rust!"); + + let mut buf = String::new(); + static_l10n::f16n_writeln!(&mut buf, "Hello, {}!", "Rust"); + assert_eq!(buf, "¡Hola, Rust!\n"); + } + + #[test] + fn assert_macros() { + static_l10n::lang!("en"); + static_l10n::l10n_assert!(1 + 1 == 2, "Hello, world!"); + static_l10n::l10n_assert_eq!(2 + 2, 4, "Hello, world!"); + static_l10n::l10n_assert_ne!(2 + 2, 5, "Hello, world!"); + static_l10n::l10n_debug_assert!(true, "Hello, world!"); + static_l10n::l10n_debug_assert_eq!(3, 3, "Hello, world!"); + static_l10n::l10n_debug_assert_ne!(3, 4, "Hello, world!"); + + static_l10n::f16n_assert!(1 + 1 == 2, "Hello, {}!", "Rust"); + static_l10n::f16n_assert_eq!(2 + 2, 4, "Hello, {}!", "Rust"); + static_l10n::f16n_assert_ne!(2 + 2, 5, "Hello, {}!", "Rust"); + static_l10n::f16n_debug_assert!(true, "Hello, {}!", "Rust"); + static_l10n::f16n_debug_assert_eq!(3, 3, "Hello, {}!", "Rust"); + static_l10n::f16n_debug_assert_ne!(3, 4, "Hello, {}!", "Rust"); + } +} -- cgit