fix(i18n): refine Persian RTL layout with atomic LTR spans, isolated hotkeys, and right-aligned descriptions

This commit is contained in:
Kirill Minovsky 2026-09-05 16:02:49 +03:00
parent aff5744d4f
commit 2aaaf74002
2 changed files with 55 additions and 15 deletions

View file

@ -767,7 +767,7 @@ pub fn get_messages(lang: Language) -> Messages {
menu_concurrency: "تعداد رشته‌ها:",
menu_hw_row: "حرکت",
menu_hw_change: "تغییر تنظیم",
menu_hw_tests: "شروع/انتخاب",
menu_hw_tests: "انتخاب/شروع",
menu_hw_start: "تغییر گزینه",
menu_hw_quit: "خروج",
menu_line_prompt: "انتخاب را وارد کنید [123]: ",
@ -776,7 +776,7 @@ pub fn get_messages(lang: Language) -> Messages {
menu_test_netinfo: "تشخیص و اصلاح مشکلات اتصال",
menu_test_dns: "تست سرورهای DNS",
menu_test_domains: "تست سرورهای ابری",
menu_test_tcp: "CDN (KB) و تخمین محدودیت (حداکثر حجم: 16)",
menu_test_tcp: "تست CDN [16 KB] و تخمین محدودیت",
menu_test_sni: "اسکن عمق بسته با واقعی",
menu_test_telegram: "تست تکه‌تکه کردن بسته",
menu_test_legend: "جمع‌بندی و پیشنهاد تنظیمات",
@ -1794,6 +1794,8 @@ mod tests {
let title = format_bidi("زبان و تنظیمات اسکن", Language::Fa);
assert_eq!(title, "ﻦﮑﺳﺍ ﺕﺎﻤﯿﻈﻨﺗ ﻭ ﻥﺎﺑﺯ");
}
let cdn_row = format_bidi("تست CDN [16 KB] و تخمین محدودیت", Language::Fa);
assert!(cdn_row.contains("CDN [16 KB]"), "CDN [16 KB] token is preserved as atomic LTR span");
}
}

View file

