use crate::i18n::Translator;
use crate::sql::{Auto, ExecError, Pool};
use crate::Model;
#[derive(Model, Debug, Clone, serde::Serialize)]
#[rustango(table = "rustango_translations", managed = false, scope = "registry")]
pub struct Translation {
#[rustango(primary_key)]
pub id: Auto<i64>,
#[rustango(max_length = 16)]
pub locale: String,
#[rustango(max_length = 200)]
pub key: String,
pub value: String,
#[rustango(max_length = 200, default = "")]
pub updated_by: String,
#[rustango(auto_now_add)]
pub created_at: Auto<chrono::DateTime<chrono::Utc>>,
#[rustango(auto_now)]
pub updated_at: Auto<chrono::DateTime<chrono::Utc>>,
}
const LOCALE_LEN: u32 = 16;
const KEY_LEN: u32 = 200;
pub(crate) fn storable(locale: &str, key: &str) -> bool {
let fits = |s: &str, max: u32| u32::try_from(s.chars().count()).is_ok_and(|n| n <= max);
let ok = fits(locale, LOCALE_LEN) && fits(key, KEY_LEN);
if !ok {
tracing::warn!(
target: "rustango::i18n",
locale,
key_chars = key.chars().count(),
"translation key longer than {KEY_LEN} chars (or locale over {LOCALE_LEN}) \
is not stored in the database; it keeps its file value"
);
}
ok
}
pub(crate) fn ddl(dialect: &dyn crate::sql::Dialect) -> String {
let ts = dialect.timestamp_now_column();
let (locale, key) = (
dialect.cs_text_type(LOCALE_LEN),
dialect.cs_text_type(KEY_LEN),
);
match dialect.name() {
"mysql" => format!(
r"
CREATE TABLE IF NOT EXISTS `rustango_translations` (
`id` BIGINT AUTO_INCREMENT PRIMARY KEY,
`locale` {locale} NOT NULL,
`key` {key} NOT NULL,
`value` TEXT NOT NULL,
`updated_by` VARCHAR(200) NOT NULL DEFAULT '',
`created_at` {ts},
`updated_at` {ts},
CONSTRAINT `rustango_translations_locale_key_uq` UNIQUE (`locale`, `key`)
);
"
),
"sqlite" => format!(
r#"
CREATE TABLE IF NOT EXISTS "rustango_translations" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"locale" TEXT NOT NULL,
"key" TEXT NOT NULL,
"value" TEXT NOT NULL,
"updated_by" TEXT NOT NULL DEFAULT '',
"created_at" {ts},
"updated_at" {ts},
CONSTRAINT "rustango_translations_locale_key_uq" UNIQUE ("locale", "key")
);
"#
),
_ => format!(
r#"
CREATE TABLE IF NOT EXISTS "rustango_translations" (
"id" BIGSERIAL PRIMARY KEY,
"locale" {locale} NOT NULL,
"key" {key} NOT NULL,
"value" TEXT NOT NULL,
"updated_by" VARCHAR(200) NOT NULL DEFAULT '',
"created_at" {ts},
"updated_at" {ts},
CONSTRAINT "rustango_translations_locale_key_uq" UNIQUE ("locale", "key")
);
"#
),
}
}
pub async fn ensure_table_pool(pool: &Pool) -> Result<(), sqlx::Error> {
crate::sql::run_ddl_idempotent(pool, &ddl(pool.dialect())).await?;
if refused(pool) {
return Ok(());
}
if let Err(e) = case_sensitive_keys(pool).await {
tracing::warn!(
error = %e,
"rustango_translations keeps case-insensitive keys: moving them needs ALTER"
);
refused_pools().push(crate::sql::PoolId::of(pool));
}
Ok(())
}
fn refused_pools() -> std::sync::MutexGuard<'static, Vec<crate::sql::PoolId>> {
static REFUSED: std::sync::Mutex<Vec<crate::sql::PoolId>> = std::sync::Mutex::new(Vec::new());
REFUSED
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn refused(pool: &Pool) -> bool {
let mut ids = refused_pools();
ids.retain(|id| !id.is_closed());
ids.iter().any(|id| id.is(pool))
}
async fn case_sensitive_keys(pool: &Pool) -> Result<(), ExecError> {
let d = pool.dialect();
let Some(probe) = d.ci_columns_sql("rustango_translations") else {
return Ok(());
};
let found: Vec<(String,)> = crate::sql::raw_query_pool(&probe.sql, probe.params, pool).await?;
for (column,) in found {
let len = match column.as_str() {
"locale" => LOCALE_LEN,
"key" => KEY_LEN,
_ => continue,
};
let sql = format!(
"ALTER TABLE {} MODIFY COLUMN {} {} NOT NULL",
d.quote_ident("rustango_translations"),
d.quote_ident(&column),
d.cs_text_type(len),
);
crate::sql::raw_execute_pool(pool, &sql, Vec::new()).await?;
}
Ok(())
}
pub async fn all_pool(pool: &Pool) -> Result<Vec<Translation>, ExecError> {
use crate::sql::FetcherPool as _;
Translation::objects().fetch(pool).await
}
pub async fn upsert_pool(
pool: &Pool,
locale: &str,
key: &str,
value: &str,
updated_by: &str,
) -> Result<(), ExecError> {
let row = Translation {
id: Auto::Unset,
locale: locale.to_owned(),
key: key.to_owned(),
value: value.to_owned(),
updated_by: updated_by.to_owned(),
created_at: Auto::Unset,
updated_at: Auto::Unset,
};
Translation::bulk_upsert_pool(
&[row],
&["locale", "key"],
&["value", "updated_by", "updated_at"],
pool,
)
.await
}
pub async fn seed_from_translator_pool(
pool: &Pool,
translator: &Translator,
) -> Result<usize, ExecError> {
let rows: Vec<Translation> = translator
.entries()
.into_iter()
.filter(|(locale, key, _)| storable(locale, key))
.map(|(locale, key, value)| Translation {
id: Auto::Unset,
locale,
key,
value,
updated_by: String::new(),
created_at: Auto::Unset,
updated_at: Auto::Unset,
})
.collect();
let n = rows.len();
if n > 0 {
Translation::bulk_insert_or_ignore_pool(&rows, pool).await?;
}
Ok(n)
}
pub async fn delete_key_pool(pool: &Pool, key: &str) -> Result<u64, ExecError> {
use crate::core::Model as _; use crate::core::{DeleteQuery, Filter, Op, SqlValue, WhereExpr};
crate::sql::delete_pool(
pool,
&DeleteQuery {
model: Translation::SCHEMA,
where_clause: WhereExpr::and_predicates(vec![Filter {
column: "key",
op: Op::Eq,
value: SqlValue::from(key),
}]),
},
)
.await
}
pub async fn delete_override(pool: &Pool, locale: &str, key: &str) -> Result<u64, ExecError> {
use crate::core::Model as _;
use crate::core::{DeleteQuery, Filter, Op, SqlValue, WhereExpr};
crate::sql::delete_pool(
pool,
&DeleteQuery {
model: Translation::SCHEMA,
where_clause: WhereExpr::and_predicates(vec![
Filter {
column: "locale",
op: Op::Eq,
value: SqlValue::from(locale),
},
Filter {
column: "key",
op: Op::Eq,
value: SqlValue::from(key),
},
]),
},
)
.await
}
static LIVE_TRANSLATORS: std::sync::Mutex<Vec<std::sync::Weak<Translator>>> =
std::sync::Mutex::new(Vec::new());
pub fn register_live_translator(translator: &std::sync::Arc<Translator>) {
let mut live = LIVE_TRANSLATORS
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
live.retain(|w| w.strong_count() > 0);
live.push(std::sync::Arc::downgrade(translator));
}
pub async fn refresh_live_translators(pool: &Pool) -> Result<(), ExecError> {
let live: Vec<std::sync::Arc<Translator>> = LIVE_TRANSLATORS
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.iter()
.filter_map(std::sync::Weak::upgrade)
.collect();
for t in live {
refresh_overrides_pool(&t, pool).await?;
}
Ok(())
}
pub async fn refresh_overrides_pool(
translator: &Translator,
pool: &Pool,
) -> Result<usize, ExecError> {
let rows = all_pool(pool).await?;
let n = rows.len();
translator.load_overrides(rows.into_iter().map(|t| (t.locale, t.key, t.value)));
Ok(n)
}