commit fa39edacd059ec42ddc3be91ecd28cc61a8b3c37
parent e860e5033560e1dbf03430a7b31d4d7fcdc6c796
Author: triesap <tyson@radroots.org>
Date: Mon, 2 Feb 2026 14:16:13 +0000
app: add i18n scaffolding
- add i18n context and translate macro
- wire i18n context into app shell
- add mf2-i18n dependencies for app
- cover i18n fallback with tests
Diffstat:
5 files changed, 77 insertions(+), 0 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
@@ -111,6 +111,8 @@ radroots-sql-core = { path = "refs/crates/sql-core" }
radroots-tangle-db = { path = "refs/crates/tangle-db" }
radroots-tangle-db-schema = { path = "refs/crates/tangle-db-schema" }
radroots-tangle-events = { path = "refs/crates/tangle-events" }
+mf2-i18n-core = { path = "refs/mf2-i18n/crates/mf2-i18n-core" }
+mf2-i18n-embedded = { path = "refs/mf2-i18n/crates/mf2-i18n-embedded" }
[profile.release]
codegen-units = 1
diff --git a/app/Cargo.toml b/app/Cargo.toml
@@ -20,6 +20,9 @@ web-sys.workspace = true
gloo-timers = { workspace = true, features = ["futures"] }
radroots-app-core = { path = "../crates/core" }
radroots-app-ui-components = { path = "../crates/ui-components" }
+radroots-app-lib = { path = "../crates/app-lib" }
+mf2-i18n-core = { path = "../refs/mf2-i18n/crates/mf2-i18n-core" }
+mf2-i18n-embedded = { path = "../refs/mf2-i18n/crates/mf2-i18n-embedded" }
radroots-log = { path = "../refs/crates/log", default-features = false }
radroots-nostr = { workspace = true }
tracing-wasm = "0.2"
diff --git a/app/src/app.rs b/app/src/app.rs
@@ -33,6 +33,7 @@ use crate::{
app_init_total_add,
app_init_total_unknown,
app_context,
+ app_i18n_init,
app_log_buffer_flush_deferred,
app_log_debug_emit,
app_log_error_emit,
@@ -1435,6 +1436,7 @@ fn AppShell() -> impl IntoView {
provide_context(init_error);
provide_context(init_state);
provide_context(setup_required);
+ provide_context(app_i18n_init());
Effect::new(move || {
let navigate = navigate.clone();
spawn_local(async move {
diff --git a/app/src/i18n.rs b/app/src/i18n.rs
@@ -0,0 +1,68 @@
+#![forbid(unsafe_code)]
+
+use leptos::prelude::{use_context, LocalStorage, RwSignal, StoredValue, WithUntracked, WithValue};
+
+use mf2_i18n_core::Args;
+use mf2_i18n_embedded::EmbeddedRuntime;
+use radroots_app_lib::get_locale;
+
+#[derive(Clone, Copy)]
+pub struct RadrootsAppI18nContext {
+ pub locale: RwSignal<String, LocalStorage>,
+ pub runtime: StoredValue<Option<EmbeddedRuntime>>,
+}
+
+pub fn app_i18n_init() -> RadrootsAppI18nContext {
+ let locale = get_locale(&["en"]);
+ let locale = RwSignal::new_local(locale);
+ let runtime = StoredValue::new(None::<EmbeddedRuntime>);
+ RadrootsAppI18nContext { locale, runtime }
+}
+
+pub fn app_i18n() -> Option<RadrootsAppI18nContext> {
+ use_context::<RadrootsAppI18nContext>()
+}
+
+pub fn translate(key: &str) -> String {
+ let Some(ctx) = app_i18n() else {
+ return key.to_string();
+ };
+ let locale = ctx.locale.with_untracked(|value| value.clone());
+ ctx.runtime.with_value(|runtime: &Option<EmbeddedRuntime>| {
+ if let Some(runtime) = runtime.as_ref() {
+ let args = Args::new();
+ runtime
+ .format(&locale, key, &args)
+ .unwrap_or_else(|_| key.to_string())
+ } else {
+ key.to_string()
+ }
+ })
+}
+
+#[macro_export]
+macro_rules! t {
+ ($key:literal) => {
+ $crate::i18n::translate($key)
+ };
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{app_i18n, app_i18n_init, translate};
+ use leptos::prelude::{provide_context, Owner};
+
+ #[test]
+ fn translate_falls_back_without_context() {
+ assert_eq!(translate("hello"), "hello");
+ }
+
+ #[test]
+ fn translate_reads_context() {
+ let owner = Owner::new();
+ owner.set();
+ provide_context(app_i18n_init());
+ assert!(app_i18n().is_some());
+ assert_eq!(translate("hello"), "hello");
+ }
+}
diff --git a/app/src/lib.rs b/app/src/lib.rs
@@ -7,6 +7,7 @@ mod config;
mod data;
mod health;
mod init;
+mod i18n;
mod keystore;
mod logging;
mod logs;
@@ -198,3 +199,4 @@ pub use init::{
RadrootsAppInitState,
APP_INIT_STORAGE_KEY,
};
+pub use i18n::{app_i18n, app_i18n_init, RadrootsAppI18nContext};