Skip to content

Commit ae28fbd

Browse files
authored
Merge pull request #10 from FlakySL/feat/templating
Add templating
2 parents 42c6b92 + 385b4a4 commit ae28fbd

File tree

4 files changed

+213
-37
lines changed

4 files changed

+213
-37
lines changed

README.md

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55

66
A robust internationalization solution for Rust featuring compile-time validation, ISO 639-1 compliance, and TOML-based translation management.
77

8+
**This library prioritizes ergonomics over raw performance.**
9+
Our goal is not to be *blazingly fast* but to provide the most user-friendly experience for implementing translations—whether you're a first-time user or an experienced developer. If you require maximum performance, consider alternative libraries, a custom implementation, or even hard-coded values on the stack.
10+
811
## Table of Contents 📖
912

1013
- [Features](#features-)
@@ -63,7 +66,7 @@ The translation files have three rules
6366
The load configuration such as `seek_mode` and `overlap` is not relevant here, as previously
6467
specified, these configuration values only get applied once by reversing the translations conveniently.
6568

66-
To load translations you make use of the `translatable::translation` macro, that macro requires two
69+
To load translations you make use of the `translatable::translation` macro, that macro requires at least two
6770
parameters to be passed.
6871

6972
The first parameter consists of the language which can be passed dynamically as a variable or an expression
@@ -74,15 +77,22 @@ The second parameter consists of the path, which can be passed dynamically as a
7477
that resolves to an `impl Into<String>` with the format `path.to.translation`, or statically with the following
7578
syntax `static path::to::translation`.
7679

80+
The rest of parameters are `meta-variable patterns` also known as `key = value` parameters or key-value pairs,
81+
these are processed as replaces, *or format if the call is all-static*. When a template (`{}`) is found with
82+
the name of a key inside it gets replaced for whatever is the `Display` implementation of the value. This meaning
83+
that the value must always implement `Display`. Otherwise, if you want to have a `{}` inside your translation,
84+
you can escape it the same way `format!` does, by using `{{}}`. Just like object construction works in rust, if
85+
you have a parameter like `x = x`, you can shorten it to `x`.
86+
7787
Depending on whether the parameters are static or dynamic the macro will act different, differing whether
7888
the checks are compile-time or run-time, the following table is a macro behavior matrix.
7989

8090
| Parameters | Compile-Time checks | Return type |
8191
|----------------------------------------------------|----------------------------------------------------------|-----------------------------------------------------------------------------------|
82-
| `static language` + `static path` (most optimized) | Path existence, Language validity, \*Template validation | `&'static str` (stack) if there are no templates or `String` (heap) if there are. |
92+
| `static language` + `static path` (most optimized) | Path existence, Language validity | `&'static str` (stack) if there are no templates or `String` (heap) if there are. |
8393
| `dynamic language` + `dynamic path` | None | `Result<String, TranslatableError>` (heap) |
8494
| `static language` + `dynamic path` | Language validity | `Result<String, TranslatableError>` (heap) |
85-
| `dynamic language` + `static path` (commonly used) | Path existence, \*Template validation | `Result<String, TranslatableError>` (heap) |
95+
| `dynamic language` + `static path` (commonly used) | Path existence | `Result<String, TranslatableError>` (heap) |
8696

8797
- For the error handling, if you want to integrate this with `thiserror` you can use a `#[from] translatable::TranslationError`,
8898
as a nested error, all the errors implement display, for optimization purposes there are not the same amount of errors with
@@ -91,11 +101,6 @@ dynamic parameters than there are with static parameters.
91101
- The runtime errors implement a `cause()` method that returns a heap allocated `String` with the error reason, essentially
92102
the error display.
93103

94-
- Template validation in the static parameter handling means variable existence, since templates are generated as a `format!`
95-
call which processes expressions found in scope. It's always recommended to use full paths in translation templates
96-
to avoid needing to make variables in scope, unless the calls are contextual, in that case there is nothing that can
97-
be done to avoid making variables.
98-
99104
## Example implementation 📂
100105

101106
The following examples are an example application structure for a possible
@@ -129,22 +134,21 @@ es = "¡Hola {name}!"
129134

130135
### Example application usage
131136

132-
Notice how that template is in scope, whole expressions can be used
133-
in the templates such as `path::to::function()`, or other constants.
137+
Notice how there is a template, this template is being replaced by the
138+
`name = "john"` key value pair passed as third parameter.
134139

135140
```rust
136141
extern crate translatable;
137142
use translatable::translation;
138143

139144
fn main() {
140-
let dynamic_lang = "es";
141-
let dynamic_path = "common.greeting"
142-
let name = "john";
143-
144-
assert!(translation!("es", static common::greeting) == "¡Hola john!");
145-
assert!(translation!("es", dynamic_path).unwrap() == "¡Hola john!".into());
146-
assert!(translation!(dynamic_lang, static common::greeting).unwrap() == "¡Hola john!".into());
147-
assert!(translation!(dynamic_lang, dynamic_path).unwrap() == "¡Hola john!".into());
145+
let dynamic_lang = "es";
146+
let dynamic_path = "common.greeting"
147+
148+
assert!(translation!("es", static common::greeting) == "¡Hola john!", name = "john");
149+
assert!(translation!("es", dynamic_path).unwrap() == "¡Hola john!".into(), name = "john");
150+
assert!(translation!(dynamic_lang, static common::greeting).unwrap() == "¡Hola john!".into(), name = "john");
151+
assert!(translation!(dynamic_lang, dynamic_path).unwrap() == "¡Hola john!".into(), name = "john");
148152
}
149153
```
150154

translatable/tests/test.rs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,31 @@ use translatable::translation;
22

33
#[test]
44
fn both_static() {
5-
let name = "john";
6-
let result = translation!("es", static common::greeting);
5+
let result = translation!("es", static common::greeting, name = "john");
76

87
assert!(result == "¡Hola john!")
98
}
109

1110
#[test]
1211
fn language_static_path_dynamic() {
13-
let name = "john";
14-
let result = translation!("es", "common.greeting");
12+
let result = translation!("es", "common.greeting", name = "john");
1513

1614
assert!(result.unwrap() == "¡Hola john!".to_string())
1715
}
1816

1917
#[test]
2018
fn language_dynamic_path_static() {
21-
let name = "john";
2219
let language = "es";
23-
let result = translation!(language, static common::greeting);
20+
let name = "john";
21+
let result = translation!(language, static common::greeting, name = name);
2422

2523
assert!(result.unwrap() == "¡Hola john!".to_string())
2624
}
2725

2826
#[test]
2927
fn both_dynamic() {
30-
let name = "john";
3128
let language = "es";
32-
let result = translation!(language, "common.greeting");
29+
let result = translation!(language, "common.greeting", lol = 10, name = "john");
3330

3431
assert!(result.unwrap() == "¡Hola john!".to_string())
3532
}

translatable_proc/src/macros.rs

Lines changed: 97 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1+
use std::collections::HashMap;
2+
use std::fmt::Display;
3+
14
use proc_macro2::TokenStream;
2-
use quote::quote;
5+
use quote::{ToTokens, quote};
36
use syn::parse::{Parse, ParseStream};
7+
use syn::punctuated::Punctuated;
48
use syn::token::Static;
5-
use syn::{Expr, ExprLit, ExprPath, Lit, Result as SynResult, Token};
9+
use syn::{
10+
Expr, ExprLit, ExprPath, Ident, Lit, MetaNameValue, Path, Result as SynResult, Token,
11+
parse_quote,
12+
};
613

714
use crate::translations::generation::{
815
load_lang_dynamic, load_lang_static, load_translation_dynamic, load_translation_static,
@@ -24,6 +31,10 @@ pub struct RawMacroArgs {
2431
static_marker: Option<Static>,
2532
/// Translation path (either static path or dynamic expression)
2633
path: Expr,
34+
/// Optional comma separator for additional arguments
35+
_comma2: Option<Token![,]>,
36+
/// Format arguments for string interpolation
37+
format_kwargs: Punctuated<MetaNameValue, Token![,]>,
2738
}
2839

2940
/// Represents the type of translation path resolution
@@ -48,15 +59,73 @@ pub struct TranslationArgs {
4859
language: LanguageType,
4960
/// Path resolution type
5061
path: PathType,
62+
/// Format arguments for string interpolation
63+
format_kwargs: HashMap<String, TokenStream>,
5164
}
5265

5366
impl Parse for RawMacroArgs {
5467
fn parse(input: ParseStream) -> SynResult<Self> {
68+
let language = input.parse()?;
69+
let _comma = input.parse()?;
70+
let static_marker = input.parse()?;
71+
let path = input.parse()?;
72+
73+
// Parse optional comma before format arguments
74+
let _comma2 = if input.peek(Token![,]) { Some(input.parse()?) } else { None };
75+
76+
let mut format_kwargs = Punctuated::new();
77+
78+
// Parse format arguments if comma was present
79+
if _comma2.is_some() {
80+
while !input.is_empty() {
81+
let lookahead = input.lookahead1();
82+
83+
// Handle both identifier-based and arbitrary key-value pairs
84+
if lookahead.peek(Ident) {
85+
let key: Ident = input.parse()?;
86+
let eq_token: Token![=] = input.parse().unwrap_or(Token![=](key.span()));
87+
let mut value = input.parse::<Expr>();
88+
89+
if let Ok(value) = &mut value {
90+
let key_string = key.to_string();
91+
if key_string == value.to_token_stream().to_string() {
92+
// let warning = format!(
93+
// "redundant field initialier, use
94+
// `{key_string}` instead of `{key_string} = {key_string}`"
95+
// );
96+
97+
// Generate warning for redundant initializer
98+
*value = parse_quote! {{
99+
// compile_warn!(#warning);
100+
// !!! https://internals.rust-lang.org/t/pre-rfc-add-compile-warning-macro/9370 !!!
101+
#value
102+
}}
103+
}
104+
}
105+
106+
let value = value.unwrap_or(parse_quote!(#key));
107+
108+
format_kwargs.push(MetaNameValue { path: Path::from(key), eq_token, value });
109+
} else {
110+
format_kwargs.push(input.parse()?);
111+
}
112+
113+
// Continue parsing while commas are present
114+
if input.peek(Token![,]) {
115+
input.parse::<Token![,]>()?;
116+
} else {
117+
break;
118+
}
119+
}
120+
};
121+
55122
Ok(RawMacroArgs {
56-
language: input.parse()?,
57-
_comma: input.parse()?,
58-
static_marker: input.parse()?,
59-
path: input.parse()?,
123+
language,
124+
_comma,
125+
static_marker,
126+
path,
127+
_comma2,
128+
format_kwargs,
60129
})
61130
}
62131
}
@@ -68,6 +137,7 @@ impl From<RawMacroArgs> for TranslationArgs {
68137
TranslationArgs {
69138
// Extract language specification
70139
language: match val.language {
140+
// Handle string literals for compile-time validation
71141
Expr::Lit(ExprLit { lit: Lit::Str(lit_str), .. }) => {
72142
LanguageType::CompileTimeLiteral(lit_str.value())
73143
},
@@ -96,6 +166,23 @@ impl From<RawMacroArgs> for TranslationArgs {
96166
// Preserve dynamic path expressions
97167
path => PathType::OnScopeExpression(quote!(#path)),
98168
},
169+
170+
// Convert format arguments to HashMap with string keys
171+
format_kwargs: val
172+
.format_kwargs
173+
.iter()
174+
.map(|pair| {
175+
(
176+
// Extract key as identifier or stringified path
177+
pair.path
178+
.get_ident()
179+
.map(|i| i.to_string())
180+
.unwrap_or_else(|| pair.path.to_token_stream().to_string()),
181+
// Store value as token stream
182+
pair.value.to_token_stream(),
183+
)
184+
})
185+
.collect(),
99186
}
100187
}
101188
}
@@ -111,7 +198,7 @@ impl From<RawMacroArgs> for TranslationArgs {
111198
/// - Runtime translation resolution logic
112199
/// - Compile errors for invalid inputs
113200
pub fn translation_macro(args: TranslationArgs) -> TokenStream {
114-
let TranslationArgs { language, path } = args;
201+
let TranslationArgs { language, path, format_kwargs } = args;
115202

116203
// Process language specification
117204
let (lang_expr, static_lang) = match language {
@@ -129,8 +216,8 @@ pub fn translation_macro(args: TranslationArgs) -> TokenStream {
129216

130217
// Process translation path
131218
let translation_expr = match path {
132-
PathType::CompileTimePath(p) => load_translation_static(static_lang, p),
133-
PathType::OnScopeExpression(p) => load_translation_dynamic(static_lang, p),
219+
PathType::CompileTimePath(p) => load_translation_static(static_lang, p, format_kwargs),
220+
PathType::OnScopeExpression(p) => load_translation_dynamic(static_lang, p, format_kwargs),
134221
};
135222

136223
match (lang_expr, translation_expr) {
@@ -142,7 +229,7 @@ pub fn translation_macro(args: TranslationArgs) -> TokenStream {
142229
}
143230

144231
/// Helper function to create compile error tokens
145-
fn error_token(e: &impl std::fmt::Display) -> TokenStream {
232+
fn error_token(e: &impl Display) -> TokenStream {
146233
let msg = format!("{e:#}");
147234
quote! { compile_error!(#msg) }
148235
}

0 commit comments

Comments
 (0)