sova-i18n 0.1.2

Two-level i18n store and locale resolution for Sova
Documentation
//! JSON endpoints under `/_i18n`.

use crate::ext::I18nState;
use crate::store::{select_tree, ROOT_SCOPE};
use sova_core::{Request, Response};
use serde_json::json;
use std::hash::{Hash, Hasher};

pub async fn locales_json(req: Request) -> Response {
    let Some(state) = req.try_state::<I18nState>() else {
        return Response::text("i18n not installed").status(500);
    };
    let store = state.store.load();
    let body = json!({
        "locales": store.locales,
        "version": store.version.as_ref(),
        "fallback": state.fallback.as_ref(),
    });
    with_cache_headers(Response::json(&body), &req, None, store.version.as_ref())
}

pub async fn all_json(req: Request) -> Response {
    let Some(state) = req.try_state::<I18nState>() else {
        return Response::text("i18n not installed").status(500);
    };
    let store = state.store.load();
    if !req
        .try_state::<crate::AllJsonEnabled>()
        .map(|f| f.0)
        .unwrap_or(false)
    {
        return Response::text("Not Found").status(404);
    }
    let mut map = serde_json::Map::new();
    for ((locale, scope), s) in &store.scopes {
        let key = if scope.as_ref() == ROOT_SCOPE {
            locale.to_string()
        } else {
            format!("{locale}/{scope}")
        };
        map.insert(
            key,
            serde_json::from_slice(&s.payload).unwrap_or(serde_json::Value::Null),
        );
    }
    with_cache_headers(
        Response::json(&serde_json::Value::Object(map)),
        &req,
        None,
        store.version.as_ref(),
    )
}

pub async fn missing_json(req: Request) -> Response {
    let Some(state) = req.try_state::<I18nState>() else {
        return Response::text("i18n not installed").status(500);
    };
    Response::json(&state.missing_snapshot())
}

pub async fn locale_or_scope_json(req: Request) -> Response {
    let Some(state) = req.try_state::<I18nState>() else {
        return Response::text("i18n not installed").status(500);
    };
    let store = state.store.load();

    let locale = req
        .param("locale")
        .unwrap_or("")
        .trim_end_matches(".json")
        .to_string();
    let scope = req
        .param("scope")
        .map(|s| s.trim_end_matches(".json").to_string())
        .unwrap_or_else(|| ROOT_SCOPE.to_string());

    if locale.is_empty() || locale == "_missing" {
        return Response::text("Not Found").status(404);
    }

    let Some(scope_data) = store.get(&locale, &scope) else {
        return Response::text("Not Found").status(404);
    };

    let prefix = req.query.get("prefix").map(|s| s.as_str());
    let keys = req.query.get("keys").map(|s| {
        s.split(',')
            .map(|k| k.trim())
            .filter(|k| !k.is_empty())
            .collect::<Vec<_>>()
    });

    let (body, etag) = if prefix.is_some() || keys.is_some() {
        let keys_ref = keys.as_deref();
        let tree = select_tree(&scope_data.tree, prefix, keys_ref);
        let bytes = serde_json::to_vec(&tree).unwrap_or_default();
        let etag = {
            let mut h = std::collections::hash_map::DefaultHasher::new();
            bytes.hash(&mut h);
            format!("\"{:016x}\"", h.finish())
        };
        (bytes, etag)
    } else {
        (
            scope_data.payload.to_vec(),
            scope_data.etag.to_string(),
        )
    };

    if let Some(inm) = req.header("if-none-match") {
        if inm.split(',').any(|t| t.trim() == etag) {
            return Response::empty()
                .status(304)
                .header("etag", &etag)
                .header("content-language", &locale);
        }
    }

    let res = Response::bytes(body, "application/json")
        .header("etag", &etag)
        .header("content-language", &locale);
    with_cache_headers(res, &req, Some(&etag), store.version.as_ref())
}

fn with_cache_headers(
    mut res: Response,
    req: &Request,
    etag: Option<&str>,
    version: &str,
) -> Response {
    if let Some(etag) = etag {
        res = res.header("etag", etag);
    }
    let immutable = req
        .query
        .get("v")
        .map(|v| v.as_str() == version)
        .unwrap_or(false);
    if immutable {
        res = res.header(
            "cache-control",
            "public, max-age=31536000, immutable",
        );
    } else {
        res = res.header("cache-control", "no-cache");
    }
    res
}