wip: localization

This commit is contained in:
Aiden McClelland
2026-01-16 11:49:06 -07:00
parent ea86117e5f
commit 763c7d9f87
8 changed files with 319 additions and 15 deletions

195
core/Cargo.lock generated
View File

@@ -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"

View File

@@ -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"] }

View File

@@ -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<LshwDevice>,
pub kiosk: Option<bool>,
pub language: Option<InternedString>,
pub keyboard: Option<KeyboardOptions>,
}
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]

View File

@@ -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",
}
}
}

View File

@@ -0,0 +1,8 @@
Section "InputClass"
Identifier "system-keyboard"
MatchIsKeyboard "on"
Option "XkbModel" "{model}"
Option "XkbLayout" "{layout}"
Option "XkbVariant" "{variant}"
Option "XkbOptions" "{options}"
EndSection

View File

@@ -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");

View File

@@ -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<InternedString>,
pub variant: Option<InternedString>,
#[arg(short, long = "option")]
#[serde(default)]
pub options: Vec<InternedString>,
}
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() {

View File

@@ -775,6 +775,23 @@ pub fn dir_copy<'a, P0: AsRef<Path> + 'a + Send + Sync, P1: AsRef<Path> + 'a + S
.boxed()
}
pub async fn copy_file(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> 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<S: AsyncRead + AsyncWrite = TcpStream> {
timeout: Duration,