add account localization and switchable rooms

Centralize control and OBS copy in a shared TOML catalog, persist the selected locale per account, and broadcast language changes to component streams. Allow account owners to atomically switch their Bilibili room and restart the shared listener without changing component URLs.
This commit is contained in:
2026-07-18 23:28:05 -07:00
parent 53ae90ec13
commit f79852d8e6
35 changed files with 1946 additions and 472 deletions
+65
View File
@@ -0,0 +1,65 @@
//! Shared language-resource validation.
//!
//! Rust embeds the same TOML file that Vite compiles into the browser bundle.
//! Backend persistence accepts only locale codes declared by that resource, so
//! the database and deployed frontend cannot drift into unsupported values.
use std::{collections::HashMap, sync::OnceLock};
use serde::Deserialize;
const RESOURCE: &str = include_str!("../../../resources/i18n.toml");
#[derive(Deserialize)]
struct Catalog {
default_language: String,
locales: HashMap<String, Locale>,
}
#[derive(Deserialize)]
struct Locale {
#[allow(dead_code)]
name: String,
messages: HashMap<String, String>,
}
fn catalog() -> &'static Catalog {
static CATALOG: OnceLock<Catalog> = OnceLock::new();
CATALOG.get_or_init(|| {
let catalog: Catalog = toml::from_str(RESOURCE).expect("resources/i18n.toml must be valid");
let default = catalog
.locales
.get(&catalog.default_language)
.expect("default language must be declared");
for (code, locale) in &catalog.locales {
for key in default.messages.keys() {
assert!(
locale.messages.contains_key(key),
"locale {code} is missing translation key {key}"
);
}
}
catalog
})
}
pub fn default_language() -> &'static str {
&catalog().default_language
}
pub fn is_supported(language: &str) -> bool {
catalog().locales.contains_key(language)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shared_catalog_is_complete_and_declares_supported_languages() {
assert_eq!(default_language(), "zh-CN");
assert!(is_supported("zh-CN"));
assert!(is_supported("en-US"));
assert!(!is_supported("unknown"));
}
}