1#![doc = include_str!("../README.md")]
2
3use std::{
4 cell::RefCell,
5 ops::Deref,
6 sync::{
7 atomic::{AtomicU64, Ordering},
8 LazyLock,
9 },
10};
11
12#[doc(hidden)]
13pub use rust_i18n_macro::{_minify_key, _tr, i18n};
14#[cfg(feature = "load-path")]
15pub use rust_i18n_support::try_load_locales;
16pub use rust_i18n_support::{
17 AtomicStr, Backend, BackendExt, CowStr, MinifyKey, NamespacedBackend, SimpleBackend,
18 DEFAULT_MINIFY_KEY, DEFAULT_MINIFY_KEY_LEN, DEFAULT_MINIFY_KEY_PREFIX,
19 DEFAULT_MINIFY_KEY_THRESH,
20};
21
22static CURRENT_LOCALE: LazyLock<AtomicStr> = LazyLock::new(|| AtomicStr::from("en"));
23
24static LOCALE_VERSION: AtomicU64 = AtomicU64::new(0);
28
29thread_local! {
30 static LOCALE_CACHE: RefCell<Option<(u64, LocaleStr)>> = const { RefCell::new(None) };
31}
32
33const INLINE_LOCALE_LEN: usize = 22;
34
35#[derive(Clone)]
38enum LocaleStr {
39 Inline {
40 len: u8,
41 bytes: [u8; INLINE_LOCALE_LEN],
42 },
43 Shared(std::sync::Arc<str>),
44}
45
46impl LocaleStr {
47 fn new(locale: &str) -> Self {
48 if locale.len() <= INLINE_LOCALE_LEN {
49 let mut bytes = [0; INLINE_LOCALE_LEN];
50 bytes[..locale.len()].copy_from_slice(locale.as_bytes());
51 Self::Inline {
52 len: locale.len() as u8,
53 bytes,
54 }
55 } else {
56 Self::Shared(locale.into())
57 }
58 }
59}
60
61impl Deref for LocaleStr {
62 type Target = str;
63
64 #[inline]
65 fn deref(&self) -> &str {
66 match self {
67 Self::Inline { len, bytes } => unsafe {
69 std::str::from_utf8_unchecked(bytes.get_unchecked(..*len as usize))
70 },
71 Self::Shared(locale) => locale,
72 }
73 }
74}
75
76pub fn set_locale(locale: &str) {
78 CURRENT_LOCALE.replace(locale);
79 LOCALE_VERSION.fetch_add(1, Ordering::Release);
82}
83
84#[inline]
86pub fn locale() -> impl Deref<Target = str> {
87 let version = LOCALE_VERSION.load(Ordering::Acquire);
88 LOCALE_CACHE
89 .try_with(|cache| {
90 let mut cache = cache.borrow_mut();
91 match &*cache {
92 Some((cached, locale)) if *cached == version => locale.clone(),
93 _ => {
94 let locale = LocaleStr::new(&CURRENT_LOCALE.as_str());
95 *cache = Some((version, locale.clone()));
96 locale
97 }
98 }
99 })
100 .unwrap_or_else(|_| LocaleStr::new(&CURRENT_LOCALE.as_str()))
103}
104
105pub fn replace_patterns(input: &str, patterns: &[&str], values: &[String]) -> String {
124 replace_patterns_impl(input, patterns, values)
125}
126
127#[doc(hidden)]
129pub fn replace_patterns_cow(
130 input: &str,
131 patterns: &[&str],
132 values: &[std::borrow::Cow<'_, str>],
133) -> String {
134 replace_patterns_impl(input, patterns, values)
135}
136
137fn replace_patterns_impl<V: AsRef<str>>(input: &str, patterns: &[&str], values: &[V]) -> String {
138 replace_patterns_fast(input, patterns, values)
139 .unwrap_or_else(|| replace_patterns_legacy(input, patterns, values))
140}
141
142fn replace_patterns_fast<V: AsRef<str>>(
145 input: &str,
146 patterns: &[&str],
147 values: &[V],
148) -> Option<String> {
149 let bytes = input.as_bytes();
150 let mut output = Vec::with_capacity(bytes.len() + 128);
151 let mut offset = 0;
152 loop {
153 let Some(percent) = bytes[offset..].iter().position(|&byte| byte == b'%') else {
154 output.extend_from_slice(&bytes[offset..]);
155 return Some(unsafe { String::from_utf8_unchecked(output) });
158 };
159 let percent = offset + percent;
160 if bytes.get(percent + 1) != Some(&b'{') {
161 return None;
162 }
163 let start = percent + 2;
164 let Some(end) = bytes[start..]
165 .iter()
166 .position(|&byte| byte == b'}' || byte == b'%')
167 else {
168 output.extend_from_slice(&bytes[offset..]);
170 return Some(unsafe { String::from_utf8_unchecked(output) });
172 };
173 let end = end + start;
174 if bytes[end] == b'%' {
175 return None;
176 }
177 output.extend_from_slice(&bytes[offset..percent]);
178 let key = &bytes[start..end];
179 if let Some((_, value)) = patterns
180 .iter()
181 .zip(values)
182 .find(|(&pattern, _)| pattern.as_bytes() == key)
183 {
184 output.extend_from_slice(value.as_ref().as_bytes());
185 } else {
186 output.extend_from_slice(&bytes[percent..=end]);
187 }
188 offset = end + 1;
189 }
190}
191
192fn replace_patterns_legacy<V: AsRef<str>>(input: &str, patterns: &[&str], values: &[V]) -> String {
193 let input_bytes = input.as_bytes();
194 let mut pattern_pos = smallvec::SmallVec::<[usize; 64]>::new();
195 let mut stage = 0;
196 for (i, &b) in input_bytes.iter().enumerate() {
197 match (stage, b) {
198 (1, b'{') => {
199 stage = 2;
200 pattern_pos.push(i);
201 }
202 (2, b'}') => {
203 stage = 0;
204 pattern_pos.push(i);
205 }
206 (_, b'%') => {
207 stage = 1;
208 }
209 _ => {}
210 }
211 }
212 let mut output: Vec<u8> = Vec::with_capacity(input_bytes.len() + 128);
213 let mut prev_end = 0;
214 let pattern_values = patterns.iter().zip(values.iter());
215 for pos in pattern_pos.chunks_exact(2) {
216 let start = pos[0];
217 let end = pos[1];
218 let key = &input_bytes[start + 1..end];
219 if prev_end < start {
220 let prev_chunk = &input_bytes[prev_end..start - 1];
221 output.extend_from_slice(prev_chunk);
222 }
223 if let Some((_, v)) = pattern_values
224 .clone()
225 .find(|(&pattern, _)| pattern.as_bytes() == key)
226 {
227 output.extend_from_slice(v.as_ref().as_bytes());
228 } else {
229 output.extend_from_slice(&input_bytes[start - 1..end + 1]);
230 }
231 prev_end = end + 1;
232 }
233 if prev_end < input_bytes.len() {
234 let remaining = &input_bytes[prev_end..];
235 output.extend_from_slice(remaining);
236 }
237 unsafe { String::from_utf8_unchecked(output) }
238}
239
240#[cfg(test)]
241mod replace_patterns_tests {
242 use super::{replace_patterns, replace_patterns_cow, replace_patterns_legacy};
243 use std::borrow::Cow;
244
245 #[test]
246 fn replaces_multiple_placeholders_without_changing_surrounding_unicode() {
247 let values = ["Jason".to_string(), "世界".to_string()];
248 assert_eq!(
249 replace_patterns(
250 "你好,%{name}!Welcome to %{place}.",
251 &["name", "place"],
252 &values
253 ),
254 "你好,Jason!Welcome to 世界."
255 );
256 }
257
258 #[test]
259 fn preserves_unknown_and_incomplete_placeholders() {
260 let values = ["Jason".to_string()];
261 assert_eq!(
262 replace_patterns("%{unknown} %{name} %{unfinished", &["name"], &values),
263 "%{unknown} Jason %{unfinished"
264 );
265 }
266
267 #[test]
268 fn long_input_uses_the_same_replacements() {
269 let input = format!(
270 "{}%{{name}}{}%{{missing}}",
271 "界".repeat(128),
272 "a".repeat(512)
273 );
274 let values = ["世界".to_string()];
275 assert_eq!(
276 replace_patterns(&input, &["name"], &values),
277 replace_patterns_legacy(&input, &["name"], &values)
278 );
279 }
280
281 #[test]
282 fn cow_values_match_string_values_for_complete_and_malformed_inputs() {
283 let string_values = ["Jason".to_string(), "世界".to_string()];
284 let cow_values = [Cow::Borrowed("Jason"), Cow::Owned("世界".to_string())];
285 let patterns = ["name", "place"];
286 let long_input = format!("{}%{{name}}{}%{{place}}", "界".repeat(128), "a".repeat(512));
287
288 for input in [
289 "你好,%{name}!Welcome to %{place}.",
290 "%{unknown} %{name} %{unfinished",
291 "%{name}%{place}",
292 "plain text",
293 "unexpected % marker",
294 "%{name%{place}",
295 long_input.as_str(),
296 ] {
297 assert_eq!(
298 replace_patterns_cow(input, &patterns, &cow_values),
299 replace_patterns(input, &patterns, &string_values),
300 "input: {input:?}"
301 );
302 }
303 }
304
305 #[test]
306 fn fast_path_matches_legacy_for_short_marker_sequences() {
307 const ALPHABET: &[u8] = b"%{}ax";
308 let values = ["世界".to_string(), "%{".to_string()];
309 let cow_values = [Cow::Borrowed("世界"), Cow::Owned("%{".to_string())];
310 for len in 0..=7_u32 {
311 for mut code in 0..ALPHABET.len().pow(len) {
312 let mut input = String::new();
313 for _ in 0..len {
314 input.push(ALPHABET[code % ALPHABET.len()] as char);
315 code /= ALPHABET.len();
316 }
317 assert_eq!(
318 replace_patterns(&input, &["a", "x"], &values),
319 replace_patterns_legacy(&input, &["a", "x"], &values),
320 "input: {input:?}"
321 );
322 assert_eq!(
323 replace_patterns_cow(&input, &["a", "x"], &cow_values),
324 replace_patterns(&input, &["a", "x"], &values),
325 "input: {input:?}"
326 );
327 }
328 }
329 }
330}
331
332#[macro_export]
381#[allow(clippy::crate_in_macro_def)]
382macro_rules! t {
383 ($($all:tt)*) => {
384 crate::_rust_i18n_t!($($all)*)
385 }
386}
387
388#[macro_export]
413#[allow(clippy::crate_in_macro_def)]
414macro_rules! tkv {
415 ($msg:literal) => {
416 crate::_rust_i18n_tkv!($msg)
417 };
418}
419
420#[macro_export(local_inner_macros)]
431#[allow(clippy::crate_in_macro_def)]
432macro_rules! available_locales {
433 () => {
434 crate::_rust_i18n_available_locales()
435 };
436}
437
438#[macro_export]
454macro_rules! extend {
455 ($target:ident) => {
456 $target::_rust_i18n_extend(crate::_rust_i18n_backend(), stringify!($target))
457 };
458}
459
460#[cfg(test)]
461mod tests {
462 use crate::{locale, CURRENT_LOCALE};
463
464 fn assert_locale_type(s: &str, val: &str) {
465 assert_eq!(s, val);
466 }
467
468 #[test]
469 fn test_locale() {
470 assert_locale_type(&locale(), &CURRENT_LOCALE.as_str());
471 assert_eq!(&*locale(), "en");
472 }
473}