use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TranslationsData {
#[serde(flatten)]
fields: HashMap<String, FieldTranslations>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct FieldTranslations {
#[serde(flatten)]
translations: HashMap<String, serde_json::Value>,
}
impl FieldTranslations {
pub fn new() -> Self {
Self { translations: HashMap::new() }
}
pub fn get(&self, lang: &str) -> Option<&serde_json::Value> {
self.translations.get(lang)
}
pub fn set(&mut self, lang: &str, value: impl Into<serde_json::Value>) {
self.translations.insert(lang.to_string(), value.into());
}
pub fn remove(&mut self, lang: &str) {
self.translations.remove(lang);
}
pub fn all(&self) -> &HashMap<String, serde_json::Value> {
&self.translations
}
pub fn has(&self, lang: &str) -> bool {
self.translations.contains_key(lang)
}
pub fn languages(&self) -> Vec<&String> {
self.translations.keys().collect()
}
}
impl TranslationsData {
pub fn new() -> Self {
Self { fields: HashMap::new() }
}
pub fn from_json(value: &serde_json::Value) -> Self {
match value {
serde_json::Value::Object(map) => {
let mut fields = HashMap::new();
for (field_name, field_value) in map {
if let serde_json::Value::Object(lang_map) = field_value {
let mut translations = HashMap::new();
for (lang, trans_value) in lang_map {
translations.insert(lang.clone(), trans_value.clone());
}
fields.insert(field_name.clone(), FieldTranslations { translations });
}
}
Self { fields }
}
_ => Self::new(),
}
}
pub fn to_json(&self) -> serde_json::Value {
let mut map = serde_json::Map::new();
for (field, trans) in &self.fields {
let mut lang_map = serde_json::Map::new();
for (lang, value) in &trans.translations {
lang_map.insert(lang.clone(), value.clone());
}
map.insert(field.clone(), serde_json::Value::Object(lang_map));
}
serde_json::Value::Object(map)
}
pub fn get_field(&self, field: &str) -> Option<&FieldTranslations> {
self.fields.get(field)
}
pub fn get_field_mut(&mut self, field: &str) -> &mut FieldTranslations {
self.fields.entry(field.to_string()).or_default()
}
pub fn get(&self, field: &str, lang: &str) -> Option<&serde_json::Value> {
self.fields.get(field)?.get(lang)
}
pub fn set(&mut self, field: &str, lang: &str, value: impl Into<serde_json::Value>) {
self.get_field_mut(field).set(lang, value);
}
pub fn remove(&mut self, field: &str, lang: &str) {
if let Some(field_trans) = self.fields.get_mut(field) {
field_trans.remove(lang);
}
}
pub fn remove_field(&mut self, field: &str) {
self.fields.remove(field);
}
pub fn has_translations(&self, field: &str) -> bool {
self.fields.get(field)
.map(|f| !f.translations.is_empty())
.unwrap_or(false)
}
pub fn fields(&self) -> Vec<&String> {
self.fields.keys().collect()
}
}
pub trait HasTranslations {
fn translatable_fields() -> Vec<&'static str>;
fn allowed_languages() -> Vec<String>;
fn fallback_language() -> String;
fn get_translations_data(&self) -> Result<TranslationsData, TranslationError>;
fn set_translations_data(&mut self, data: TranslationsData) -> Result<(), TranslationError>;
fn get_default_value(&self, field: &str) -> Result<serde_json::Value, TranslationError>;
fn set_translation(&mut self, field: &str, lang: &str, value: impl Into<serde_json::Value>) -> Result<(), TranslationError> {
self.validate_field(field)?;
self.validate_language(lang)?;
let mut data = self.get_translations_data()?;
data.set(field, lang, value);
self.set_translations_data(data)
}
fn set_translations<V: Into<serde_json::Value>>(&mut self, field: &str, translations: HashMap<&str, V>) -> Result<(), TranslationError> {
self.validate_field(field)?;
let mut data = self.get_translations_data()?;
for (lang, value) in translations {
self.validate_language(lang)?;
data.set(field, lang, value);
}
self.set_translations_data(data)
}
fn sync_translations<V: Into<serde_json::Value>>(&mut self, field: &str, translations: HashMap<&str, V>) -> Result<(), TranslationError> {
self.validate_field(field)?;
let mut data = self.get_translations_data()?;
data.remove_field(field);
for (lang, value) in translations {
self.validate_language(lang)?;
data.set(field, lang, value);
}
self.set_translations_data(data)
}
fn get_translation(&self, field: &str, lang: &str) -> Result<Option<serde_json::Value>, TranslationError> {
let data = self.get_translations_data()?;
Ok(data.get(field, lang).cloned())
}
fn get_translated(&self, field: &str, lang: &str) -> Result<serde_json::Value, TranslationError> {
let data = self.get_translations_data()?;
let fallback = Self::fallback_language();
if let Some(value) = data.get(field, lang) {
return Ok(value.clone());
}
if lang != fallback {
if let Some(value) = data.get(field, &fallback) {
return Ok(value.clone());
}
}
self.get_default_value(field)
}
fn get_all_translations(&self, field: &str) -> Result<HashMap<String, serde_json::Value>, TranslationError> {
let data = self.get_translations_data()?;
Ok(data.get_field(field)
.map(|f| f.all().clone())
.unwrap_or_default())
}
fn get_translations_for_language(&self, lang: &str) -> Result<HashMap<String, serde_json::Value>, TranslationError> {
let data = self.get_translations_data()?;
let mut result = HashMap::new();
for field in Self::translatable_fields() {
if let Some(value) = data.get(field, lang) {
result.insert(field.to_string(), value.clone());
}
}
Ok(result)
}
fn remove_translation(&mut self, field: &str, lang: &str) -> Result<(), TranslationError> {
let mut data = self.get_translations_data()?;
data.remove(field, lang);
self.set_translations_data(data)
}
fn remove_field_translations(&mut self, field: &str) -> Result<(), TranslationError> {
let mut data = self.get_translations_data()?;
data.remove_field(field);
self.set_translations_data(data)
}
fn clear_translations(&mut self) -> Result<(), TranslationError> {
self.set_translations_data(TranslationsData::new())
}
fn has_translation(&self, field: &str, lang: &str) -> Result<bool, TranslationError> {
let data = self.get_translations_data()?;
Ok(data.get(field, lang).is_some())
}
fn has_any_translation(&self, field: &str) -> Result<bool, TranslationError> {
let data = self.get_translations_data()?;
Ok(data.has_translations(field))
}
fn available_languages(&self, field: &str) -> Result<Vec<String>, TranslationError> {
let data = self.get_translations_data()?;
Ok(data.get_field(field)
.map(|f| f.languages().into_iter().cloned().collect())
.unwrap_or_default())
}
fn to_translated_json(&self, options: Option<HashMap<String, String>>) -> serde_json::Value
where
Self: serde::Serialize,
{
let opts = options.unwrap_or_default();
let fallback = Self::fallback_language();
let requested_lang = opts.get("language")
.map(|s| s.as_str())
.unwrap_or(&fallback);
let mut json = match serde_json::to_value(self) {
Ok(serde_json::Value::Object(map)) => map,
_ => return serde_json::json!({}),
};
let translations = json.get("translations")
.map(TranslationsData::from_json)
.unwrap_or_default();
for field in Self::translatable_fields() {
if let Some(value) = translations.get(field, requested_lang) {
json.insert(field.to_string(), value.clone());
} else if requested_lang != fallback {
if let Some(value) = translations.get(field, &fallback) {
json.insert(field.to_string(), value.clone());
}
}
}
json.remove("translations");
serde_json::Value::Object(json)
}
fn to_json_with_all_translations(&self) -> serde_json::Value
where
Self: serde::Serialize,
{
serde_json::to_value(self).unwrap_or(serde_json::json!({}))
}
fn validate_field(&self, field: &str) -> Result<(), TranslationError> {
if !Self::translatable_fields().contains(&field) {
return Err(TranslationError::InvalidField(
format!("'{}' is not a translatable field. Available: {:?}", field, Self::translatable_fields())
));
}
Ok(())
}
fn validate_language(&self, lang: &str) -> Result<(), TranslationError> {
let allowed = Self::allowed_languages();
if !allowed.iter().any(|l| l == lang) {
return Err(TranslationError::InvalidLanguage(
format!("'{}' is not an allowed language. Allowed: {:?}", lang, allowed)
));
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct TranslationInput {
pub fields: HashMap<String, HashMap<String, serde_json::Value>>,
}
impl TranslationInput {
pub fn new() -> Self {
Self { fields: HashMap::new() }
}
pub fn from_json(value: &serde_json::Value) -> Result<Self, TranslationError> {
match value {
serde_json::Value::Object(map) => {
let mut fields = HashMap::new();
for (field, trans) in map {
if let serde_json::Value::Object(lang_map) = trans {
let mut translations = HashMap::new();
for (lang, val) in lang_map {
translations.insert(lang.clone(), val.clone());
}
fields.insert(field.clone(), translations);
}
}
Ok(Self { fields })
}
_ => Err(TranslationError::ParseError("Expected JSON object".to_string())),
}
}
pub fn add(&mut self, field: &str, lang: &str, value: impl Into<serde_json::Value>) {
self.fields
.entry(field.to_string())
.or_default()
.insert(lang.to_string(), value.into());
}
}
impl Default for TranslationInput {
fn default() -> Self {
Self::new()
}
}
pub trait ApplyTranslations: HasTranslations {
fn apply_translations(&mut self, input: TranslationInput) -> Result<(), TranslationError> {
let mut data = self.get_translations_data()?;
for (field, translations) in input.fields {
for (lang, value) in translations {
data.set(&field, &lang, value);
}
}
self.set_translations_data(data)
}
}
impl<T: HasTranslations> ApplyTranslations for T {}
#[derive(Debug, Clone)]
pub enum TranslationError {
InvalidField(String),
InvalidLanguage(String),
ParseError(String),
NotSupported,
}
impl std::fmt::Display for TranslationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TranslationError::InvalidField(msg) => write!(f, "Invalid field: {}", msg),
TranslationError::InvalidLanguage(msg) => write!(f, "Invalid language: {}", msg),
TranslationError::ParseError(msg) => write!(f, "Parse error: {}", msg),
TranslationError::NotSupported => write!(f, "Model does not support translations"),
}
}
}
impl std::error::Error for TranslationError {}
impl From<TranslationError> for crate::Error {
fn from(err: TranslationError) -> Self {
crate::Error::query(err.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_translations_data_basic() {
let mut data = TranslationsData::new();
data.set("name", "en", "Product");
data.set("name", "ar", "منتج");
assert_eq!(data.get("name", "en"), Some(&serde_json::json!("Product")));
assert_eq!(data.get("name", "ar"), Some(&serde_json::json!("منتج")));
assert_eq!(data.get("name", "fr"), None);
}
#[test]
fn test_translations_data_from_json() {
let json = serde_json::json!({
"name": {"en": "Product", "ar": "منتج"},
"description": {"en": "A great product"}
});
let data = TranslationsData::from_json(&json);
assert_eq!(data.get("name", "en"), Some(&serde_json::json!("Product")));
assert_eq!(data.get("name", "ar"), Some(&serde_json::json!("منتج")));
assert_eq!(data.get("description", "en"), Some(&serde_json::json!("A great product")));
}
#[test]
fn test_translations_data_to_json() {
let mut data = TranslationsData::new();
data.set("name", "en", "Product");
data.set("name", "ar", "منتج");
let json = data.to_json();
let expected = serde_json::json!({
"name": {"en": "Product", "ar": "منتج"}
});
assert_eq!(json, expected);
}
#[test]
fn test_field_translations() {
let mut field = FieldTranslations::new();
field.set("en", "Hello");
field.set("ar", "مرحبا");
assert!(field.has("en"));
assert!(field.has("ar"));
assert!(!field.has("fr"));
assert_eq!(field.languages().len(), 2);
field.remove("ar");
assert!(!field.has("ar"));
}
#[test]
fn test_translation_input() {
let mut input = TranslationInput::new();
input.add("name", "en", "Product");
input.add("name", "ar", "منتج");
input.add("description", "en", "Description");
assert_eq!(input.fields.len(), 2);
assert_eq!(input.fields.get("name").unwrap().len(), 2);
}
#[test]
fn test_translation_input_from_json() {
let json = serde_json::json!({
"name": {"en": "Product", "ar": "منتج"},
"description": {"en": "A product"}
});
let input = TranslationInput::from_json(&json).unwrap();
assert_eq!(input.fields.len(), 2);
assert_eq!(
input.fields.get("name").unwrap().get("en"),
Some(&serde_json::json!("Product"))
);
}
}