Skip to main content

rust_i18n/
lib.rs

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
24// Incremented after every store to `CURRENT_LOCALE`. Each thread keeps a copy
25// of the locale with the version it was read at, and reloads from
26// `CURRENT_LOCALE` only when the version changes.
27static 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/// A copy of the current locale. Short locales are stored inline, so copying
36/// one from the thread cache needs no allocation or atomic reference count.
37#[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            // SAFETY: `bytes[..len]` was copied from a `str` in `LocaleStr::new`.
68            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
76/// Set current locale
77pub fn set_locale(locale: &str) {
78    CURRENT_LOCALE.replace(locale);
79    // Release pairs with the Acquire load in `locale()`, so a reader that sees
80    // the new version also sees the new locale.
81    LOCALE_VERSION.fetch_add(1, Ordering::Release);
82}
83
84/// Get current locale
85#[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        // The thread cache is unavailable while thread-local storage is
101        // being destroyed.
102        .unwrap_or_else(|_| LocaleStr::new(&CURRENT_LOCALE.as_str()))
103}
104
105/// Replace patterns and return a new string.
106///
107/// # Arguments
108///
109/// * `input` - The input string, containing patterns like `%{name}`.
110/// * `patterns` - The patterns to replace.
111/// * `values` - The values to replace.
112///
113/// # Example
114///
115/// ```
116/// # use rust_i18n::replace_patterns;
117/// let input = "Hello, %{name}!";
118/// let patterns = &["name"];
119/// let values = &["world".to_string()];
120/// let output = replace_patterns(input, patterns, values);
121/// assert_eq!(output, "Hello, world!");
122/// ```
123pub fn replace_patterns(input: &str, patterns: &[&str], values: &[String]) -> String {
124    replace_patterns_impl(input, patterns, values)
125}
126
127/// Replace patterns using borrowed or owned values from generated macros.
128#[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
142// The common form has complete `%{name}` markers. Keep the original state
143// machine for malformed input, whose historical behavior is more permissive.
144fn 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            // SAFETY: Each copied input slice begins or ends at an ASCII marker
156            // boundary, so it remains valid UTF-8. Replacements are strings.
157            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            // An unfinished final marker is literal text in the legacy parser.
169            output.extend_from_slice(&bytes[offset..]);
170            // SAFETY: As above, all copied slices have UTF-8 boundaries.
171            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/// Get I18n text
333///
334/// This macro forwards to the `crate::_rust_i18n_t!` macro, which is generated by the [`i18n!`] macro.
335///
336/// # Arguments
337///
338/// * `expr` - The key or message for translation.
339///   - A key usually looks like `"foo.bar.baz"`.
340///   - A literal message usually looks like `"Hello, world!"`.
341///   - The variable names in the message should be wrapped in `%{}`, like `"Hello, %{name}!"`.
342///   - Dynamic messages are also supported, such as `t!(format!("Hello, {}!", name))`.
343///     However, if `minify_key` is enabled, the entire message will be hashed and used as a key for every lookup, which may consume more CPU cycles.
344/// * `locale` - The locale to use. If not specified, the current locale will be used.
345/// * `args` - The arguments to be replaced in the translated text.
346///    - These should be passed in the format `key = value` or `key => value`.
347///    - Alternatively, you can specify the value format using the `key = value : {:format_specifier}` syntax.
348///      For example, `key = value : {:08}` will format the value as a zero-padded string with a length of 8.
349///
350/// # Example
351///
352/// ```no_run
353/// #[macro_use] extern crate rust_i18n;
354///
355/// # macro_rules! t { ($($all:tt)*) => {} }
356/// # fn main() {
357/// // Simple get text with current locale
358/// t!("greeting");
359/// // greeting: "Hello world" => "Hello world"
360///
361/// // Get a special locale's text
362/// t!("greeting", locale = "de");
363/// // greeting: "Hallo Welt!" => "Hallo Welt!"
364///
365/// // With variables
366/// t!("messages.hello", name = "world");
367/// // messages.hello: "Hello, %{name}" => "Hello, world"
368/// t!("messages.foo", name = "Foo", other ="Bar");
369/// // messages.foo: "Hello, %{name} and %{other}" => "Hello, Foo and Bar"
370///
371/// // With variables and format specifiers
372/// t!("Hello, %{name}, you serial number is: %{sn}", name = "Jason", sn = 123 : {:08});
373/// // => "Hello, Jason, you serial number is: 000000123"
374///
375/// // With locale and variables
376/// t!("messages.hello", locale = "de", name = "Jason");
377/// // messages.hello: "Hallo, %{name}" => "Hallo, Jason"
378/// # }
379/// ```
380#[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/// A macro that generates a translation key and corresponding value pair from a given input value.
389///
390/// It's useful when you want to use a long string as a key, but you don't want to type it twice.
391///
392/// # Arguments
393///
394/// * `msg` - The input value.
395///
396/// # Returns
397///
398/// A tuple of `(key, msg)`.
399///
400/// # Example
401///
402/// ```no_run
403/// use rust_i18n::{t, tkv};
404///
405/// # macro_rules! t { ($($all:tt)*) => { } }
406/// # macro_rules! tkv { ($($all:tt)*) => { (1,2) } }
407///
408/// let (key, msg) = tkv!("Hello world");
409/// // => key is `"Hello world"` and msg is the translated message.
410/// // => If there is hints the minify_key logic, the key will returns a minify key.
411/// ```
412#[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/// Get available locales
421///
422/// ```no_run
423/// #[macro_use] extern crate rust_i18n;
424/// # pub fn _rust_i18n_available_locales() -> Vec<&'static str> { todo!() }
425/// # fn main() {
426/// rust_i18n::available_locales!();
427/// # }
428/// // => ["en", "zh-CN"]
429/// ```
430#[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/// Extend a dependency's translations with the matching crate namespace from
439/// the current crate's backend.
440///
441/// Given `extend!(ui_component)`, translations below the `ui_component` key in
442/// the current crate are lazily merged with translations in the `ui_component`
443/// crate. The extension takes priority for the same locale and key.
444///
445/// The lookup order for each locale and key is equivalent to:
446///
447/// ```rs, ignore
448/// app.translate(key)
449///     .or_else(|| ui_component.translate(key))
450/// ```
451///
452/// If both miss, the existing locale fallback rules continue as usual.
453#[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}