use std::collections::HashMap;
#[cfg(feature = "fs-loader")]
use std::path::Path;
use crate::core::Reloadable;
use crate::error::I18nError;
use crate::flat_backend::{FlatBackend, flatten_json};
fn parse_json(locale: &str, content: &str) -> Result<HashMap<String, String>, I18nError> {
let value: serde_json::Value =
serde_json::from_str(content).map_err(|e| I18nError::JsonParseError {
locale: locale.to_string(),
source: e,
})?;
let mut messages = HashMap::new();
flatten_json(&value, "", &mut messages);
Ok(messages)
}
pub struct JsonBackend {
inner: FlatBackend,
}
impl Default for JsonBackend {
fn default() -> Self {
Self::new()
}
}
impl JsonBackend {
pub fn new() -> Self {
Self {
inner: FlatBackend::new(),
}
}
pub fn from_embedded(pairs: &[(&str, &str)]) -> Result<Self, I18nError> {
let backend = Self::new();
for (locale, content) in pairs {
backend.add_locale_from_str(locale, content)?;
}
Ok(backend)
}
#[cfg(feature = "fs-loader")]
pub fn from_dir(path: impl AsRef<Path>) -> Result<Self, I18nError> {
let backend = Self::new();
backend
.inner
.load_dir(path, "json", &|locale, content| parse_json(locale, content))?;
Ok(backend)
}
#[cfg(feature = "fs-loader")]
pub fn try_from_dir(path: impl AsRef<Path>) -> Result<Self, I18nError> {
let backend = Self::new();
backend
.inner
.try_load_dir(path, "json", &|locale, content| parse_json(locale, content))?;
Ok(backend)
}
pub fn add_locale_from_str(&self, locale: &str, content: &str) -> Result<(), I18nError> {
let messages = parse_json(locale, content)?;
self.inner.add_locale(locale, messages);
Ok(())
}
pub fn inner(&self) -> &FlatBackend {
&self.inner
}
}
impl crate::core::Backend for JsonBackend {
fn get(&self, locale: &str, key: &str) -> Option<String> {
self.inner.get(locale, key)
}
fn available_locales(&self) -> Vec<String> {
self.inner.available_locales()
}
fn has_locale(&self, locale: &str) -> bool {
self.inner.has_locale(locale)
}
fn dump(&self, locale: &str) -> HashMap<String, String> {
self.inner.dump(locale)
}
}
impl Reloadable for JsonBackend {
fn reload_from_str(&self, locale: &str, content: &str) -> Result<(), I18nError> {
self.add_locale_from_str(locale, content)
}
fn file_extension(&self) -> &'static str {
"json"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Backend;
#[test]
fn test_flatten_simple() {
let json_str = r#"{
"app.title": "My App",
"app.desc": "A cool app"
}"#;
let backend = JsonBackend::new();
backend.add_locale_from_str("en", json_str).unwrap();
assert_eq!(backend.get("en", "app.title").unwrap(), "My App");
assert_eq!(backend.get("en", "app.desc").unwrap(), "A cool app");
}
#[test]
fn test_flatten_nested_object() {
let json_str = r#"{
"errors": {
"not_found": "Not found",
"unauthorized": "Unauthorized",
"auth": {
"expired": "Session expired"
}
}
}"#;
let backend = JsonBackend::new();
backend.add_locale_from_str("en", json_str).unwrap();
assert_eq!(backend.get("en", "errors.not_found").unwrap(), "Not found");
assert_eq!(
backend.get("en", "errors.auth.expired").unwrap(),
"Session expired"
);
}
#[test]
fn test_plural_group() {
let json_str = r#"{
"items": {
"zero": "No items",
"one": "One item",
"other": "%{count} items"
}
}"#;
let backend = JsonBackend::new();
backend.add_locale_from_str("en", json_str).unwrap();
assert_eq!(backend.get("en", "items.zero").unwrap(), "No items");
assert_eq!(backend.get("en", "items.one").unwrap(), "One item");
assert_eq!(backend.get("en", "items.other").unwrap(), "%{count} items");
}
#[test]
fn test_json_backend_implements_reloadable() {
let backend = JsonBackend::new();
assert_eq!(backend.file_extension(), "json");
backend.reload_from_str("en", r#"{"hello": "Hi"}"#).unwrap();
assert_eq!(backend.get("en", "hello").unwrap(), "Hi");
}
#[test]
fn test_reload_replaces_locale() {
let backend = JsonBackend::new();
backend
.add_locale_from_str("en", r#"{"greeting": "Hello"}"#)
.unwrap();
assert_eq!(backend.get("en", "greeting").unwrap(), "Hello");
backend
.reload_from_str("en", r#"{"farewell": "Goodbye"}"#)
.unwrap();
assert!(backend.get("en", "greeting").is_none());
assert_eq!(backend.get("en", "farewell").unwrap(), "Goodbye");
}
#[test]
fn test_from_embedded() {
let backend = JsonBackend::from_embedded(&[
("en", r#"{"hello": "Hello", "bye": "Goodbye"}"#),
("zh-CN", r#"{"hello": "你好", "bye": "再见"}"#),
])
.unwrap();
assert_eq!(backend.get("en", "hello").unwrap(), "Hello");
assert_eq!(backend.get("zh-CN", "hello").unwrap(), "你好");
assert_eq!(backend.available_locales().len(), 2);
assert!(backend.has_locale("en"));
assert!(backend.has_locale("zh-CN"));
assert!(!backend.has_locale("ja"));
}
#[test]
fn test_invalid_json_returns_error() {
let backend = JsonBackend::new();
let result = backend.add_locale_from_str("en", "not valid json {{{");
assert!(result.is_err());
}
#[cfg(feature = "fs-loader")]
#[test]
fn test_from_dir() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("en.json"),
r#"{"welcome": "Welcome!", "greeting": "Hello, %{name}!"}"#,
)
.unwrap();
std::fs::write(
dir.path().join("zh-CN.json"),
r#"{"welcome": "欢迎使用!", "greeting": "你好,%{name}!"}"#,
)
.unwrap();
let backend = JsonBackend::from_dir(dir.path()).unwrap();
assert_eq!(backend.get("en", "welcome").unwrap(), "Welcome!");
assert_eq!(backend.get("zh-CN", "welcome").unwrap(), "欢迎使用!");
assert_eq!(backend.available_locales().len(), 2);
}
}