From b259bf39bb50d5002b32a73b16c2f314b4d1cd58 Mon Sep 17 00:00:00 2001 From: Bendik Aagaard Lynghaug Date: Tue, 1 Sep 2026 17:23:28 +0200 Subject: [PATCH] Chrome speaks the site's language: site.yaml lang + i18n table The strings the portal itself says - Asked by, Also worth asking, Sign in, the not-found page, defaults - translate via a small en/no table selected by site.yaml's lang, which also sets the html lang attribute. Content keeps speaking for itself; unknown languages fall back to English per key. Also: a scheme-gated markdown image now swallows its alt text instead of leaking it as stray text - this is the fix for the test that has been red since 0.3.19 (publish is tag-triggered and does not gate on the test workflow; caught late). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ --- src/app.rs | 48 +++++++++++++++++-------- src/content.rs | 17 ++++++--- src/i18n.rs | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 4 files changed, 145 insertions(+), 18 deletions(-) create mode 100644 src/i18n.rs diff --git a/src/app.rs b/src/app.rs index 14e6b46..1ef756a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,5 +1,5 @@ use leptos::prelude::*; -use leptos_meta::{provide_meta_context, HashedStylesheet, MetaTags, Title}; +use leptos_meta::{provide_meta_context, HashedStylesheet, Html, MetaTags, Title}; use leptos_router::{ components::{Route, Router, Routes}, hooks::{use_location, use_navigate, use_query_map}, @@ -10,6 +10,7 @@ use serde::{Deserialize, Serialize}; use crate::answers::{Answer, SelfTransitionAnswer, TransitionAnswers, TransitionItem}; use crate::auth::{current_user, User}; use crate::content::{is_qualified, render_inline_markdown, Alternative, Question, Responsible, SiteConfig, Transition}; +use crate::i18n::t; use crate::resource::{get_requirement_binding, get_requirement_options, get_resource}; /// The visible site name/wordmark - "portal" is just this codebase's @@ -68,8 +69,18 @@ pub fn App() -> impl IntoView { // context instead of fetching their own copy. let site = Resource::new(|| (), |_| get_site()); provide_context(site); + // The chrome's language, from site.yaml (default "en"); content + // speaks for itself. Components read this via context for t(). + let lang = Memo::new(move |_| { + site.get() + .and_then(|r| r.ok()) + .and_then(|s| s.lang) + .unwrap_or_else(|| "en".to_string()) + }); + provide_context(Lang(lang)); view! { + {move || { site.get() @@ -88,6 +99,15 @@ pub fn App() -> impl IntoView { } } +/// The site's chrome language as a context signal - see App. +#[derive(Clone, Copy)] +pub struct Lang(pub Memo); + +/// The current chrome language, for `t()` calls inside views. +fn lang() -> Memo { + expect_context::().0 +} + /// Every server-fn resource the pages read, created exactly once for /// the app's lifetime and handed down via context. Wrapper structs /// because a bare `Resource` context is claimed by whoever provides @@ -241,7 +261,7 @@ fn QuestionView( />
-

"This page follows from an answer you don't seem to carry yet."

+

{move || t(&lang().get(), "follows_answer")}

{label} @@ -269,7 +289,7 @@ fn QuestionView(
{if signed_in { - view! {

"This part of the site is for organizational owners — sign in with that account to take a look."

} + view! {

{move || t(&lang().get(), "owners_only")}

} .into_any() } else { view! { @@ -278,7 +298,7 @@ fn QuestionView( href=format!("/auth/login?redirect={question_id}") rel="external" > - "Sign in" + {move || t(&lang().get(), "sign_in")} } .into_any() @@ -371,7 +391,7 @@ fn QuestionNav(current_id: String, has_chain: bool) -> impl IntoView { .collect(); (!others.is_empty()).then(|| view! {