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_yaml(locale: &str, content: &str) -> Result<HashMap<String, String>, I18nError> {
let yaml_value: serde_yaml::Value =
serde_yaml::from_str(content).map_err(|e| I18nError::YamlParseError {
locale: locale.to_string(),
source: e,
})?;
let json_value: serde_json::Value =
serde_json::to_value(&yaml_value).map_err(|e| I18nError::YamlParseError {
locale: locale.to_string(),
source: serde_yaml::from_str::<serde_yaml::Value>(&format!(
"failed to convert YAML to JSON: {e}"
))
.unwrap_err(),
})?;
let mut messages = HashMap::new();
flatten_json(&json_value, "", &mut messages);
Ok(messages)
}
pub struct YamlBackend {
inner: FlatBackend,
}
impl Default for YamlBackend {
fn default() -> Self {
Self::new()
}
}
impl YamlBackend {
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, "yaml", &|locale, content| parse_yaml(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, "yaml", &|locale, content| parse_yaml(locale, content))?;
Ok(backend)
}
pub fn add_locale_from_str(&self, locale: &str, content: &str) -> Result<(), I18nError> {
let messages = parse_yaml(locale, content)?;
self.inner.add_locale(locale, messages);
Ok(())
}
pub fn inner(&self) -> &FlatBackend {
&self.inner
}
}
impl crate::core::Backend for YamlBackend {
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 YamlBackend {
fn reload_from_str(&self, locale: &str, content: &str) -> Result<(), I18nError> {
self.add_locale_from_str(locale, content)
}
fn file_extension(&self) -> &'static str {
"yaml"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Backend;
#[test]
fn test_flatten_simple() {
let yaml_str = r#"
app.title: My App
app.desc: A cool app
"#;
let backend = YamlBackend::new();
backend.add_locale_from_str("en", yaml_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 yaml_str = r#"
errors:
not_found: Not found
unauthorized: Unauthorized
auth:
expired: Session expired
"#;
let backend = YamlBackend::new();
backend.add_locale_from_str("en", yaml_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 yaml_str = r#"
items:
zero: No items
one: One item
other: "%{count} items"
"#;
let backend = YamlBackend::new();
backend.add_locale_from_str("en", yaml_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_yaml_backend_implements_reloadable() {
let backend = YamlBackend::new();
assert_eq!(backend.file_extension(), "yaml");
backend.reload_from_str("en", "hello: Hi").unwrap();
assert_eq!(backend.get("en", "hello").unwrap(), "Hi");
}
#[test]
fn test_reload_replaces_locale() {
let backend = YamlBackend::new();
backend
.add_locale_from_str("en", "greeting: Hello")
.unwrap();
assert_eq!(backend.get("en", "greeting").unwrap(), "Hello");
backend.reload_from_str("en", "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 = YamlBackend::from_embedded(&[
("en", "hello: Hello\nbye: Goodbye"),
("zh-CN", "hello: 你好\nbye: 再见"),
])
.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_yaml_returns_error() {
let backend = YamlBackend::new();
let result = backend.add_locale_from_str("en", "not: valid: yaml: {{{");
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.yaml"),
"welcome: Welcome!\ngreeting: Hello, %{name}!",
)
.unwrap();
std::fs::write(
dir.path().join("zh-CN.yaml"),
"welcome: 欢迎使用!\ngreeting: 你好,%{name}!",
)
.unwrap();
let backend = YamlBackend::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);
}
}