1use std::io::IsTerminal;
10
11pub const NAVY: (u8, u8, u8) = (59, 110, 165); pub const TEAL: (u8, u8, u8) = (42, 157, 143); pub const ROSE: (u8, u8, u8) = (232, 74, 111); pub const GREY: (u8, u8, u8) = (141, 153, 174); pub const VINE: (u8, u8, u8) = (127, 176, 105); #[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub enum OutputLang {
23 English,
24 Thai,
25 Korean,
26 Japanese,
27 Chinese,
28}
29
30impl OutputLang {
31 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
48fn 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 (_, _) => "error",
90 }
91}
92
93fn 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#[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
135struct 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
156fn 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
177pub 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
216pub 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
224fn 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
254fn localize_message(msg: &str, lang: OutputLang) -> String {
259 use OutputLang::*;
260 if lang == English {
261 return msg.to_string();
262 }
263
264 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 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}