ling-lang 2030.1.26

Ling - The Omniglot Systems Language
Documentation
// src/diag.rs — colored, Rust-style error reporting with localized labels.
//
// Output language defaults to English and can be overridden with the `LING_LANG`
// environment variable (en | th | ko | ja | zh). Colors auto-disable when stderr
// is not a terminal or when `NO_COLOR` is set; `CLICOLOR_FORCE` forces them on.
//
// Default palette: navy blue, teal, rose red, grey, vine green.

use std::io::IsTerminal;

// ─── Palette (truecolor RGB) ───────────────────────────────────────────────────

pub const NAVY:  (u8, u8, u8) = (59, 110, 165);  // #3B6EA5 — notes / secondary
pub const TEAL:  (u8, u8, u8) = (42, 157, 143);  // #2A9D8F — frames / structure
pub const ROSE:  (u8, u8, u8) = (232, 74, 111);  // #E84A6F — errors
pub const GREY:  (u8, u8, u8) = (141, 153, 174); // #8D99AE — locations / dim
pub const VINE:  (u8, u8, u8) = (127, 176, 105); // #7FB069 — hints / success

// ─── Output language ────────────────────────────────────────────────────────────

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OutputLang {
    English,
    Thai,
    Korean,
    Japanese,
    Chinese,
}

impl OutputLang {
    /// Resolve from the `LING_LANG` env var; defaults to English.
    pub fn from_env() -> Self {
        match std::env::var("LING_LANG").unwrap_or_default().trim().to_lowercase().as_str() {
            "th" | "thai" | "ภาษาไทย"      => OutputLang::Thai,
            "ko" | "korean" | "한국어"      => OutputLang::Korean,
            "ja" | "japanese" | "日本語"    => OutputLang::Japanese,
            "zh" | "chinese" | "中文"       => OutputLang::Chinese,
            _                               => OutputLang::English,
        }
    }
}

/// Localized label lookup. `key` is one of the keys handled below.
fn t(lang: OutputLang, key: &str) -> &'static str {
    use OutputLang::*;
    match (key, lang) {
        ("error", English) => "error",
        ("error", Thai)    => "ข้อผิดพลาด",
        ("error", Korean)  => "오류",
        ("error", Japanese) => "エラー",
        ("error", Chinese) => "错误",

        ("parse", English) => "parse",
        ("parse", Thai)    => "แยกวิเคราะห์",
        ("parse", Korean)  => "구문",
        ("parse", Japanese) => "構文",
        ("parse", Chinese) => "解析",

        ("runtime", English) => "runtime",
        ("runtime", Thai)    => "ขณะทำงาน",
        ("runtime", Korean)  => "런타임",
        ("runtime", Japanese) => "実行時",
        ("runtime", Chinese) => "运行时",

        ("traceback", English) => "traceback (deepest call last)",
        ("traceback", Thai)    => "การย้อนรอย (เรียกล่าสุดอยู่ท้าย)",
        ("traceback", Korean)  => "역추적 (최근 호출이 마지막)",
        ("traceback", Japanese) => "トレースバック (最新の呼び出しが最後)",
        ("traceback", Chinese) => "回溯(最近的调用在最后)",

        ("in", English) => "in",
        ("in", Thai)    => "ใน",
        ("in", Korean)  => "위치",
        ("in", Japanese) => "",
        ("in", Chinese) => "",

        ("hint", English) => "hint",
        ("hint", Thai)    => "คำแนะนำ",
        ("hint", Korean)  => "힌트",
        ("hint", Japanese) => "ヒント",
        ("hint", Chinese) => "提示",

        // Fallback to English for any unmapped key.
        (_, _) => "error",
    }
}

// ─── Color gating ────────────────────────────────────────────────────────────────

fn colors_enabled() -> bool {
    if std::env::var_os("NO_COLOR").is_some() {
        return false;
    }
    if std::env::var_os("CLICOLOR_FORCE").is_some() {
        enable_windows_vt();
        return true;
    }
    let on = std::io::stderr().is_terminal();
    if on {
        enable_windows_vt();
    }
    on
}

