#![doc = include_str!("../README.md")]
use std::{
cell::RefCell,
ops::Deref,
sync::{
atomic::{AtomicU64, Ordering},
LazyLock,
},
};
#[doc(hidden)]
pub use rust_i18n_macro::{_minify_key, _tr, i18n};
#[cfg(feature = "load-path")]
pub use rust_i18n_support::try_load_locales;
pub use rust_i18n_support::{
AtomicStr, Backend, BackendExt, CowStr, MinifyKey, NamespacedBackend, SimpleBackend,
DEFAULT_MINIFY_KEY, DEFAULT_MINIFY_KEY_LEN, DEFAULT_MINIFY_KEY_PREFIX,
DEFAULT_MINIFY_KEY_THRESH,
};
static CURRENT_LOCALE: LazyLock<AtomicStr> = LazyLock::new(|| AtomicStr::from("en"));
static LOCALE_VERSION: AtomicU64 = AtomicU64::new(0);
thread_local! {
static LOCALE_CACHE: RefCell<Option<(u64, LocaleStr)>> = const { RefCell::new(None) };
}
const INLINE_LOCALE_LEN: usize = 22;
#[derive(Clone)]
enum LocaleStr {
Inline {
len: u8,
bytes: [u8; INLINE_LOCALE_LEN],
},
Shared(std::sync::Arc<str>),
}
impl LocaleStr {
fn new(locale: &str) -> Self {
if locale.len() <= INLINE_LOCALE_LEN {
let mut bytes = [0; INLINE_LOCALE_LEN];
bytes[..locale.len()].copy_from_slice(locale.as_bytes());
Self::Inline {
len: locale.len() as u8,
bytes,
}
} else {
Self::Shared(locale.into())
}
}
}
impl Deref for LocaleStr {
type Target = str;
#[inline]
fn deref(&self) -> &str {
match self {
Self::Inline { len, bytes } => unsafe {
std::str::from_utf8_unchecked(bytes.get_unchecked(..*len as usize))
},
Self::Shared(locale) => locale,
}
}
}
pub fn set_locale(locale: &str) {
CURRENT_LOCALE.replace(locale);
LOCALE_VERSION.fetch_add(1, Ordering::Release);
}
#[inline]
pub fn locale() -> impl Deref<Target = str> {
let version = LOCALE_VERSION.load(Ordering::Acquire);
LOCALE_CACHE
.try_with(|cache| {
let mut cache = cache.borrow_mut();
match &*cache {
Some((cached, locale)) if *cached == version => locale.clone(),
_ => {
let locale = LocaleStr::new(&CURRENT_LOCALE.as_str());
*cache = Some((version, locale.clone()));
locale
}
}
})
.unwrap_or_else(|_| LocaleStr::new(&CURRENT_LOCALE.as_str()))
}
pub fn replace_patterns(input: &str, patterns: &[&str], values: &[String]) -> String {
replace_patterns_impl(input, patterns, values)
}
#[doc(hidden)]
pub fn replace_patterns_cow(
input: &str,
patterns: &[&str],
values: &[std::borrow::Cow<'_, str>],
) -> String {
replace_patterns_impl(input, patterns, values)
}
fn replace_patterns_impl<V: AsRef<str>>(input: &str, patterns: &[&str], values: &[V]) -> String {
replace_patterns_fast(input, patterns, values)
.unwrap_or_else(|| replace_patterns_legacy(input, patterns, values))
}
fn replace_patterns_fast<V: AsRef<str>>(
input: &str,
patterns: &[&str],
values: &[V],
) -> Option<String> {
let bytes = input.as_bytes();
let mut output = Vec::with_capacity(bytes.len() + 128);
let mut offset = 0;
loop {
let Some(percent) = bytes[offset..].iter().position(|&byte| byte == b'%') else {
output.extend_from_slice(&bytes[offset..]);
return Some(unsafe { String::from_utf8_unchecked(output) });
};
let percent = offset + percent;
if bytes.get(percent + 1) != Some(&b'{') {
return None;
}
let start = percent + 2;
let Some(end) = bytes[start..]
.iter()
.position(|&byte| byte == b'}' || byte == b'%')
else {
output.extend_from_slice(&bytes[offset..]);
return Some(unsafe { String::from_utf8_unchecked(output) });
};
let end = end + start;
if bytes[end] == b'%' {
return None;
}
output.extend_from_slice(&bytes[offset..percent]);
let key = &bytes[start..end];
if let Some((_, value)) = patterns
.iter()
.zip(values)
.find(|(&pattern, _)| pattern.as_bytes() == key)
{
output.extend_from_slice(value.as_ref().as_bytes());
} else {
output.extend_from_slice(&bytes[percent..=end]);
}
offset = end + 1;
}
}
fn replace_patterns_legacy<V: AsRef<str>>(input: &str, patterns: &[&str], values: &[V]) -> String {
let input_bytes = input.as_bytes();
let mut pattern_pos = smallvec::SmallVec::<[usize; 64]>::new();
let mut stage = 0;
for (i, &b) in input_bytes.iter().enumerate() {
match (stage, b) {
(1, b'{') => {
stage = 2;
pattern_pos.push(i);
}
(2, b'}') => {
stage = 0;
pattern_pos.push(i);
}
(_, b'%') => {
stage = 1;
}
_ => {}
}
}
let mut output: Vec<u8> = Vec::with_capacity(input_bytes.len() + 128);
let mut prev_end = 0;
let pattern_values = patterns.iter().zip(values.iter());
for pos in pattern_pos.chunks_exact(2) {
let start = pos[0];
let end = pos[1];
let key = &input_bytes[start + 1..end];
if prev_end < start {
let prev_chunk = &input_bytes[prev_end..start - 1];
output.extend_from_slice(prev_chunk);
}
if let Some((_, v)) = pattern_values
.clone()
.find(|(&pattern, _)| pattern.as_bytes() == key)
{
output.extend_from_slice(v.as_ref().as_bytes());
} else {
output.extend_from_slice(&input_bytes[start - 1..end + 1]);
}
prev_end = end + 1;
}
if prev_end < input_bytes.len() {
let remaining = &input_bytes[prev_end..];
output.extend_from_slice(remaining);
}
unsafe { String::from_utf8_unchecked(output) }
}
#[cfg(test)]
mod replace_patterns_tests {
use super::{replace_patterns, replace_patterns_cow, replace_patterns_legacy};
use std::borrow::Cow;
#[test]
fn replaces_multiple_placeholders_without_changing_surrounding_unicode() {
let values = ["Jason".to_string(), "世界".to_string()];
assert_eq!(
replace_patterns(
"你好,%{name}!Welcome to %{place}.",
&["name", "place"],
&values
),
"你好,Jason!Welcome to 世界."
);
}
#[test]
fn preserves_unknown_and_incomplete_placeholders() {
let values = ["Jason".to_string()];
assert_eq!(
replace_patterns("%{unknown} %{name} %{unfinished", &["name"], &values),
"%{unknown} Jason %{unfinished"
);
}
#[test]
fn long_input_uses_the_same_replacements() {
let input = format!(
"{}%{{name}}{}%{{missing}}",
"界".repeat(128),
"a".repeat(512)
);
let values = ["世界".to_string()];
assert_eq!(
replace_patterns(&input, &["name"], &values),
replace_patterns_legacy(&input, &["name"], &values)
);
}
#[test]
fn cow_values_match_string_values_for_complete_and_malformed_inputs() {
let string_values = ["Jason".to_string(), "世界".to_string()];
let cow_values = [Cow::Borrowed("Jason"), Cow::Owned("世界".to_string())];
let patterns = ["name", "place"];
let long_input = format!("{}%{{name}}{}%{{place}}", "界".repeat(128), "a".repeat(512));
for input in [
"你好,%{name}!Welcome to %{place}.",
"%{unknown} %{name} %{unfinished",
"%{name}%{place}",
"plain text",
"unexpected % marker",
"%{name%{place}",
long_input.as_str(),
] {
assert_eq!(
replace_patterns_cow(input, &patterns, &cow_values),
replace_patterns(input, &patterns, &string_values),
"input: {input:?}"
);
}
}
#[test]
fn fast_path_matches_legacy_for_short_marker_sequences() {
const ALPHABET: &[u8] = b"%{}ax";
let values = ["世界".to_string(), "%{".to_string()];
let cow_values = [Cow::Borrowed("世界"), Cow::Owned("%{".to_string())];
for len in 0..=7_u32 {
for mut code in 0..ALPHABET.len().pow(len) {
let mut input = String::new();
for _ in 0..len {
input.push(ALPHABET[code % ALPHABET.len()] as char);
code /= ALPHABET.len();
}
assert_eq!(
replace_patterns(&input, &["a", "x"], &values),
replace_patterns_legacy(&input, &["a", "x"], &values),
"input: {input:?}"
);
assert_eq!(
replace_patterns_cow(&input, &["a", "x"], &cow_values),
replace_patterns(&input, &["a", "x"], &values),
"input: {input:?}"
);
}
}
}
}
#[macro_export]
#[allow(clippy::crate_in_macro_def)]
macro_rules! t {
($($all:tt)*) => {
crate::_rust_i18n_t!($($all)*)
}
}
#[macro_export]
#[allow(clippy::crate_in_macro_def)]
macro_rules! tkv {
($msg:literal) => {
crate::_rust_i18n_tkv!($msg)
};
}
#[macro_export(local_inner_macros)]
#[allow(clippy::crate_in_macro_def)]
macro_rules! available_locales {
() => {
crate::_rust_i18n_available_locales()
};
}
#[macro_export]
macro_rules! extend {
($target:ident) => {
$target::_rust_i18n_extend(crate::_rust_i18n_backend(), stringify!($target))
};
}
#[cfg(test)]
mod tests {
use crate::{locale, CURRENT_LOCALE};
fn assert_locale_type(s: &str, val: &str) {
assert_eq!(s, val);
}
#[test]
fn test_locale() {
assert_locale_type(&locale(), &CURRENT_LOCALE.as_str());
assert_eq!(&*locale(), "en");
}
}