From 763c7d9f871a61c1b3e8dd32142a58f5f69fe98d Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Fri, 16 Jan 2026 11:49:06 -0700 Subject: [PATCH] wip: localization --- core/Cargo.lock | 195 ++++++++++++++++++++++++++++++-- core/Cargo.toml | 3 +- core/src/db/model/public.rs | 6 +- core/src/error.rs | 2 + core/src/keyboard.conf.template | 8 ++ core/src/lib.rs | 2 + core/src/system.rs | 101 ++++++++++++++++- core/src/util/io.rs | 17 +++ 8 files changed, 319 insertions(+), 15 deletions(-) create mode 100644 core/src/keyboard.conf.template diff --git a/core/Cargo.lock b/core/Cargo.lock index 4dcc5641e..de3ff8c11 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -239,6 +239,15 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arc-swap" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d03449bb8ca2cc2ef70869af31463d1ae5ccc8fa3e334b307203fbf815207e" +dependencies = [ + "rustversion", +] + [[package]] name = "archery" version = "1.2.2" @@ -848,6 +857,12 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" +[[package]] +name = "base62" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1adf9755786e27479693dedd3271691a92b5e242ab139cacb9fb8e7fb5381111" + [[package]] name = "base64" version = "0.13.1" @@ -1109,7 +1124,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", - "regex-automata", + "regex-automata 0.4.13", "serde", ] @@ -3094,12 +3109,42 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "glob-match" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9985c9503b412198aa4197559e9a318524ebc4519c229bfa05a535828c950b9d" +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata 0.4.13", + "regex-syntax 0.8.8", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags 1.3.2", + "ignore", + "walkdir", +] + [[package]] name = "gloo-timers" version = "0.3.0" @@ -3740,6 +3785,22 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "ignore" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata 0.4.13", + "same-file", + "walkdir", + "winapi-util", +] + [[package]] name = "image" version = "0.25.9" @@ -4254,7 +4315,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" dependencies = [ - "regex-automata", + "regex-automata 0.4.13", ] [[package]] @@ -4479,11 +4540,11 @@ dependencies = [ [[package]] name = "matchers" -version = "0.2.0" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" dependencies = [ - "regex-automata", + "regex-automata 0.1.10", ] [[package]] @@ -4784,6 +4845,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "normpath" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf23ab2b905654b4cb177e30b629937b3868311d4e1cba859f899c041046e69b" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "notify" version = "8.2.0" @@ -4818,11 +4888,12 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.50.3" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" dependencies = [ - "windows-sys 0.61.2", + "overload", + "winapi", ] [[package]] @@ -5199,6 +5270,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "owo-colors" version = "4.2.3" @@ -6364,10 +6441,19 @@ checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", - "regex-automata", + "regex-automata 0.4.13", "regex-syntax 0.8.8", ] +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + [[package]] name = "regex-automata" version = "0.4.13" @@ -6585,6 +6671,60 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rust-i18n" +version = "3.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda2551fdfaf6cc5ee283adc15e157047b92ae6535cf80f6d4962d05717dc332" +dependencies = [ + "globwalk", + "once_cell", + "regex", + "rust-i18n-macro", + "rust-i18n-support", + "smallvec", +] + +[[package]] +name = "rust-i18n-macro" +version = "3.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22baf7d7f56656d23ebe24f6bb57a5d40d2bce2a5f1c503e692b5b2fa450f965" +dependencies = [ + "glob", + "once_cell", + "proc-macro2", + "quote", + "rust-i18n-support", + "serde", + "serde_json", + "serde_yaml", + "syn 2.0.114", +] + +[[package]] +name = "rust-i18n-support" +version = "3.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940ed4f52bba4c0152056d771e563b7133ad9607d4384af016a134b58d758f19" +dependencies = [ + "arc-swap", + "base62", + "globwalk", + "itertools 0.11.0", + "lazy_static", + "normpath", + "once_cell", + "proc-macro2", + "regex", + "serde", + "serde_json", + "serde_yaml", + "siphasher", + "toml 0.8.23", + "triomphe", +] + [[package]] name = "rustc-demangle" version = "0.1.26" @@ -7106,6 +7246,19 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.13.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "serde_yml" version = "0.0.12" @@ -7763,6 +7916,7 @@ dependencies = [ "rpassword", "rpc-toolkit", "rust-argon2", + "rust-i18n", "safelog", "semver", "serde", @@ -9616,14 +9770,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex-automata", + "regex", "sharded-slab", "smallvec", "thread_local", @@ -9653,6 +9807,17 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "triomphe" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39" +dependencies = [ + "arc-swap", + "serde", + "stable_deref_trait", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -9854,6 +10019,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.9.0" diff --git a/core/Cargo.toml b/core/Cargo.toml index ddd3c4463..ba1fbaaf3 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -213,6 +213,7 @@ reqwest = { version = "0.12.25", features = [ reqwest_cookie_store = "0.9.0" rpassword = "7.2.0" rust-argon2 = "3.0.0" +rust-i18n = "3.1.5" rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git" } safelog = { version = "0.4.8", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true } semver = { version = "1.0.20", features = ["serde"] } @@ -263,7 +264,7 @@ tower-service = "0.3.3" tracing = "0.1.39" tracing-error = "0.2.0" tracing-journald = "0.3.0" -tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } +tracing-subscriber = { version = "=0.3.19", features = ["env-filter"] } ts-rs = "9.0.1" typed-builder = "0.23.2" url = { version = "2.4.1", features = ["serde"] } diff --git a/core/src/db/model/public.rs b/core/src/db/model/public.rs index b24b2a210..b3cfc4401 100644 --- a/core/src/db/model/public.rs +++ b/core/src/db/model/public.rs @@ -25,7 +25,7 @@ use crate::net::utils::ipv6_is_local; use crate::net::vhost::AlpnInfo; use crate::prelude::*; use crate::progress::FullProgress; -use crate::system::SmtpValue; +use crate::system::{KeyboardOptions, SmtpValue}; use crate::util::cpupower::Governor; use crate::util::lshw::LshwDevice; use crate::util::serde::MaybeUtf8String; @@ -139,6 +139,8 @@ impl Public { ram: 0, devices: Vec::new(), kiosk, + language: None, + keyboard: None, }, package_data: AllPackageData::default(), ui: serde_json::from_str(*DB_UI_SEED_CELL.get().unwrap_or(&"null")) @@ -195,6 +197,8 @@ pub struct ServerInfo { pub ram: u64, pub devices: Vec, pub kiosk: Option, + pub language: Option, + pub keyboard: Option, } #[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] diff --git a/core/src/error.rs b/core/src/error.rs index f8554e378..3e6a1ee86 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -97,6 +97,7 @@ pub enum ErrorKind { InstallFailed = 76, UpdateFailed = 77, Smtp = 78, + SetSysInfo = 79, } impl ErrorKind { pub fn as_str(&self) -> &'static str { @@ -180,6 +181,7 @@ impl ErrorKind { InstallFailed => "Install Failed", UpdateFailed => "Update Failed", Smtp => "SMTP Error", + SetSysInfo => "Error Setting System Info", } } } diff --git a/core/src/keyboard.conf.template b/core/src/keyboard.conf.template new file mode 100644 index 000000000..178cb4cec --- /dev/null +++ b/core/src/keyboard.conf.template @@ -0,0 +1,8 @@ +Section "InputClass" + Identifier "system-keyboard" + MatchIsKeyboard "on" + Option "XkbModel" "{model}" + Option "XkbLayout" "{layout}" + Option "XkbVariant" "{variant}" + Option "XkbOptions" "{options}" +EndSection diff --git a/core/src/lib.rs b/core/src/lib.rs index 4be4448dd..4e9f186a0 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,5 +1,7 @@ use const_format::formatcp; +rust_i18n::i18n!("locales", fallback = ["en_US"]); + pub const DATA_DIR: &str = "/media/startos/data"; pub const MAIN_DATA: &str = formatcp!("{DATA_DIR}/main"); pub const PACKAGE_DATA: &str = formatcp!("{DATA_DIR}/package-data"); diff --git a/core/src/system.rs b/core/src/system.rs index f12f6195f..333d7fa99 100644 --- a/core/src/system.rs +++ b/core/src/system.rs @@ -23,7 +23,7 @@ use crate::rpc_continuations::{Guid, RpcContinuation, RpcContinuations}; use crate::shutdown::Shutdown; use crate::util::Invoke; use crate::util::cpupower::{Governor, get_available_governors, set_governor}; -use crate::util::io::open_file; +use crate::util::io::{copy_file, open_file, write_file_atomic}; use crate::util::serde::{HandlerExtSerde, WithIoFormat, display_serializable}; use crate::util::sync::Watch; use crate::{MAIN_DATA, PACKAGE_DATA}; @@ -1053,6 +1053,105 @@ pub async fn test_smtp( Ok(()) } +#[derive(Debug, Clone, Deserialize, Serialize, TS, Parser)] +#[serde(rename_all = "camelCase")] +pub struct KeyboardOptions { + pub layout: InternedString, + pub model: Option, + pub variant: Option, + #[arg(short, long = "option")] + #[serde(default)] + pub options: Vec, +} +impl KeyboardOptions { + /// NOTE: will error if kiosk inactive + pub async fn apply_to_session(&self) -> Result<(), Error> { + let mut cmd = Command::new("setxkbmap"); + cmd.env("DISPLAY", ":0") + .env("XAUTHORITY", "/home/kiosk/.Xauthority"); + cmd.arg("-layout").arg(&*self.layout); + if let Some(variant) = self.variant.as_deref() { + cmd.arg("-variant").arg(variant); + } + for option in &self.options { + cmd.arg("-option").arg(&**option); + } + cmd.invoke(ErrorKind::SetSysInfo).await?; + Ok(()) + } + + pub async fn save(&self) -> Result<(), Error> { + // TODO: set console keyboard + write_file_atomic( + "/media/startos/config/overlay/etc/X11/xorg.conf.d/00-keyboard.conf", + format!( + include_str!("./keyboard.conf.template"), + model = self.model.as_deref().unwrap_or_default(), + layout = &*self.layout, + variant = self.variant.as_deref().unwrap_or_default(), + options = self.options.join(","), + ), + ) + .await + } +} + +pub async fn set_keyboard(ctx: RpcContext, options: KeyboardOptions) -> Result<(), Error> { + options.apply_to_session().await.log_err(); + options.save().await?; + ctx.db + .mutate(|db| { + db.as_public_mut() + .as_server_info_mut() + .as_keyboard_mut() + .ser(&Some(options)) + }) + .await + .result?; + Ok(()) +} + +#[derive(Debug, Clone, Deserialize, Serialize, TS, Parser)] +#[serde(rename_all = "camelCase")] +pub struct SetLanguageParams { + pub language: InternedString, +} + +pub async fn set_language( + ctx: RpcContext, + SetLanguageParams { language }: SetLanguageParams, +) -> Result<(), Error> { + write_file_atomic( + "/etc/locale.gen", + format!("{language}.UTF-8 UTF-8\n").as_bytes(), + ) + .await?; + Command::new("locale-gen") + .invoke(ErrorKind::SetSysInfo) + .await?; + copy_file( + "/usr/lib/locale/locale-archive", + "/media/startos/config/overlay/usr/lib/locale/locale-archive", + ) + .await?; + write_file_atomic( + "/media/startos/config/overlay/etc/default/locale", + format!("{language}.UTF-8\n").as_bytes(), + ) + .await?; + ctx.db + .mutate(|db| { + db.as_public_mut() + .as_server_info_mut() + .as_language_mut() + .ser(&Some(language.clone())) + }) + .await + .result?; + rust_i18n::set_locale(&*language); + Ok(()) +} + #[tokio::test] #[ignore] pub async fn test_get_temp() { diff --git a/core/src/util/io.rs b/core/src/util/io.rs index fbcd9f840..99940c373 100644 --- a/core/src/util/io.rs +++ b/core/src/util/io.rs @@ -775,6 +775,23 @@ pub fn dir_copy<'a, P0: AsRef + 'a + Send + Sync, P1: AsRef + 'a + S .boxed() } +pub async fn copy_file(src: impl AsRef, dst: impl AsRef) -> Result<(), Error> { + let src = src.as_ref(); + tokio::fs::metadata(src) + .await + .with_ctx(|_| (ErrorKind::Filesystem, src.display()))?; + let dst = dst.as_ref(); + if let Some(parent) = dst.parent() { + tokio::fs::create_dir_all(parent) + .await + .with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("mkdir -p {parent:?}")))?; + } + tokio::fs::copy(src, dst) + .await + .with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("cp {src:?} -> {dst:?}")))?; + Ok(()) +} + #[pin_project::pin_project] pub struct TimeoutStream { timeout: Duration,