Profilo utente (birthdate/sex/notes), kid agent, i18n, shared folders, UX

This commit is contained in:
2026-07-19 08:45:17 +01:00
parent 126886e309
commit c389962e3c
25 changed files with 809 additions and 117 deletions
+86
View File
@@ -18,6 +18,42 @@ pub fn is_supported(locale: &str) -> bool {
SUPPORTED_LOCALES.contains(&locale)
}
/// The instance default locale (registry `config.ui_locale`), `"en"` when
/// unset. A read — no system bus involved — so it works from any context that
/// has a pool (sessions, shells), not just where a `GlobalConfigManager`
/// lives. An unreadable config table degrades to `"en"` rather than failing
/// the caller.
pub async fn default_locale(pool: &sqlx::SqlitePool) -> String {
crate::db::config::get(pool, DEFAULT_LOCALE_KEY)
.await
.ok()
.flatten()
.filter(|s| !s.trim().is_empty())
.unwrap_or_else(|| "en".into())
}
/// The effective locale for a user: their `users.locale` override when set,
/// the instance default otherwise (which itself falls back to `"en"`).
/// **The** resolution chain — call this instead of re-implementing it.
pub async fn resolve_locale(pool: &sqlx::SqlitePool, user_locale: Option<&str>) -> String {
match user_locale.map(str::trim).filter(|s| !s.is_empty()) {
Some(l) => l.to_string(),
None => default_locale(pool).await,
}
}
/// Human language name for prompt rendering (`"it"` → `"Italian"`). Unknown
/// codes pass through unchanged — the model copes, and this list need not
/// track every locale ever stored.
pub fn language_name(locale: &str) -> String {
match locale {
"en" => "English".into(),
"it" => "Italian".into(),
"fr" => "French".into(),
other => other.into(),
}
}
/// Writes the instance default locale straight to the registry `config` table.
/// Used by first-run provisioning shells (e.g. `skald-setup`), where no
/// `GlobalConfigManager` — hence no system bus — exists. A running server
@@ -53,3 +89,53 @@ pub fn config_set() -> ConfigSet {
],
}
}
#[cfg(test)]
mod tests {
use super::*;
fn temp_db_path(tag: &str) -> String {
let mut p = std::env::temp_dir();
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos();
p.push(format!("skald-test-i18n-{tag}-{}-{nanos}", std::process::id()));
p.push("database");
p.push("system.db");
p.to_string_lossy().into_owned()
}
fn cleanup(path: &str) {
if let Some(dir) = std::path::Path::new(path).parent().and_then(|p| p.parent()) {
let _ = std::fs::remove_dir_all(dir);
}
}
#[test]
fn language_name_maps_known_codes_and_passes_through_unknown() {
assert_eq!(language_name("en"), "English");
assert_eq!(language_name("it"), "Italian");
assert_eq!(language_name("fr"), "French");
assert_eq!(language_name("de"), "de");
}
#[tokio::test]
async fn resolve_locale_follows_user_then_instance_then_builtin() {
let path = temp_db_path("resolve");
let pool = crate::db::init_system_pool(&path).await.unwrap();
// No override, no instance default → built-in English.
assert_eq!(resolve_locale(&pool, None).await, "en");
assert_eq!(default_locale(&pool).await, "en");
// Instance default kicks in when the user has no override.
set_default_locale(&pool, "it").await.unwrap();
assert_eq!(resolve_locale(&pool, None).await, "it");
assert_eq!(resolve_locale(&pool, Some(" ")).await, "it", "blank override counts as none");
// The user override always wins.
assert_eq!(resolve_locale(&pool, Some("fr")).await, "fr");
pool.close().await;
cleanup(&path);
}
}