aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcopi143 <copi143@outlook.com>2026-01-17 04:02:47 +0800
committercopi143 <copi143@outlook.com>2026-01-17 04:02:47 +0800
commitc38d117b5803c65ad57a4c4704ee0d897d9fb3cb (patch)
tree006977b48229ab2ef282bce705fc4caf60160252
init
-rw-r--r--.gitignore1
-rw-r--r--Cargo.lock151
-rw-r--r--Cargo.toml3
-rw-r--r--LICENSE121
-rw-r--r--README.md66
-rw-r--r--derive/Cargo.toml19
-rw-r--r--derive/README.md133
-rw-r--r--derive/src/lib.rs957
-rw-r--r--test/Cargo.toml13
-rw-r--r--test/lang/src.toml14
-rw-r--r--test/src/main.rs79
11 files changed, 1557 insertions, 0 deletions
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 <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
+}
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");
+ }
+}