amql-cli 0.0.0-alpha.0

AQL command-line interface and REPL
//! Compile-time message catalog for user-facing strings.
//!
//! The catalog is bundled at build time from `locales/{AQL_LOCALE}.toml`
//! (default: `en`). Use `msg!` to retrieve and interpolate messages.
//!
//! Interpolation: `{key}` placeholders replaced with named values.
//!
//! Example:
//! ```ignore
//! msg!(CATALOG, "config.no_root")
//! msg!(CATALOG, "fs.read_failed", "file" => path, "error" => e)
//! ```

use rustc_hash::FxHashMap;

/// The compiled-in message catalog. Parsed once at startup.
pub static CATALOG: std::sync::LazyLock<Catalog> = std::sync::LazyLock::new(Catalog::load);

const RAW: &str = include_str!(concat!(env!("OUT_DIR"), "/messages.toml"));

/// A flat key → template map loaded from the locale TOML.
pub struct Catalog {
    messages: FxHashMap<String, String>,
}

impl Catalog {
    fn load() -> Self {
        let table: toml::Table = RAW.parse().expect("bundled messages.toml is valid TOML");
        let mut messages = FxHashMap::default();
        flatten_table(&table, String::new(), &mut messages);
        Self { messages }
    }

    /// Retrieve a message by dot-separated key, with `{placeholder}` interpolation.
    ///
    /// Returns the key itself if not found (never panics at runtime).
    pub fn get(&self, key: &str, vars: &[(&str, &str)]) -> String {
        let template = match self.messages.get(key) {
            Some(t) => t.as_str(),
            None => return key.to_string(),
        };
        let mut out = template.to_string();
        for (k, v) in vars {
            out = out.replace(&format!("{{{k}}}"), v);
        }
        out
    }
}

/// Flatten a nested TOML table into dot-separated keys.
fn flatten_table(table: &toml::Table, prefix: String, out: &mut FxHashMap<String, String>) {
    for (k, v) in table {
        let key = if prefix.is_empty() {
            k.clone()
        } else {
            format!("{prefix}.{k}")
        };
        match v {
            toml::Value::String(s) => {
                out.insert(key, s.clone());
            }
            toml::Value::Table(t) => {
                flatten_table(t, key, out);
            }
            _ => {}
        }
    }
}

/// Look up a message from the global catalog with optional interpolation.
///
/// ```ignore
/// msg!("config.no_root")
/// msg!("fs.read_failed", "file" => path, "error" => e)
/// ```
#[macro_export]
macro_rules! msg {
    ($key:expr) => {
        $crate::messages::CATALOG.get($key, &[])
    };
    ($key:expr, $($name:expr => $val:expr),+) => {
        $crate::messages::CATALOG.get($key, &[$( ($name, &$val.to_string()) ),+])
    };
}