Skip to main content

qa_spec/
i18n.rs

1use std::collections::BTreeMap;
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7/// i18n text descriptor used by form/question display fields.
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
9pub struct I18nText {
10    pub key: String,
11    #[serde(default, skip_serializing_if = "Option::is_none")]
12    pub args: Option<BTreeMap<String, Value>>,
13}
14
15/// Pre-resolved i18n map injected by adapters/callers.
16pub type ResolvedI18nMap = BTreeMap<String, String>;
17
18pub fn resolve_i18n_text(
19    fallback: &str,
20    text: Option<&I18nText>,
21    resolved: Option<&ResolvedI18nMap>,
22) -> String {
23    resolve_i18n_text_with_locale(fallback, text, resolved, None, None)
24}
25
26pub fn resolve_i18n_text_with_locale(
27    fallback: &str,
28    text: Option<&I18nText>,
29    resolved: Option<&ResolvedI18nMap>,
30    requested_locale: Option<&str>,
31    default_locale: Option<&str>,
32) -> String {
33    let Some(text) = text else {
34        return fallback.to_string();
35    };
36    let Some(resolved) = resolved else {
37        return fallback.to_string();
38    };
39    let Some(base) = resolve_by_locale(resolved, &text.key, requested_locale, default_locale)
40    else {
41        return fallback.to_string();
42    };
43
44    interpolate_args(base, text.args.as_ref())
45}
46
47fn resolve_by_locale<'a>(
48    resolved: &'a ResolvedI18nMap,
49    key: &str,
50    requested_locale: Option<&str>,
51    default_locale: Option<&str>,
52) -> Option<&'a str> {
53    for locale in [requested_locale, default_locale].iter().flatten() {
54        if let Some(value) = resolved.get(&format!("{}:{}", locale, key)) {
55            return Some(value);
56        }
57        if let Some(value) = resolved.get(&format!("{}/{}", locale, key)) {
58            return Some(value);
59        }
60    }
61    resolved.get(key).map(String::as_str)
62}
63
64fn interpolate_args(template: &str, args: Option<&BTreeMap<String, Value>>) -> String {
65    let Some(args) = args else {
66        return template.to_string();
67    };
68    let mut output = template.to_string();
69    for (name, value) in args {
70        let token = format!("{{{}}}", name);
71        let value_text = match value {
72            Value::String(v) => v.clone(),
73            _ => value.to_string(),
74        };
75        output = output.replace(&token, &value_text);
76    }
77    output
78}