use once_cell::sync::Lazy;
use rustc_hash::FxHashMap;
use std::borrow::Cow;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[repr(u8)]
pub enum Locale {
#[default]
En = 0,
Ja = 1,
Zh = 2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ParseLocaleError;
impl std::fmt::Display for ParseLocaleError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "invalid locale string")
}
}
impl std::error::Error for ParseLocaleError {}
impl FromStr for Locale {
type Err = ParseLocaleError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.to_ascii_lowercase();
match s.as_str() {
"en" | "en-us" | "en-gb" | "english" => Ok(Self::En),
"ja" | "ja-jp" | "japanese" => Ok(Self::Ja),
"zh" | "zh-cn" | "zh-hans" | "chinese" => Ok(Self::Zh),
_ => Err(ParseLocaleError),
}
}
}
impl Locale {
pub const ALL: &'static [Locale] = &[Locale::En, Locale::Ja, Locale::Zh];
#[inline]
pub fn parse(s: &str) -> Option<Self> {
s.parse().ok()
}
#[inline]
pub const fn code(self) -> &'static str {
match self {
Self::En => "en",
Self::Ja => "ja",
Self::Zh => "zh",
}
}
#[inline]
pub const fn display_name(self) -> &'static str {
match self {
Self::En => "English",
Self::Ja => "日本語",
Self::Zh => "中文",
}
}
#[inline]
pub const fn index(self) -> usize {
self as usize
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Domain {
Lint,
Compiler,
Cli,
General,
}
impl Domain {
#[inline]
pub const fn prefix(self) -> &'static str {
match self {
Self::Lint => "lint",
Self::Compiler => "compiler",
Self::Cli => "cli",
Self::General => "general",
}
}
}
#[derive(Debug, Clone)]
pub enum Message {
Static(&'static str),
Owned(String),
}
impl Message {
#[inline]
pub fn as_str(&self) -> &str {
match self {
Message::Static(s) => s,
Message::Owned(s) => s,
}
}
}
impl From<&'static str> for Message {
#[inline]
fn from(s: &'static str) -> Self {
Message::Static(s)
}
}
impl From<String> for Message {
#[inline]
fn from(s: String) -> Self {
Message::Owned(s)
}
}
impl AsRef<str> for Message {
#[inline]
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl std::fmt::Display for Message {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
pub struct Translator {
messages: [FxHashMap<&'static str, &'static str>; 3],
}
impl Translator {
#[inline]
pub fn new() -> &'static Self {
&GLOBAL_TRANSLATOR
}
#[inline]
pub fn get(&self, locale: Locale, key: &str) -> Cow<'static, str> {
let idx = locale.index();
if let Some(msg) = self.messages[idx].get(key) {
return Cow::Borrowed(*msg);
}
if locale != Locale::En
&& let Some(msg) = self.messages[0].get(key)
{
return Cow::Borrowed(*msg);
}
Cow::Owned(key.to_string())
}
#[inline]
pub fn format(&self, locale: Locale, key: &str, vars: &[(&str, &str)]) -> String {
let template = self.get(locale, key);
if vars.is_empty() {
return template.into_owned();
}
let mut result = template.into_owned();
for (name, value) in vars {
#[allow(clippy::disallowed_macros)]
let placeholder = format!("{{{}}}", name);
result = result.replace(&placeholder, value);
}
result
}
#[inline]
pub fn has_key(&self, locale: Locale, key: &str) -> bool {
self.messages[locale.index()].contains_key(key)
}
pub fn keys(&self, locale: Locale) -> impl Iterator<Item = &'static str> + '_ {
self.messages[locale.index()].keys().copied()
}
}
impl Default for Translator {
fn default() -> Self {
Self::new().clone()
}
}
impl Clone for Translator {
fn clone(&self) -> Self {
Self {
messages: self.messages.clone(),
}
}
}
static GLOBAL_TRANSLATOR: Lazy<Translator> = Lazy::new(|| {
let mut messages: [FxHashMap<&'static str, &'static str>; 3] = [
FxHashMap::default(),
FxHashMap::default(),
FxHashMap::default(),
];
load_json(&mut messages[0], include_str!("i18n/en.json"));
load_json(&mut messages[1], include_str!("i18n/ja.json"));
load_json(&mut messages[2], include_str!("i18n/zh.json"));
crate::i18n_supplemental::register(&mut messages);
Translator { messages }
});
fn load_json(map: &mut FxHashMap<&'static str, &'static str>, json: &'static str) {
let json = json.trim();
if json.len() < 2 || !json.starts_with('{') || !json.ends_with('}') {
return;
}
let content = &json[1..json.len() - 1];
let mut idx = 0;
while idx < content.len() {
while idx < content.len() && content.as_bytes()[idx].is_ascii_whitespace() {
idx += 1;
}
if idx >= content.len() {
break;
}
if content.as_bytes()[idx] != b'"' {
idx += 1;
continue;
}
idx += 1;
let key_start = idx;
while idx < content.len() && content.as_bytes()[idx] != b'"' {
if content.as_bytes()[idx] == b'\\' {
idx += 2;
} else {
idx += 1;
}
}
let key_end = idx;
idx += 1;
while idx < content.len() && content.as_bytes()[idx] != b':' {
idx += 1;
}
idx += 1;
while idx < content.len() && content.as_bytes()[idx].is_ascii_whitespace() {
idx += 1;
}
if idx >= content.len() || content.as_bytes()[idx] != b'"' {
continue;
}
idx += 1;
let value_start = idx;
while idx < content.len() {
if content.as_bytes()[idx] == b'\\' {
idx += 2;
} else if content.as_bytes()[idx] == b'"' {
break;
} else {
idx += 1;
}
}
let value_end = idx;
idx += 1;
while idx < content.len() && content.as_bytes()[idx] != b',' {
idx += 1;
}
idx += 1;
let key = &content[key_start..key_end];
let value = &content[value_start..value_end];
let key: &'static str = Box::leak(key.to_string().into_boxed_str());
let value: &'static str = Box::leak(unescape_json_string(value).into_boxed_str());
map.insert(key, value);
}
}
#[inline]
fn unescape_json_string(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c == '\\' {
match chars.next() {
Some('n') => result.push('\n'),
Some('r') => result.push('\r'),
Some('t') => result.push('\t'),
Some('"') => result.push('"'),
Some('\\') => result.push('\\'),
Some('/') => result.push('/'),
Some('u') => {
let hex: String = chars.by_ref().take(4).collect();
if let Ok(cp) = u32::from_str_radix(&hex, 16)
&& let Some(c) = char::from_u32(cp)
{
result.push(c);
}
}
Some(other) => {
result.push('\\');
result.push(other);
}
None => result.push('\\'),
}
} else {
result.push(c);
}
}
result
}
#[inline]
pub fn translator() -> &'static Translator {
&GLOBAL_TRANSLATOR
}
#[inline]
pub fn t(locale: Locale, key: &str) -> Cow<'static, str> {
translator().get(locale, key)
}
#[inline]
pub fn t_fmt(locale: Locale, key: &str, vars: &[(&str, &str)]) -> String {
translator().format(locale, key, vars)
}
#[cfg(test)]
mod tests {
use super::{Locale, Translator, unescape_json_string};
#[test]
fn test_locale_from_str() {
assert_eq!("en".parse::<Locale>(), Ok(Locale::En));
assert_eq!("EN".parse::<Locale>(), Ok(Locale::En));
assert_eq!("ja".parse::<Locale>(), Ok(Locale::Ja));
assert_eq!("JA-JP".parse::<Locale>(), Ok(Locale::Ja));
assert_eq!("zh".parse::<Locale>(), Ok(Locale::Zh));
assert_eq!("zh-CN".parse::<Locale>(), Ok(Locale::Zh));
assert!("unknown".parse::<Locale>().is_err());
}
#[test]
fn test_locale_parse() {
assert_eq!(Locale::parse("en"), Some(Locale::En));
assert_eq!(Locale::parse("ja"), Some(Locale::Ja));
assert_eq!(Locale::parse("zh"), Some(Locale::Zh));
assert_eq!(Locale::parse("unknown"), None);
}
#[test]
fn test_locale_code() {
assert_eq!(Locale::En.code(), "en");
assert_eq!(Locale::Ja.code(), "ja");
assert_eq!(Locale::Zh.code(), "zh");
}
#[test]
fn test_locale_display_name() {
assert_eq!(Locale::En.display_name(), "English");
assert_eq!(Locale::Ja.display_name(), "日本語");
assert_eq!(Locale::Zh.display_name(), "中文");
}
#[test]
fn test_translator_get() {
let t = Translator::new();
let msg = t.get(Locale::En, "test.hello");
assert!(!msg.is_empty());
}
#[test]
fn test_translator_format() {
let t = Translator::new();
let msg = t.format(Locale::En, "test.greeting", &[("name", "World")]);
assert!(!msg.is_empty());
}
#[test]
fn test_unescape_json_string() {
assert_eq!(unescape_json_string("hello"), "hello");
assert_eq!(unescape_json_string("hello\\nworld"), "hello\nworld");
assert_eq!(unescape_json_string("hello\\tworld"), "hello\tworld");
assert_eq!(unescape_json_string("he said \\\"hi\\\""), "he said \"hi\"");
assert_eq!(unescape_json_string("path\\\\to\\\\file"), "path\\to\\file");
}
}