use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Locale(String);
impl Locale {
pub fn new<S: Into<String>>(s: S) -> Self {
Locale(s.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
pub struct I18n {
pub runtime_maps: HashMap<String, HashMap<String, String>>,
pub generated_registry:
Option<&'static [(&'static str, &'static phf::Map<&'static str, &'static str>)]>,
pub fallback: String,
}
impl I18n {
pub fn new<S: Into<String>>(fallback: S) -> Self {
Self {
runtime_maps: HashMap::new(),
generated_registry: None,
fallback: fallback.into(),
}
}
#[cfg(feature = "runtime-json")]
pub fn from_json_dir<P: AsRef<std::path::Path>>(
dir: P,
fallback: &str,
) -> Result<Self, String> {
let mut s = Self::new(fallback.to_string());
let dir = dir.as_ref();
if !dir.exists() {
return Err(format!("dir {} does not exist", dir.display()));
}
for entry in std::fs::read_dir(dir).map_err(|e| e.to_string())? {
let entry = entry.map_err(|e| e.to_string())?;
let p = entry.path();
if p.extension().and_then(|e| e.to_str()) != Some("json") {
continue;
}
let tag = p
.file_stem()
.and_then(|s| s.to_str())
.ok_or_else(|| "bad filename".to_string())?;
let content = std::fs::read_to_string(&p).map_err(|e| e.to_string())?;
let obj: serde_json::Map<String, serde_json::Value> =
serde_json::from_str(&content).map_err(|e| e.to_string())?;
let mut map = HashMap::new();
for (k, v) in obj.into_iter() {
if let Some(sv) = v.as_str() {
map.insert(k, sv.to_string());
} else {
return Err(format!("value for key {} in {} not string", k, p.display()));
}
}
s.runtime_maps.insert(tag.to_string(), map);
}
Ok(s)
}
pub fn from_generated_registry(
registry: &'static [(&'static str, &'static phf::Map<&'static str, &'static str>)],
fallback: &str,
) -> Self {
Self {
runtime_maps: HashMap::new(),
generated_registry: Some(registry),
fallback: fallback.into(),
}
}
fn normalize_tag(tag: &str) -> String {
let parts: Vec<&str> = tag.split('-').filter(|p| !p.is_empty()).collect();
if parts.is_empty() {
return "".into();
}
let mut out: Vec<String> = Vec::new();
for (i, p) in parts.iter().enumerate() {
if i == 0 {
out.push(p.to_lowercase());
} else {
out.push(p.to_uppercase());
}
}
out.join("-")
}
fn resolve_generated_map(
&self,
tag: &str,
) -> Option<&'static phf::Map<&'static str, &'static str>> {
let reg = self.generated_registry?;
let mut tag = Self::normalize_tag(tag);
if tag.is_empty() {
return None;
}
loop {
for (t, map) in reg.iter() {
if *t == tag {
return Some(*map);
}
}
if let Some(idx) = tag.rfind('-') {
tag.truncate(idx);
} else {
break;
}
}
None
}
fn resolve_runtime_map(&self, tag: &str) -> Option<&HashMap<String, String>> {
let mut tag = Self::normalize_tag(tag);
if tag.is_empty() {
return None;
}
loop {
if let Some(m) = self.runtime_maps.get(&tag) {
return Some(m);
}
if let Some(idx) = tag.rfind('-') {
tag.truncate(idx);
} else {
break;
}
}
None
}
pub fn get_by_str_key(&self, locale: &Locale, key: &str) -> Option<&'static str> {
if let Some(m) = self.resolve_generated_map(locale.as_str()) {
if let Some(v) = m.get(key) {
return Some(*v);
}
}
if let Some(map) = self.resolve_runtime_map(locale.as_str()) {
if let Some(_) = map.get(key) {
return None;
}
}
if let Some(m) = self.resolve_generated_map(&self.fallback) {
if let Some(v) = m.get(key) {
return Some(*v);
}
}
None
}
pub fn get_owned(&self, locale: &Locale, key: &str) -> Option<String> {
if let Some(map) = self.resolve_runtime_map(locale.as_str()) {
if let Some(v) = map.get(key) {
return Some(v.clone());
}
}
if let Some(map) = self.resolve_runtime_map(&self.fallback) {
if let Some(v) = map.get(key) {
return Some(v.clone());
}
}
if let Some(m) = self.resolve_generated_map(locale.as_str()) {
if let Some(v) = m.get(key) {
return Some(v.to_string());
}
}
if let Some(m) = self.resolve_generated_map(&self.fallback) {
if let Some(v) = m.get(key) {
return Some(v.to_string());
}
}
None
}
}