@ -374,20 +374,18 @@ fn draw_menu(
lines.push(format!(" {}", pad_left_width(&lang_opts, 61)));
// IP version row
let ip_cursor = if cursor == 1 { "►" } else { " " };
let ip_opts = if v6_supported {
if ip_version == "ipv4" {
"IPv4 \x1b[1;32m●\x1b[0m IPv6 \x1b[2m○\x1b[0m".to_string()
"IPv6 \x1b[2m○\x1b[0m IPv4 \x1b[1;32m●\x1b[0m".to_string()
} else {
"IPv4 \x1b[2m○\x1b[0m IPv6 \x1b[1;32m●\x1b[0m".to_string()
"IPv6 \x1b[1;32m●\x1b[0m IPv4 \x1b[2m○\x1b[0m".to_string()
}
} else {
let unavail = match current_lang {
Language::Fa => format_bidi("[در آینده]", current_lang),
_ => format_bidi(msg.unavailable, current_lang),
};
format!("{} IPv4 \x1b[1;32m●\x1b[0m IPv6 \x1b[2m○\x1b[0m", unavail)
let unavail_str = format!("({})", msg.unavailable);
let unavail = format_bidi(&unavail_str, current_lang);
format!("{} IPv6 \x1b[2m○\x1b[0m IPv4 \x1b[1;32m●\x1b[0m", unavail)
};
let ip_cursor = if cursor == 1 { "►" } else { " " };
let ip_lbl = format_bidi(msg.menu_ip_version, current_lang);
lines.push(format!(" {} {}", ip_cursor, pad_left_width(&ip_lbl, 60)));
lines.push(format!(" {}", pad_left_width(&ip_opts, 61)));
@ -473,7 +471,13 @@ fn draw_menu(
"\x1b[2m[ ]\x1b[0m"
};
let row_cursor = if cursor == i + offset { "►" } else { " " };
lines.push(format!(" {} {} {}. {}", row_cursor, check_box, digit, format_bidi(label, current_lang)));
if current_lang.is_rtl() {
let desc = format_bidi(label, current_lang);
let prefix = format!(" {} {} {}. ", row_cursor, check_box, digit);
lines.push(format!("{}{}", prefix, pad_left_width(&desc, 50)));
} else {
lines.push(format!(" {} {} {}. {}", row_cursor, check_box, digit, format_bidi(label, current_lang)));
}
}
// Glyph-safe content first: widths are measured after replacement.
let lines: Vec<String> = lines.into_iter().map(|l| asc(&l)).collect();
@ -505,8 +509,28 @@ fn draw_menu(
let _ = out.write_all(clean_output(&bot_border).as_bytes());
let hotkey_row = if plain_mode() {
if current_lang.is_rtl() {
format!(
" [↑↓/WS] {} | [←→/AD] {} | [0-6] {} | [Enter] {} | [Q] {}\r\n",
format_bidi(msg.menu_hw_row, current_lang),
format_bidi(msg.menu_hw_change, current_lang),
format_bidi(msg.menu_hw_tests, current_lang),
format_bidi(msg.menu_hw_start, current_lang),
format_bidi(msg.menu_hw_quit, current_lang)
)
} else {
format!(
" [↑↓/WS] {} | [←→/AD] {} | [0-6] {} | [Enter] {} | [Q] {}\r\n",
format_bidi(msg.menu_hw_row, current_lang),
format_bidi(msg.menu_hw_change, current_lang),
format_bidi(msg.menu_hw_tests, current_lang),
format_bidi(msg.menu_hw_start, current_lang),
format_bidi(msg.menu_hw_quit, current_lang)
)
}
} else if current_lang.is_rtl() {
format!(
" [↑↓/WS] {} | [←→/AD] {} | [0-6] {} | [Enter] {} | [Q] {}\r\n",
" \x1b[1;46;37m ↑↓/WS \x1b[0m {} │ \x1b[1;46;37m ←→/AD \x1b[0m {} │ \x1b[1;46;37m 0-6 \x1b[0m {} │ \x1b[1;42;37m Enter \x1b[0m {} │ \x1b[1;41;37m Q \x1b[0m {}\r\n",
format_bidi(msg.menu_hw_row, current_lang),
format_bidi(msg.menu_hw_change, current_lang),
format_bidi(msg.menu_hw_tests, current_lang),
@ -648,7 +672,13 @@ mod tests {
}
// Test options
for (digit, label) in test_options {
let test_line = format!(" [ ] {}. {}", digit, format_bidi(label, lang));
let test_line = if lang.is_rtl() {
let desc = format_bidi(label, lang);
let prefix = format!(" ► [ ] {}. ", digit);
format!("{}{}", prefix, pad_left_width(&desc, 50))
} else {
format!(" [ ] {}. {}", digit, format_bidi(label, lang))
};
let w = strip_ansi_len(&test_line);
assert!(w + 3 <= BOX_WIDTH, "test row {} for {:?} overflows box: w={}", digit, lang, w);
}
@ -692,8 +722,16 @@ mod tests {
let banner = render_banner(&msg, RegionProfile::Ir, "✓ وضعیت: متصل به اینترنت");
println!("{}", banner);
for (digit, label) in get_test_options(&msg) {
let desc = format_bidi(label, Language::Fa);
let prefix = format!(" [ ] {}. ", digit);
println!("│ {}{} │", prefix, pad_left_width(&desc, 50));
}
let bot_border = format!("╰{}╯", "─".repeat(BOX_WIDTH - 2));
println!("{}", bot_border);
let hotkey_row = format!(
" [↑↓/WS] {} │ [←→/AD] {} │ [0-6] {} │ [Enter] {} │ [Q] {}",
" [↑↓/WS] {} │ [←→/AD] {} │ [0-6] {} │ [Enter] {} │ [Q] {}",
format_bidi(msg.menu_hw_row, Language::Fa),
format_bidi(msg.menu_hw_change, Language::Fa),
format_bidi(msg.menu_hw_tests, Language::Fa),