AkurAI Build
Menu

Public repository / 1f1e9ee3a896111b3fb2dd86a7295dfd55c1d56a

AkurAI-Notes / crates/notes-api/src/handlers/ssr.rs

Latest change 1f1e9ee3a896111b3fb2dd86a7295dfd55c1d56a - SSR template engine + i18n (EN/IS via agy) + nav fix + theme wiring by Claude Code

use crate::router_setup::NotesState;
use akurai_http::{Request, Response};
use akurai_json::Value;
use akurai_template::Engine;
use std::fs;
use std::sync::{Arc, Mutex};

pub type RouteHandler = Box<dyn Fn(&Request) -> Response + Send + Sync>;

fn negotiate_locale(req: &Request, known: &[String]) -> String {
    if let Some(cookie) = req.header("Cookie") {
        for pair in cookie.split(';') {
            let pair = pair.trim();
            if let Some(val) = pair.strip_prefix("akurai-lang=").or_else(|| pair.strip_prefix("akurai_lang=")) {
                let lang = val.trim().trim_matches('"').to_string();
                if known.iter().any(|l| l == &lang) { return lang; }
            }
        }
    }
    if let Some(accept) = req.header("Accept-Language") {
        for part in accept.split(',') {
            let lang = part.split(';').next().unwrap_or("").trim().split('-').next().unwrap_or("").trim();
            if !lang.is_empty() && known.iter().any(|l| l == lang) { return lang.to_string(); }
        }
    }
    "en".into()
}

pub fn ssr_index_route(state: Arc<Mutex<NotesState>>) -> RouteHandler {
    Box::new(move |req: &Request| -> Response {
        let s = state.lock().unwrap();
        let frontend = &s.frontend_dir;
        let Some(parent) = frontend.parent() else {
            return Response::new(500).with_text("cannot find parent dir");
        };
        let backend = parent.join("backend");

        let mut engine = Engine::new();

        let entries = match fs::read_dir(frontend) {
            Ok(e) => e,
            Err(e) => return Response::new(500).with_text(&format!("cannot read frontend: {e}")),
        };
        for entry in entries {
            let path = match entry { Ok(e) => e.path(), Err(_) => continue };
            if path.extension().and_then(|e| e.to_str()) != Some("html") { continue; }
            let stem = match path.file_stem().and_then(|s| s.to_str()) { Some(s) => s.to_string(), None => continue };
            let source = match fs::read_to_string(&path) { Ok(s) => s, Err(_) => continue };
            if let Err(e) = engine.register(&stem, &source) {
                eprintln!("ssr: failed to register {stem}.html: {}", e.message);
            }
        }

        let mut pairs: Vec<(String, Value)> = Vec::new();
        pairs.push(("version".into(), Value::Str("v0.0.1".into())));

        let page_path = backend.join("page.json");
        if page_path.exists() {
            if let Ok(text) = fs::read_to_string(&page_path) {
                if let Ok(Value::Object(p)) = akurai_json::parse(&text) {
                    for (k, v) in p { pairs.push((k.clone(), v.clone())); }
                }
            }
        }

        let locales_dir = backend.join("locales");
        let mut known: Vec<String> = Vec::new();
        let mut catalogs: Vec<(String, Vec<(String, Value)>)> = Vec::new();

        if locales_dir.exists() {
            if let Ok(entries) = fs::read_dir(&locales_dir) {
                for entry in entries {
                    let path = match entry { Ok(e) => e.path(), Err(_) => continue };
                    if path.extension().and_then(|e| e.to_str()) != Some("json") { continue; }
                    let locale = match path.file_stem().and_then(|s| s.to_str()) { Some(s) => s.to_string(), None => continue };
                    if let Ok(text) = fs::read_to_string(&path) {
                        if let Ok(Value::Object(msgs)) = akurai_json::parse(&text) {
                            known.push(locale.clone());
                            catalogs.push((locale, msgs));
                        }
                    }
                }
            }
        }

        let locale = negotiate_locale(req, &known);
        pairs.push(("__locale".into(), Value::Str(locale.clone())));

        for (l, msgs) in &catalogs {
            if *l == locale {
                pairs.push(("__messages".into(), Value::Object(msgs.clone())));
                break;
            }
        }

        let context = Value::Object(pairs);

        match engine.render("index", &context) {
            Ok(html) => Response::ok()
                .with_header("Content-Type", "text/html; charset=utf-8")
                .with_body("text/html", html.into_bytes()),
            Err(e) => {
                let fallback = match fs::read_to_string(frontend.join("index.html")) {
                    Ok(s) => s,
                    Err(_) => format!("<h1>500</h1><p>template error: {}</p>", e.message),
                };
                Response::new(500)
                    .with_header("Content-Type", "text/html; charset=utf-8")
                    .with_body("text/html", fallback.into_bytes())
            }
        }
    })
}