mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-31 22:36:51 -06:00
Add Vietnamese (vi) to the interface languages (#2541)
* Add Vietnamese (vi) to the interface languages vi.tsv covers every key of the other catalogs and the three strings none of them has yet, with one plural form. Its tone-marked letters (U+1EA0-U+1EF9) aren't in egui's bundled faces, so the Vietnamese interface takes the first system interface font that has them (Segoe UI, Arial, SF, DejaVu Sans, Noto Sans) and otherwise stays English and says why, as Korean does without Hangul. AI-assisted; native-speaker review pending. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Vietnamese: the terminology Vietnamese AutoCAD users speak Polyline, Pan, Offset, Spline and Grip stay in English as they are said in the office; Modify is Hiệu chỉnh, LINE draws an Đoạn thẳng, Explode is Phân rã, Linetype Loại nét, the plot style table Bảng nét in, Polar and Object Snap Tracking Dò theo góc / Dò theo điểm bắt, Settings Cài đặt; the snap modes Endpoint, Midpoint and Nearest are Đầu mút, Điểm giữa and Điểm gần nhất. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Vietnamese: name the language by the font loaded; Arabic's own message The picker writes Tiếng Việt only when the interface's loaded system font has its letters (another interface loads the first installed candidate, which may lack them). The font choice is one function with a unit test, gated off wasm with its callers. Choosing Arabic without an Arabic font no longer gets the Chinese message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
87f8ce22f6
commit
fa060b0998
@@ -77,3 +77,4 @@ Generated-in-code assets are original and have no file to list:
|
||||
| `crates/ui-egui/src/i18n/hu.tsv` | @Skaywalker | Original Hungarian translations of CADCraft English strings | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | No proprietary localization resources copied |
|
||||
| `crates/ui-egui/src/i18n/tr.tsv` | @ogzhnklvz (AI-assisted) | Original Turkish translations of CADCraft English strings | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | No proprietary localization resources copied; written from the English strings, no other catalog used as a source; the running app reviewed by a native speaker |
|
||||
| `crates/ui-egui/src/i18n/el.tsv` | @Camthalion77 with ChatGPT (AI-assisted) | Original Greek translations of CADCraft English strings | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | No proprietary localization resources copied; independent native proofreading pending |
|
||||
| `crates/ui-egui/src/i18n/vi.tsv` | @2ez4gcx with Claude Code (AI-assisted) | Original Vietnamese translations of CADCraft English strings | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | No proprietary localization resources copied; terminology chosen with a native speaker, full proofreading pending |
|
||||
|
||||
@@ -47,10 +47,12 @@ pub enum Preference {
|
||||
Turkish,
|
||||
#[serde(rename = "bs")]
|
||||
Bosnian,
|
||||
#[serde(rename = "vi")]
|
||||
Vietnamese,
|
||||
}
|
||||
|
||||
impl Preference {
|
||||
pub const ALL: [Self; 18] = [
|
||||
pub const ALL: [Self; 19] = [
|
||||
Self::Auto,
|
||||
Self::English,
|
||||
Self::Greek,
|
||||
@@ -69,6 +71,7 @@ impl Preference {
|
||||
Self::Hungarian,
|
||||
Self::Turkish,
|
||||
Self::Bosnian,
|
||||
Self::Vietnamese,
|
||||
];
|
||||
|
||||
/// Every preference in the order the Interface Language dialog lists them: Auto, then English
|
||||
@@ -117,6 +120,7 @@ impl Preference {
|
||||
Self::Hungarian => "hu",
|
||||
Self::Turkish => "tr",
|
||||
Self::Bosnian => "bs",
|
||||
Self::Vietnamese => "vi",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,6 +150,14 @@ impl Preference {
|
||||
Self::Hungarian => "Magyar",
|
||||
Self::Turkish => "Türkçe",
|
||||
Self::Bosnian => "Bosanski",
|
||||
// Its letters need a font egui doesn't bundle ([`crate::theme::vietnamese_font`]).
|
||||
Self::Vietnamese => {
|
||||
if crate::theme::shows_vietnamese(CURRENT.with(Cell::get)) {
|
||||
"Tiếng Việt"
|
||||
} else {
|
||||
"Vietnamese"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,6 +189,7 @@ impl Preference {
|
||||
Self::Hungarian => "hu",
|
||||
Self::Turkish => "tr",
|
||||
Self::Bosnian => "bs",
|
||||
Self::Vietnamese => "vi",
|
||||
Self::Auto => locales.iter().find_map(|s| locale_language(s)).unwrap_or("en"),
|
||||
}
|
||||
}
|
||||
@@ -193,7 +206,7 @@ pub struct Language {
|
||||
catalog: OnceLock<Catalog<'static>>,
|
||||
}
|
||||
|
||||
pub static LANGUAGES: [Language; 17] = [
|
||||
pub static LANGUAGES: [Language; 18] = [
|
||||
Language { code: "en", name: "English", source: "", plural: plural_en, plural_forms: 2, catalog: OnceLock::new() },
|
||||
// Greek: one for exactly 1; other for zero and all other integer counts.
|
||||
Language {
|
||||
@@ -243,6 +256,10 @@ pub static LANGUAGES: [Language; 17] = [
|
||||
// Bosnian (Latin script, Ijekavian): one (1, 21), few (2–4, 22–24, not 12–14), other — the
|
||||
// Ukrainian rule for integer counts.
|
||||
Language { code: "bs", name: "Bosanski", source: include_str!("i18n/bs.tsv"), plural: plural_uk, plural_forms: 3, catalog: OnceLock::new() },
|
||||
// Vietnamese doesn't inflect a noun for number ("3 commit"), so like Korean it has one form.
|
||||
Language {
|
||||
code: "vi", name: "Tiếng Việt", source: include_str!("i18n/vi.tsv"), plural: plural_ko, plural_forms: 1, catalog: OnceLock::new()
|
||||
},
|
||||
];
|
||||
|
||||
/// BCP 47 and POSIX locale tags, including `uk-UA`, `uk_UA.UTF-8` and `pt_BR.UTF-8`.
|
||||
@@ -447,6 +464,8 @@ mod picker_order_tests {
|
||||
assert!(pos(Preference::Italian) < pos(Preference::Hungarian), "Magyar sorts under M");
|
||||
assert!(pos(Preference::Turkish) < pos(Preference::Greek), "Latin names come before Greek");
|
||||
assert!(pos(Preference::Bosnian) < pos(Preference::Czech), "Bosanski sorts under B");
|
||||
assert!(pos(Preference::Swedish) < pos(Preference::Vietnamese), "Tiếng Việt sorts under T");
|
||||
assert!(pos(Preference::Vietnamese) < pos(Preference::Turkish), "Tiếng comes before Türkçe");
|
||||
assert_eq!(name_sort_key("Türkçe"), "turkce");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,6 +187,7 @@ fn assert_additional_locale_aliases() {
|
||||
("hu", &["hu", "hu-HU", "hu_HU.UTF-8", "HU-hu", " hu-HU "]),
|
||||
("tr", &["tr", "tr-TR", "tr_TR.UTF-8", "TR-tr", " tr-TR ", "tr-CY"]),
|
||||
("bs", &["bs", "bs-BA", "bs_BA.UTF-8", "BS-ba", " bs-BA ", "bs-Latn-BA"]),
|
||||
("vi", &["vi", "vi-VN", "vi_VN.UTF-8", "VI-vn", " vi-VN "]),
|
||||
("el", &["el", "el-GR", "el_GR.UTF-8", "EL-cy"]),
|
||||
("zh", &["zh", "zh-CN"]),
|
||||
] {
|
||||
@@ -432,12 +433,13 @@ fn dialogs_render_in_every_language_and_keep_their_source_ids() {
|
||||
}
|
||||
|
||||
/// The letters of every catalog have glyphs in both UI families. Latin, Cyrillic and Greek come
|
||||
/// from egui's bundled fonts alone, as in the web build; the scripts they lack need a font that
|
||||
/// `install_fonts` finds, and are checked only where one is there.
|
||||
/// from egui's bundled fonts alone, as in the web build; the scripts they lack (and Vietnamese's
|
||||
/// tone-marked letters) need a font that `install_fonts` finds, and are checked only where one is
|
||||
/// there.
|
||||
#[test]
|
||||
fn interface_fonts_cover_every_letter_in_each_catalog() {
|
||||
for language in translated() {
|
||||
let extra_font = ["ko", "zh", "ar"].contains(&language.code);
|
||||
let extra_font = ["ko", "zh", "ar", "vi"].contains(&language.code);
|
||||
if extra_font && !crate::theme::can_show(language.code) {
|
||||
continue;
|
||||
}
|
||||
@@ -566,3 +568,22 @@ fn bosnian_counts_take_one_few_other() {
|
||||
assert_eq!(Preference::parse("bs"), Some(Preference::Bosnian));
|
||||
assert_eq!(Preference::Bosnian.native_name(), "Bosanski");
|
||||
}
|
||||
|
||||
/// Vietnamese has one form for every count, and shows only with an interface font that has its
|
||||
/// tone-marked letters; without one it stays English and says why.
|
||||
#[test]
|
||||
fn vietnamese_takes_one_form_and_needs_a_font_with_its_letters() {
|
||||
assert_eq!(Preference::parse("vi"), Some(Preference::Vietnamese));
|
||||
assert_eq!(Preference::Vietnamese.native_name(), "Tiếng Việt");
|
||||
let vi = LANGUAGES.iter().find(|l| l.code == "vi").unwrap();
|
||||
assert_eq!(vi.plural_forms, 1);
|
||||
assert!([0, 1, 2, 5, 21, 111].into_iter().all(|n| (vi.plural)(n) == 0));
|
||||
let mut app = CadApp::new(Session::new(), Services::default());
|
||||
let r = app.run("ui.language", json!({"lang": "vi"})).unwrap();
|
||||
let shown = crate::theme::can_show("vi");
|
||||
assert_eq!(r["shown"], if shown { "vi" } else { "en" });
|
||||
if !shown {
|
||||
assert!(app.status.as_ref().is_some_and(|(s, _)| s.contains("Vietnamese letters")));
|
||||
assert_eq!(Preference::Vietnamese.name(), "Vietnamese");
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -265,10 +265,15 @@ fn run_language_command(app: &mut CadApp, id: &str, params: &Value) -> Option<Re
|
||||
app.ui.interface_language = preference;
|
||||
let shown = app.language();
|
||||
if shown != preference.resolve(&app.system_languages) {
|
||||
app.set_status(if preference.resolve(&app.system_languages) == "ko" {
|
||||
"No font covers the Korean interface, so it stays in English. Install a Hangul font (Malgun Gothic or Nanum Gothic), or build with a Hang/Kore craft-fonts face."
|
||||
} else {
|
||||
"No Chinese font is installed, so the interface stays in English. Install one (Noto Sans CJK, for example) to use Chinese."
|
||||
app.set_status(match preference.resolve(&app.system_languages) {
|
||||
"ko" => {
|
||||
"No font covers the Korean interface, so it stays in English. Install a Hangul font (Malgun Gothic or Nanum Gothic), or build with a Hang/Kore craft-fonts face."
|
||||
}
|
||||
"vi" => {
|
||||
"No installed interface font has the Vietnamese letters, so the interface stays in English. Install one that does (Segoe UI, Arial, DejaVu Sans or Noto Sans) to use Vietnamese."
|
||||
}
|
||||
"ar" => "No Arabic font is installed, so the interface stays in English. Install one (Noto Sans Arabic, for example) to use Arabic.",
|
||||
_ => "No Chinese font is installed, so the interface stays in English. Install one (Noto Sans CJK, for example) to use Chinese.",
|
||||
});
|
||||
}
|
||||
Some(Ok(json!({"interfaceLanguage": preference.code(), "shown": shown})))
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
//! |---|---|
|
||||
//! | `theme` | `"system"`, `"light"` or `"dark"` |
|
||||
//! | `animations` | `"system"`, `"on"` or `"off"`: dialog and menu fades and smoothed scrolling |
|
||||
//! | `interfaceLanguage` | `"auto"`, `"en"`, `"uk"`, `"pt-BR"`, `"de"`, `"es"`, `"cs"`, `"ru"`, `"fr"`, `"zh"`, `"it"`, `"ko"`, `"sv"`, `"ar"`, `"hu"`, `"tr"`, `"bs"` or `"el"` |
|
||||
//! | `interfaceLanguage` | `"auto"`, `"en"`, `"uk"`, `"pt-BR"`, `"de"`, `"es"`, `"cs"`, `"ru"`, `"fr"`, `"zh"`, `"it"`, `"ko"`, `"sv"`, `"ar"`, `"hu"`, `"tr"`, `"bs"`, `"vi"` or `"el"` |
|
||||
//! | `saveFormat` | `"dxf"` or `"dwg"`: the format Save As suggests for new drawings |
|
||||
//! | `saveVersion` | `"2000"` … `"2018"`: the version drawings with no file yet are saved in |
|
||||
//! | `SAVETIME` | minutes between automatic saves, 0–600 (0: off) |
|
||||
|
||||
+96
-14
@@ -326,19 +326,11 @@ pub fn install_fonts(ctx: &egui::Context, language: &str) {
|
||||
let chinese = language.starts_with("zh");
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let candidates: &[&str] = if cfg!(target_os = "macos") {
|
||||
&["/System/Library/Fonts/SFNS.ttf", "/System/Library/Fonts/Helvetica.ttc", "/Library/Fonts/Arial.ttf"]
|
||||
} else if cfg!(windows) {
|
||||
&["C:\\Windows\\Fonts\\segoeui.ttf", "C:\\Windows\\Fonts\\arial.ttf"]
|
||||
} else {
|
||||
&[
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/TTF/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/noto/NotoSans-Regular.ttf",
|
||||
"/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf",
|
||||
]
|
||||
};
|
||||
for path in candidates {
|
||||
for path in system_ui_candidates() {
|
||||
// A Vietnamese interface takes the first one with its letters.
|
||||
if language == "vi" && vietnamese_font() != Some(std::path::Path::new(path)) {
|
||||
continue;
|
||||
}
|
||||
if let Ok(bytes) = std::fs::read(path) {
|
||||
fonts.font_data.insert("system-ui".into(), std::sync::Arc::new(egui::FontData::from_owned(bytes)));
|
||||
if let Some(f) = fonts.families.get_mut(&FontFamily::Proportional) {
|
||||
@@ -409,6 +401,82 @@ pub fn install_fonts(ctx: &egui::Context, language: &str) {
|
||||
ctx.set_fonts(fonts);
|
||||
}
|
||||
|
||||
/// The system interface fonts [`install_fonts`] looks for, in order; it loads the first installed
|
||||
/// one ahead of egui's bundled faces.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn system_ui_candidates() -> &'static [&'static str] {
|
||||
if cfg!(target_os = "macos") {
|
||||
&["/System/Library/Fonts/SFNS.ttf", "/System/Library/Fonts/Helvetica.ttc", "/Library/Fonts/Arial.ttf"]
|
||||
} else if cfg!(windows) {
|
||||
&["C:\\Windows\\Fonts\\segoeui.ttf", "C:\\Windows\\Fonts\\arial.ttf"]
|
||||
} else {
|
||||
&[
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/TTF/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/noto/NotoSans-Regular.ttf",
|
||||
"/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/// The first of `candidates` that is `installed` and, for the Vietnamese interface, has the
|
||||
/// Vietnamese letters: the system interface font [`install_fonts`] loads ahead of egui's faces.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn ui_font_choice<'a>(
|
||||
candidates: &[&'a str],
|
||||
vietnamese: bool,
|
||||
installed: impl Fn(&str) -> bool,
|
||||
has_vietnamese: impl Fn(&str) -> bool,
|
||||
) -> Option<&'a str> {
|
||||
candidates.iter().copied().find(|p| installed(p) && (!vietnamese || has_vietnamese(p)))
|
||||
}
|
||||
|
||||
/// Whether `face` has every letter Vietnamese adds to Latin: the tone-marked vowels of Latin
|
||||
/// Extended Additional (U+1EA0–U+1EF9), and ơ, ư, đ.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn covers_vietnamese(face: &cadcraft_fonts::ttf::Face) -> bool {
|
||||
('\u{1ea0}'..='\u{1ef9}').chain(['ơ', 'Ơ', 'ư', 'Ư', 'đ', 'Đ']).all(|c| face.has_glyph(c))
|
||||
}
|
||||
|
||||
/// The system interface font the Vietnamese interface takes: the first of
|
||||
/// [`system_ui_candidates`] whose first face (the one egui uses) has every Vietnamese letter.
|
||||
/// Segoe UI, Arial, SF, DejaVu Sans and Noto Sans do; egui's bundled faces stop at Latin
|
||||
/// Extended-A, so without one the interface stays English (as the web build does). Checked once.
|
||||
pub fn vietnamese_font() -> Option<&'static std::path::Path> {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
static FOUND: std::sync::OnceLock<Option<std::path::PathBuf>> = std::sync::OnceLock::new();
|
||||
FOUND
|
||||
.get_or_init(|| {
|
||||
let has_vietnamese = |p: &str| {
|
||||
read_capped(std::path::Path::new(p), MAX_UI_FONT_BYTES)
|
||||
.and_then(|bytes| cadcraft_fonts::ttf::faces(&std::sync::Arc::new(bytes)).into_iter().next())
|
||||
.is_some_and(|face| covers_vietnamese(&face))
|
||||
};
|
||||
ui_font_choice(system_ui_candidates(), true, |p| std::path::Path::new(p).is_file(), has_vietnamese).map(std::path::PathBuf::from)
|
||||
})
|
||||
.as_deref()
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
None
|
||||
}
|
||||
|
||||
/// Whether the interface in `language` shows the Vietnamese letters (Tiếng Việt in the language
|
||||
/// picker): the Vietnamese interface with [`vietnamese_font`], any other when the system interface
|
||||
/// font it loads is that one.
|
||||
pub fn shows_vietnamese(language: &str) -> bool {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let loaded = || ui_font_choice(system_ui_candidates(), false, |p| std::path::Path::new(p).is_file(), |_| true);
|
||||
vietnamese_font().is_some_and(|v| language == "vi" || loaded().is_some_and(|p| std::path::Path::new(p) == v))
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let _ = language;
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// The faces `build.rs` embedded from craft-fonts that the interface loads, in order: every CJK
|
||||
/// face, because file, layout and layer names can be Japanese or Chinese whatever the interface
|
||||
/// shows (#513), with the interface's own face leading. A sans-serif face reads best, and a
|
||||
@@ -659,10 +727,12 @@ pub fn shows_hangul(language: &str) -> bool {
|
||||
|
||||
/// Whether the interface can show `language` without missing-glyph boxes: Chinese needs a font
|
||||
/// with its glyphs (installed, or the CJK face this build embedded from craft-fonts), Korean a
|
||||
/// font with Hangul, العربية an installed Arabic font or the embedded Arabic face.
|
||||
/// font with Hangul, العربية an installed Arabic font or the embedded Arabic face, Tiếng Việt a
|
||||
/// system interface font with its tone-marked letters ([`vietnamese_font`]).
|
||||
pub fn can_show(language: &str) -> bool {
|
||||
match language {
|
||||
"ko" => shows_hangul("ko"),
|
||||
"vi" => vietnamese_font().is_some(),
|
||||
"ar" => arabic_font().is_some() || embedded_script("Arab"),
|
||||
l if l.starts_with("zh") => cjk_font().is_some() || embedded_script("Hans") || embedded_script("Hant"),
|
||||
_ => true,
|
||||
@@ -737,6 +807,18 @@ pub fn apply(ctx: &egui::Context, theme: Theme, follow_system: bool) {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
/// The system interface font loaded: the first installed one, and for Vietnamese the first
|
||||
/// installed one with its letters (a later one when the first lacks them).
|
||||
#[test]
|
||||
fn the_vietnamese_interface_takes_the_first_installed_font_with_its_letters() {
|
||||
let candidates = ["a.ttf", "b.ttf", "c.ttf", "d.ttf"];
|
||||
let installed = |p: &str| p != "a.ttf";
|
||||
let has_vietnamese = |p: &str| p == "c.ttf" || p == "d.ttf";
|
||||
assert_eq!(super::ui_font_choice(&candidates, false, installed, has_vietnamese), Some("b.ttf"));
|
||||
assert_eq!(super::ui_font_choice(&candidates, true, installed, has_vietnamese), Some("c.ttf"));
|
||||
assert_eq!(super::ui_font_choice(&candidates, true, installed, |_| false), None);
|
||||
}
|
||||
|
||||
use cadcraft_fonts::CraftFont;
|
||||
|
||||
/// A face shaped like `build.rs` writes them, with no font file (as in `cadcraft-fonts`).
|
||||
|
||||
Reference in New Issue
Block a user