use std::collections::HashMap;
use anycms_i18n::Backend;
#[cfg(any(feature = "postgres", feature = "mysql", feature = "sqlite"))]
use anycms_i18n::I18nError;
use dashmap::DashMap;
pub struct SqlxBackend {
cache: DashMap<String, HashMap<String, String>>,
}
impl SqlxBackend {
pub fn new() -> Self {
Self {
cache: DashMap::new(),
}
}
pub fn from_translations(
translations: impl IntoIterator<Item = (String, String, String)>,
) -> Self {
let cache = DashMap::new();
for (locale, key, value) in translations {
cache
.entry(locale)
.or_insert_with(HashMap::new)
.insert(key, value);
}
Self { cache }
}
pub fn reload_from_translations(
&self,
translations: impl IntoIterator<Item = (String, String, String)>,
) {
self.cache.clear();
for (locale, key, value) in translations {
self.cache.entry(locale).or_default().insert(key, value);
}
}
#[cfg(feature = "postgres")]
pub async fn from_postgres(pool: &sqlx::PgPool) -> Result<Self, I18nError> {
let rows: Vec<(String, String, String)> =
sqlx::query_as("SELECT locale, key, value FROM i18n_translations")
.fetch_all(pool)
.await
.map_err(|e| I18nError::DatabaseError(e.to_string()))?;
Ok(Self::from_translations(rows))
}
#[cfg(feature = "postgres")]
pub async fn reload_postgres(&self, pool: &sqlx::PgPool) -> Result<(), I18nError> {
let rows: Vec<(String, String, String)> =
sqlx::query_as("SELECT locale, key, value FROM i18n_translations")
.fetch_all(pool)
.await
.map_err(|e| I18nError::DatabaseError(e.to_string()))?;
self.reload_from_translations(rows);
Ok(())
}
#[cfg(feature = "mysql")]
pub async fn from_mysql(pool: &sqlx::MySqlPool) -> Result<Self, I18nError> {
let rows: Vec<(String, String, String)> =
sqlx::query_as("SELECT locale, key, value FROM i18n_translations")
.fetch_all(pool)
.await
.map_err(|e| I18nError::DatabaseError(e.to_string()))?;
Ok(Self::from_translations(rows))
}
#[cfg(feature = "mysql")]
pub async fn reload_mysql(&self, pool: &sqlx::MySqlPool) -> Result<(), I18nError> {
let rows: Vec<(String, String, String)> =
sqlx::query_as("SELECT locale, key, value FROM i18n_translations")
.fetch_all(pool)
.await
.map_err(|e| I18nError::DatabaseError(e.to_string()))?;
self.reload_from_translations(rows);
Ok(())
}
#[cfg(feature = "sqlite")]
pub async fn from_sqlite(pool: &sqlx::SqlitePool) -> Result<Self, I18nError> {
let rows: Vec<(String, String, String)> =
sqlx::query_as("SELECT locale, key, value FROM i18n_translations")
.fetch_all(pool)
.await
.map_err(|e| I18nError::DatabaseError(e.to_string()))?;
Ok(Self::from_translations(rows))
}
#[cfg(feature = "sqlite")]
pub async fn reload_sqlite(&self, pool: &sqlx::SqlitePool) -> Result<(), I18nError> {
let rows: Vec<(String, String, String)> =
sqlx::query_as("SELECT locale, key, value FROM i18n_translations")
.fetch_all(pool)
.await
.map_err(|e| I18nError::DatabaseError(e.to_string()))?;
self.reload_from_translations(rows);
Ok(())
}
}
impl Default for SqlxBackend {
fn default() -> Self {
Self::new()
}
}
impl Backend for SqlxBackend {
fn get(&self, locale: &str, key: &str) -> Option<String> {
self.cache.get(locale).and_then(|map| map.get(key).cloned())
}
fn available_locales(&self) -> Vec<String> {
self.cache.iter().map(|r| r.key().clone()).collect()
}
fn has_locale(&self, locale: &str) -> bool {
self.cache.contains_key(locale)
}
fn dump(&self, locale: &str) -> HashMap<String, String> {
self.cache
.get(locale)
.map(|m| m.clone())
.unwrap_or_default()
}
}
pub struct SqlxBackendBuilder {
table: String,
locale_col: String,
key_col: String,
value_col: String,
}
impl SqlxBackendBuilder {
pub fn new() -> Self {
Self {
table: "i18n_translations".into(),
locale_col: "locale".into(),
key_col: "key".into(),
value_col: "value".into(),
}
}
pub fn table(mut self, name: impl Into<String>) -> Self {
self.table = name.into();
self
}
pub fn locale_col(mut self, name: impl Into<String>) -> Self {
self.locale_col = name.into();
self
}
pub fn key_col(mut self, name: impl Into<String>) -> Self {
self.key_col = name.into();
self
}
pub fn value_col(mut self, name: impl Into<String>) -> Self {
self.value_col = name.into();
self
}
#[allow(dead_code)] fn query(&self) -> String {
format!(
"SELECT {} AS locale, {} AS key, {} AS value FROM {}",
self.locale_col, self.key_col, self.value_col, self.table,
)
}
#[cfg(feature = "postgres")]
pub async fn build_postgres(&self, pool: &sqlx::PgPool) -> Result<SqlxBackend, I18nError> {
let sql = self.query();
let rows: Vec<(String, String, String)> = sqlx::query_as(sql.as_str())
.fetch_all(pool)
.await
.map_err(|e| I18nError::DatabaseError(e.to_string()))?;
Ok(SqlxBackend::from_translations(rows))
}
#[cfg(feature = "mysql")]
pub async fn build_mysql(&self, pool: &sqlx::MySqlPool) -> Result<SqlxBackend, I18nError> {
let sql = self.query();
let rows: Vec<(String, String, String)> = sqlx::query_as(sql.as_str())
.fetch_all(pool)
.await
.map_err(|e| I18nError::DatabaseError(e.to_string()))?;
Ok(SqlxBackend::from_translations(rows))
}
#[cfg(feature = "sqlite")]
pub async fn build_sqlite(&self, pool: &sqlx::SqlitePool) -> Result<SqlxBackend, I18nError> {
let sql = self.query();
let rows: Vec<(String, String, String)> = sqlx::query_as(sql.as_str())
.fetch_all(pool)
.await
.map_err(|e| I18nError::DatabaseError(e.to_string()))?;
Ok(SqlxBackend::from_translations(rows))
}
}
impl Default for SqlxBackendBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_backend() {
let backend = SqlxBackend::new();
assert!(!backend.has_locale("en"));
assert!(backend.available_locales().is_empty());
assert_eq!(backend.get("en", "hello"), None);
}
#[test]
fn from_translations() {
let backend = SqlxBackend::from_translations(vec![
("en".into(), "hello".into(), "Hello".into()),
("en".into(), "world".into(), "World".into()),
("zh-CN".into(), "hello".into(), "你好".into()),
]);
assert!(backend.has_locale("en"));
assert!(backend.has_locale("zh-CN"));
assert!(!backend.has_locale("ja"));
assert_eq!(backend.get("en", "hello"), Some("Hello".into()));
assert_eq!(backend.get("en", "world"), Some("World".into()));
assert_eq!(backend.get("zh-CN", "hello"), Some("你好".into()));
assert_eq!(backend.get("en", "missing"), None);
let mut locales = backend.available_locales();
locales.sort();
assert_eq!(locales, vec!["en", "zh-CN"]);
}
#[test]
fn reload_clears_old_data() {
let backend =
SqlxBackend::from_translations(vec![("en".into(), "hello".into(), "Hello".into())]);
assert_eq!(backend.get("en", "hello"), Some("Hello".into()));
backend.reload_from_translations(vec![("de".into(), "hello".into(), "Hallo".into())]);
assert_eq!(backend.get("en", "hello"), None);
assert_eq!(backend.get("de", "hello"), Some("Hallo".into()));
}
#[test]
fn builder_default_query() {
let builder = SqlxBackendBuilder::new();
assert_eq!(
builder.query(),
"SELECT locale AS locale, key AS key, value AS value FROM i18n_translations"
);
}
#[test]
fn builder_custom_query() {
let builder = SqlxBackendBuilder::new()
.table("my_table")
.locale_col("lang")
.key_col("msg_key")
.value_col("msg_val");
assert_eq!(
builder.query(),
"SELECT lang AS locale, msg_key AS key, msg_val AS value FROM my_table"
);
}
#[test]
fn dump_returns_all_keys_for_locale() {
let backend = SqlxBackend::from_translations(vec![
("en".into(), "hello".into(), "Hello".into()),
("en".into(), "world".into(), "World".into()),
("zh-CN".into(), "hello".into(), "你好".into()),
]);
let en = backend.dump("en");
assert_eq!(en.len(), 2);
assert_eq!(en.get("hello").unwrap(), "Hello");
assert_eq!(en.get("world").unwrap(), "World");
let zh = backend.dump("zh-CN");
assert_eq!(zh.len(), 1);
assert_eq!(zh.get("hello").unwrap(), "你好");
assert!(backend.dump("ja").is_empty());
}
}