Skip to main content

ling/
diag.rs

1// src/diag.rs — colored, Rust-style error reporting with localized labels.
2//
3// Output language defaults to English and can be overridden with the `LING_LANG`
4// environment variable (en | th | ko | ja | zh). Colors auto-disable when stderr
5// is not a terminal or when `NO_COLOR` is set; `CLICOLOR_FORCE` forces them on.
6//
7// Default palette: navy blue, teal, rose red, grey, vine green.
8
9use std::io::IsTerminal;
10
11// ─── Palette (truecolor RGB) ───────────────────────────────────────────────────
12
13pub const NAVY: (u8, u8, u8) = (59, 110, 165); // #3B6EA5 — notes / secondary
14pub const TEAL: (u8, u8, u8) = (42, 157, 143); // #2A9D8F — frames / structure
15pub const ROSE: (u8, u8, u8) = (232, 74, 111); // #E84A6F — errors
16pub const GREY: (u8, u8, u8) = (141, 153, 174); // #8D99AE — locations / dim
17pub const VINE: (u8, u8, u8) = (127, 176, 105); // #7FB069 — hints / success
18
19// ─── Output language ────────────────────────────────────────────────────────────
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub enum OutputLang {
23    English,
24    Thai,
25    Korean,
26    Japanese,
27    Chinese,
28}
29
30impl OutputLang {
31    /// Resolve from the `LING_LANG` env var; defaults to English.
32    pub fn from_env() -> Self {
33        match std::env::var("LING_LANG")
34            .unwrap_or_default()
35            .trim()
36            .to_lowercase()
37            .as_str()
38        {
39            "th" | "thai" | "ภาษาไทย" => OutputLang::Thai,
40            "ko" | "korean" | "한국어" => OutputLang::Korean,
41            "ja" | "japanese" | "日本語" => OutputLang::Japanese,
42            "zh" | "chinese" | "中文" => OutputLang::Chinese,
43            _ => OutputLang::English,
44        }
45    }
46}
47
48/// Localized label lookup. `key` is one of the keys handled below.
49fn t(lang: OutputLang, key: &str) -> &'static str {
50    use OutputLang::*;
51    match (key, lang) {
52        ("error", English) => "error",
53        ("error", Thai) => "ข้อผิดพลาด",
54        ("error", Korean) => "오류",
55        ("error", Japanese) => "エラー",
56        ("error", Chinese) => "错误",
57
58        ("parse", English) => "parse",
59        ("parse", Thai) => "แยกวิเคราะห์",
60        ("parse", Korean) => "구문",
61        ("parse", Japanese) => "構文",
62        ("parse", Chinese) => "解析",
63
64        ("runtime", English) => "runtime",
65        ("runtime", Thai) => "ขณะทำงาน",
66        ("runtime", Korean) => "런타임",
67        ("runtime", Japanese) => "実行時",
68        ("runtime", Chinese) => "运行时",
69
70        ("traceback", English) => "traceback (deepest call last)",
71        ("traceback", Thai) => "การย้อนรอย (เรียกล่าสุดอยู่ท้าย)",
72        ("traceback", Korean) => "역추적 (최근 호출이 마지막)",
73        ("traceback", Japanese) => "トレースバック (最新の呼び出しが最後)",
74        ("traceback", Chinese) => "回溯(最近的调用在最后)",
75
76        ("in", English) => "in",
77        ("in", Thai) => "ใน",
78        ("in", Korean) => "위치",
79        ("in", Japanese) => "内",
80        ("in", Chinese) => "于",
81
82        ("hint", English) => "hint",
83        ("hint", Thai) => "คำแนะนำ",
84        ("hint", Korean) => "힌트",
85        ("hint", Japanese) => "ヒント",
86        ("hint", Chinese) => "提示",
87
88        // Fallback to English for any unmapped key.
89        (_, _) => "error",
90    }
91}
92
93// ─── Color gating ────────────────────────────────────────────────────────────────
94
95fn colors_enabled() -> bool {
96    if std::env::var_os("NO_COLOR").is_some() {
97        return false;
98    }
99    if std::env::var_os("CLICOLOR_FORCE").is_some() {
100        enable_windows_vt();
101        return true;
102    }
103    let on = std::io::stderr().is_terminal();
104    if on {
105        enable_windows_vt();
106    }
107    on
108}
109
110/// On Windows, enable ANSI escape (virtual terminal) processing on the legacy
111/// console. No-op on terminals that already support it (Windows Terminal, VSCode).
112#[cfg(windows)]
113fn enable_windows_vt() {
114    use std::sync::Once;
115    static ONCE: Once = Once::new();
116    ONCE.call_once(|| unsafe {
117        extern "system" {
118            fn GetStdHandle(n: u32) -> *mut core::ffi::c_void;
119            fn GetConsoleMode(h: *mut core::ffi::c_void, m: *mut u32) -> i32;
120            fn SetConsoleMode(h: *mut core::ffi::c_void, m: u32) -> i32;
121        }
122        const STD_ERROR_HANDLE: u32 = -12i32 as u32;
123        const ENABLE_VIRTUAL_TERMINAL_PROCESSING: u32 = 0x0004;
124        let h = GetStdHandle(STD_ERROR_HANDLE);
125        let mut mode = 0u32;
126        if GetConsoleMode(h, &mut mode) != 0 {
127            let _ = SetConsoleMode(h, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
128        }
129    });
130}
131
132#[cfg(not(windows))]
133fn enable_windows_vt() {}
134
135// ─── Painter ─────────────────────────────────────────────────────────────────────
136
137struct Paint {
138    on: bool,
139}
140
141impl Paint {
142    fn new() -> Self {
143        Paint { on: colors_enabled() }
144    }
145
146    fn color(&self, rgb: (u8, u8, u8), bold: bool, s: &str) -> String {
147        if !self.on {
148            return s.to_string();
149        }
150        let (r, g, bl) = rgb;
151        let bold_seq = if bold { "\x1b[1m" } else { "" };
152        format!("\x1b[38;2;{};{};{}m{}{}\x1b[0m", r, g, bl, bold_seq, s)
153    }
154}
155
156// ─── Rendering ─────────────────────────────────────────────────────────────────
157
158fn header(p: &Paint, lang: OutputLang, kind_key: &str, message: &str) -> String {
159    let error_word = t(lang, "error");
160    let kind = t(lang, kind_key);
161    format!(
162        "{}{}{} {}",
163        p.color(ROSE, true, error_word),
164        p.color(GREY, false, &format!("[{kind}]")),
165        p.color(ROSE, true, ":"),
166        p.color((230, 230, 235), true, message),
167    )
168}
169
170fn location(p: &Paint, file: Option<&str>) -> String {
171    match file {
172        Some(f) => format!("\n {}", p.color(GREY, false, &format!("--> {f}"))),
173        None => String::new(),
174    }
175}
176
177/// Render a runtime error with an optional call-stack traceback.
178pub fn render_runtime(
179    message: &str,
180    _source: &str,
181    file: Option<&str>,
182    trace: &[String],
183    lang: OutputLang,
184) -> String {
185    let p = Paint::new();
186    let localized = localize_message(message, lang);
187    let mut out = header(&p, lang, "runtime", &localized);
188    out.push_str(&location(&p, file));
189
190    if !trace.is_empty() {
191        out.push('\n');
192        out.push_str(&format!("  {}", p.color(TEAL, true, t(lang, "traceback"))));
193        out.push(':');
194        for (i, frame) in trace.iter().enumerate() {
195            out.push('\n');
196            out.push_str(&format!(
197                "    {} {}",
198                p.color(GREY, false, &format!("{i}:")),
199                p.color(TEAL, false, frame),
200            ));
201        }
202    }
203
204    if let Some(hint) = hint_for(message, lang) {
205        out.push('\n');
206        out.push_str(&format!(
207            "  {}{} {}",
208            p.color(VINE, true, t(lang, "hint")),
209            p.color(VINE, true, ":"),
210            p.color(GREY, false, &hint),
211        ));
212    }
213    out
214}
215
216/// Render a parse error.
217pub fn render_parse(message: &str, _source: &str, file: Option<&str>, lang: OutputLang) -> String {
218    let p = Paint::new();
219    let mut out = header(&p, lang, "parse", message);
220    out.push_str(&location(&p, file));
221    out
222}
223
224/// Localized, best-effort hint for common runtime errors.
225fn hint_for(message: &str, lang: OutputLang) -> Option<String> {
226    use OutputLang::*;
227    if message.contains("unknown function") || message.contains("undefined") {
228        Some(
229            match lang {
230                English => "check the spelling, or `use` the module that defines it",
231                Thai => "ตรวจการสะกด หรือ `use` โมดูลที่กำหนดมัน",
232                Korean => "철자를 확인하거나, 정의한 모듈을 `use` 하세요",
233                Japanese => "綴りを確認するか、定義しているモジュールを `use` してください",
234                Chinese => "检查拼写,或 `use` 定义它的模块",
235            }
236            .to_string(),
237        )
238    } else if message.contains("no entry point") {
239        Some(
240            match lang {
241                English => "add `bind start = do { ... }`",
242                Thai => "เพิ่ม `bind start = do { ... }`",
243                Korean => "`bind start = do { ... }` 를 추가하세요",
244                Japanese => "`bind start = do { ... }` を追加してください",
245                Chinese => "添加 `bind start = do { ... }`",
246            }
247            .to_string(),
248        )
249    } else {
250        None
251    }
252}
253
254/// Translate the *body* of the most common runtime errors, preserving any
255/// dynamic suffix (e.g. the quoted identifier). English passes through. This
256/// lets diagnostics read fully in the chosen language without touching the
257/// thousands of `format!`-built messages in the runtime.
258fn localize_message(msg: &str, lang: OutputLang) -> String {
259    use OutputLang::*;
260    if lang == English {
261        return msg.to_string();
262    }
263
264    // (english_prefix, [th, ko, ja, zh])
265    let prefixes: &[(&str, [&str; 4])] = &[
266        (
267            "unknown function ",
268            [
269                "ฟังก์ชันที่ไม่รู้จัก ",
270                "알 수 없는 함수 ",
271                "不明な関数 ",
272                "未知函数 ",
273            ],
274        ),
275        (
276            "undefined: ",
277            ["ไม่ได้กำหนด: ", "정의되지 않음: ", "未定義: ", "未定义: "],
278        ),
279        (
280            "cannot call ",
281            [
282                "เรียกใช้ไม่ได้ ",
283                "호출할 수 없음 ",
284                "呼び出せません ",
285                "无法调用 ",
286            ],
287        ),
288        (
289            "division by zero",
290            ["หารด้วยศูนย์", "0으로 나눔", "ゼロ除算", "除以零"],
291        ),
292        (
293            "index out of",
294            [
295                "ดัชนีเกินขอบเขต",
296                "인덱스 범위 초과",
297                "範囲外インデックス",
298                "索引越界",
299            ],
300        ),
301    ];
302    let idx = match lang {
303        Thai => 0,
304        Korean => 1,
305        Japanese => 2,
306        Chinese => 3,
307        English => return msg.to_string(),
308    };
309    for (en, tr) in prefixes {
310        if let Some(rest) = msg.strip_prefix(en) {
311            return format!("{}{}", tr[idx], rest);
312        }
313    }
314    // Whole-message special cases.
315    if msg.starts_with("no entry point") {
316        return match lang {
317            Thai => "ไม่มีจุดเริ่มต้น — ต้องมี `bind เริ่ม = ทำ {...}`",
318            Korean => "진입점 없음 — `bind 시작 = do {...}` 가 필요합니다",
319            Japanese => "エントリポイントがありません — `bind 始め = do {...}` が必要です",
320            Chinese => "没有入口点 — 需要 `bind 始 = do {...}`",
321            English => msg,
322        }
323        .to_string();
324    }
325    msg.to_string()
326}