/// On Windows, enable ANSI escape (virtual terminal) processing on the legacy
/// console. No-op on terminals that already support it (Windows Terminal, VSCode).
#[cfg(windows)]
fn enable_windows_vt() {
    use std::sync::Once;
    static ONCE: Once = Once::new();
    ONCE.call_once(|| unsafe {
        extern "system" {
            fn GetStdHandle(n: u32) -> *mut core::ffi::c_void;
            fn GetConsoleMode(h: *mut core::ffi::c_void, m: *mut u32) -> i32;
            fn SetConsoleMode(h: *mut core::ffi::c_void, m: u32) -> i32;
        }
        const STD_ERROR_HANDLE: u32 = -12i32 as u32;
        const ENABLE_VIRTUAL_TERMINAL_PROCESSING: u32 = 0x0004;
        let h = GetStdHandle(STD_ERROR_HANDLE);
        let mut mode = 0u32;
        if GetConsoleMode(h, &mut mode) != 0 {
            let _ = SetConsoleMode(h, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
        }
    });
}

#[cfg(not(windows))]
fn enable_windows_vt() {}

// ─── Painter ─────────────────────────────────────────────────────────────────────

struct Paint {
    on: bool,
}

impl Paint {
    fn new() -> Self {
        Paint { on: colors_enabled() }
    }

    fn color(&self, rgb: (u8, u8, u8), bold: bool, s: &str) -> String {
        if !self.on {
            return s.to_string();
        }
        let (r, g, bl) = rgb;
        let bold_seq = if bold { "\x1b[1m" } else { "" };
        format!("\x1b[38;2;{};{};{}m{}{}\x1b[0m", r, g, bl, bold_seq, s)
    }
}

// ─── Rendering ─────────────────────────────────────────────────────────────────

fn header(p: &Paint, lang: OutputLang, kind_key: &str, message: &str) -> String {
    let error_word = t(lang, "error");
    let kind = t(lang, kind_key);
    format!(
        "{}{}{} {}",
        p.color(ROSE, true, error_word),
        p.color(GREY, false, &format!("[{kind}]")),
        p.color(ROSE, true, ":"),
        p.color((230, 230, 235), true, message),
    )
}

fn location(p: &Paint, file: Option<&str>) -> String {
    match file {
        Some(f) => format!("\n {}", p.color(GREY, false, &format!("--> {f}"))),
        None => String::new(),
    }
}

/// Render a runtime error with an optional call-stack traceback.
pub fn render_runtime(
    message: &str,
    _source: &str,
    file: Option<&str>,
    trace: &[String],
    lang: OutputLang,
) -> String {
    let p = Paint::new();
    let localized = localize_message(message, lang);
    let mut out = header(&p, lang, "runtime", &localized);
    out.push_str(&location(&p, file));

    if !trace.is_empty() {
        out.push('\n');
        out.push_str(&format!("  {}", p.color(TEAL, true, t(lang, "traceback"))));
        out.push(':');
        for (i, frame) in trace.iter().enumerate() {
            out.push('\n');
            out.push_str(&format!(
                "    {} {}",
                p.color(GREY, false, &format!("{i}:")),
                p.color(TEAL, false, frame),
            ));
        }
    }

    if let Some(hint) = hint_for(message, lang) {
        out.push('\n');
        out.push_str(&format!(
            "  {}{} {}",
            p.color(VINE, true, t(lang, "hint")),
            p.color(VINE, true, ":"),
            p.color(GREY, false, &hint),
        ));
    }
    out
}

/// Render a parse error.
pub fn render_parse(message: &str, _source: &str, file: Option<&str>, lang: OutputLang) -> String {
    let p = Paint::new();
    let mut out = header(&p, lang, "parse", message);
    out.push_str(&location(&p, file));
    out
}

/// Localized, best-effort hint for common runtime errors.
fn hint_for(message: &str, lang: OutputLang) -> Option<String> {
    use OutputLang::*;
    if message.contains("unknown function") || message.contains("undefined") {
        Some(match lang {
            English  => "check the spelling, or `use` the module that defines it",
            Thai     => "ตรวจการสะกด หรือ `use` โมดูลที่กำหนดมัน",
            Korean   => "철자를 확인하거나, 정의한 모듈을 `use` 하세요",
            Japanese => "綴りを確認するか、定義しているモジュールを `use` してください",
            Chinese  => "检查拼写,或 `use` 定义它的模块",
        }.to_string())
    } else if message.contains("no entry point") {
        Some(match lang {
            English  => "add `bind start = do { ... }`",
            Thai     => "เพิ่ม `bind start = do { ... }`",
            Korean   => "`bind start = do { ... }` 를 추가하세요",
            Japanese => "`bind start = do { ... }` を追加してください",
            Chinese  => "添加 `bind start = do { ... }`",
        }.to_string())
    } else {
        None
    }
}

/// Translate the *body* of the most common runtime errors, preserving any
/// dynamic suffix (e.g. the quoted identifier). English passes through. This
/// lets diagnostics read fully in the chosen language without touching the
/// thousands of `format!`-built messages in the runtime.
fn localize_message(msg: &str, lang: OutputLang) -> String {
    use OutputLang::*;
    if lang == English { return msg.to_string(); }

    // (english_prefix, [th, ko, ja, zh])
    let prefixes: &[(&str, [&str; 4])] = &[
        ("unknown function ", ["ฟังก์ชันที่ไม่รู้จัก ", "알 수 없는 함수 ", "不明な関数 ", "未知函数 "]),
        ("undefined: ",       ["ไม่ได้กำหนด: ",         "정의되지 않음: ",    "未定義: ",       "未定义: "]),
        ("cannot call ",      ["เรียกใช้ไม่ได้ ",       "호출할 수 없음 ",     "呼び出せません ", "无法调用 "]),
        ("division by zero",  ["หารด้วยศูนย์",          "0으로 나눔",         "ゼロ除算",        "除以零"]),
        ("index out of",      ["ดัชนีเกินขอบเขต",       "인덱스 범위 초과",    "範囲外インデックス", "索引越界"]),
    ];
    let idx = match lang { Thai => 0, Korean => 1, Japanese => 2, Chinese => 3, English => return msg.to_string() };
    for (en, tr) in prefixes {
        if let Some(rest) = msg.strip_prefix(en) {
            return format!("{}{}", tr[idx], rest);
        }
    }
    // Whole-message special cases.
    if msg.starts_with("no entry point") {
        return match lang {
            Thai     => "ไม่มีจุดเริ่มต้น — ต้องมี `bind เริ่ม = ทำ {...}`",
            Korean   => "진입점 없음 — `bind 시작 = do {...}` 가 필요합니다",
            Japanese => "エントリポイントがありません — `bind 始め = do {...}` が必要です",
            Chinese  => "没有入口点 — 需要 `bind 始 = do {...}`",
            English  => msg,
        }.to_string();
    }
    msg.to_string()
}