Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ All current and future right-sidebar tables use `crate::inspector_table::Inspect
English is the source language for all user-facing application text, even when the user requests features or changes in any language. Display text in the user's selected application language through the existing translation system. This includes section titles, menus, controls, tooltips, notifications, and errors. Preserve stable internal preference keys and saved-project compatibility when translating visible text.

- Every new or changed user-facing label must be translatable. Use the existing `tr!` message lookup with a stable catalog key instead of hard-coded display text.
- Reuse an existing message key when appropriate; otherwise add the English source message to `assets/i18n/en.json` and corresponding translations to every supported language catalog in `assets/i18n/`. Keep named placeholders and plural forms consistent across catalogs.
- Reuse an existing message key when appropriate; otherwise add the English source message to `assets/i18n/en_US.json` and corresponding translations to every supported language catalog in `assets/i18n/`. Keep named placeholders and plural forms consistent across catalogs.

# PDF export header

Expand Down
9 changes: 5 additions & 4 deletions assets/i18n/en.json → assets/i18n/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -1342,7 +1342,7 @@
"settings.inference_points": "Inference points",
"settings.infinite_grid": "Infinite grid",
"settings.language": "Language",
"settings.language_english": "English",
"settings.language.english": "English",
"settings.length_of_each_side_of_the_square_centered": "Length of each side of the square, centered on the model origin. The boundary stays fixed when zooming or rotating.",
"settings.material": "Material",
"settings.materials_estimates": "Materials & estimates",
Expand Down Expand Up @@ -2067,9 +2067,10 @@
"settings.invert_pan": "Invert pan",
"settings.invert_tilt_and_twist": "Invert tilt and twist",
"settings.invert_zoom": "Invert zoom",
"settings.language_simplified_chinese": "Simplified Chinese",
"settings.language_brazilian_portuguese": "Brazilian Portuguese",
"settings.language_vietnamese": "Vietnamese",
"settings.language.simplified_chinese": "Simplified Chinese",
"settings.language.brazilian_portuguese": "Brazilian Portuguese",
"settings.language.vietnamese": "Vietnamese",
"settings.language.spanish": "Spanish",
"settings.look_sensitivity": "Look sensitivity",
"settings.mouse_trackpad": "Mouse/Trackpad",
"settings.navigation_type": "Navigation type",
Expand Down
2,185 changes: 2,185 additions & 0 deletions assets/i18n/es_419.json

Large diffs are not rendered by default.

9 changes: 5 additions & 4 deletions assets/i18n/pt_BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -1340,7 +1340,7 @@
"settings.inference_points": "Pontos de inferência",
"settings.infinite_grid": "Grade infinita",
"settings.language": "Idioma",
"settings.language_english": "English",
"settings.language.english": "English",
"settings.length_of_each_side_of_the_square_centered": "Comprimento de cada lado do quadrado, centralizado na origem do modelo. O limite permanece fixo ao dar zoom ou girar.",
"settings.material": "Material",
"settings.materials_estimates": "Materiais e estimativas",
Expand Down Expand Up @@ -2065,9 +2065,10 @@
"settings.invert_pan": "Inverter mover vista",
"settings.invert_tilt_and_twist": "Inverter inclinação e giro",
"settings.invert_zoom": "Inverter zoom",
"settings.language_simplified_chinese": "简体中文",
"settings.language_brazilian_portuguese": "Português (Brasil)",
"settings.language_vietnamese": "Vietnamita",
"settings.language.simplified_chinese": "简体中文",
"settings.language.brazilian_portuguese": "Português (Brasil)",
"settings.language.vietnamese": "Tiếng Việt",
"settings.language.spanish": "Español",
"settings.look_sensitivity": "Sensibilidade do olhar",
"settings.mouse_trackpad": "Mouse/trackpad",
"settings.navigation_type": "Tipo de navegação",
Expand Down
9 changes: 5 additions & 4 deletions assets/i18n/vi.json → assets/i18n/vi_VN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1342,7 +1342,7 @@
"settings.inference_points": "Điểm bắt",
"settings.infinite_grid": "Lưới vô hạn",
"settings.language": "Ngôn ngữ",
"settings.language_english": "Tiếng Anh",
"settings.language.english": "English",
"settings.length_of_each_side_of_the_square_centered": "Chiều dài mỗi cạnh của hình vuông, lấy gốc mô hình làm tâm. Ranh giới giữ nguyên khi thu phóng hoặc xoay.",
"settings.material": "Vật liệu",
"settings.materials_estimates": "Vật liệu và dự toán",
Expand Down Expand Up @@ -2067,9 +2067,10 @@
"settings.invert_pan": "Đảo chiều dịch chuyển",
"settings.invert_tilt_and_twist": "Đảo chiều nghiêng và xoay",
"settings.invert_zoom": "Đảo chiều thu phóng",
"settings.language_simplified_chinese": "Tiếng Trung giản thể",
"settings.language_brazilian_portuguese": "Tiếng Bồ Đào Nha (Brazil)",
"settings.language_vietnamese": "Tiếng Việt",
"settings.language.simplified_chinese": "简体中文",
"settings.language.brazilian_portuguese": "Português (Brasil)",
"settings.language.vietnamese": "Tiếng Việt",
"settings.language.spanish": "Español",
"settings.look_sensitivity": "Độ nhạy nhìn",
"settings.mouse_trackpad": "Chuột/Bàn di chuột",
"settings.navigation_type": "Kiểu điều hướng",
Expand Down
9 changes: 5 additions & 4 deletions assets/i18n/zh_CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1342,7 +1342,7 @@
"settings.inference_points": "推断点",
"settings.infinite_grid": "无限网格",
"settings.language": "语言",
"settings.language_english": "English",
"settings.language.english": "English",
"settings.length_of_each_side_of_the_square_centered": "以模型原点为中心的正方形边长。缩放或旋转时,边界保持固定。",
"settings.material": "材质",
"settings.materials_estimates": "材料与估算",
Expand Down Expand Up @@ -2067,9 +2067,10 @@
"settings.invert_pan": "反转平移",
"settings.invert_tilt_and_twist": "反转倾斜和扭转",
"settings.invert_zoom": "反转缩放",
"settings.language_simplified_chinese": "简体中文",
"settings.language_brazilian_portuguese": "Português (Brasil)",
"settings.language_vietnamese": "越南语",
"settings.language.simplified_chinese": "简体中文",
"settings.language.brazilian_portuguese": "Português (Brasil)",
"settings.language.vietnamese": "Tiếng Việt",
"settings.language.spanish": "Español",
"settings.look_sensitivity": "视角灵敏度",
"settings.mouse_trackpad": "鼠标/触控板",
"settings.navigation_type": "导航类型",
Expand Down
5 changes: 3 additions & 2 deletions crates/benchup-app/src/app_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7281,10 +7281,11 @@ fn view_menu_localizes_display_options_and_toggles_selected_edge_lengths_without
let _reset = ResetLanguage;
let mut h = Harness::new();
for (language, view, wireframe, lengths) in [
("en", "View", "Wireframe other objects while moving", "Selected Edge Lengths"),
("en_US", "View", "Wireframe other objects while moving", "Selected Edge Lengths"),
("zh_CN", "视图", "移动时以线框显示其他对象", "所选边的长度"),
("pt_BR", "Visualizar", "Exibir outros objetos em aramado ao mover", "Comprimentos das arestas selecionadas"),
("en", "View", "Wireframe other objects while moving", "Selected Edge Lengths"),
("es_419", "Vista", "Estructura alámbrica de otros objetos al mover", "Longitudes de aristas seleccionadas"),
("en_US", "View", "Wireframe other objects while moving", "Selected Edge Lengths"),
] {
h.app.general_settings.language = language.into();
benchup_core::i18n::set_language(language);
Expand Down
4 changes: 2 additions & 2 deletions crates/benchup-app/src/estimate_pdf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -656,14 +656,14 @@ mod tests {
}
}
let _reset = ResetLanguage;
benchup_core::i18n::set_language("vi");
benchup_core::i18n::set_language("vi_VN");
let mut report = Report {
name: "Xưởng gỗ Việt".into(),
currency: "VND".into(),
notes: vec![tr!("wood_glue.estimate_note", area = "1 m²")],
..Default::default()
};
for language in ["vi", "en"] {
for language in ["vi_VN", "en_US"] {
benchup_core::i18n::set_language(language);
let bytes =
render(&report).expect("Vietnamese project names and notes can be exported");
Expand Down
17 changes: 13 additions & 4 deletions crates/benchup-app/src/export_scale.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,10 +362,11 @@ mod tests {
prefs.presets.push(custom);
let saved = serde_json::to_string(&prefs).unwrap();
for (language, mockup) in [
("en", "Woodworking mockup 1:2"),
("en_US", "Woodworking mockup 1:2"),
("zh_CN", "木工样机 1:2"),
("pt_BR", "Maquete de marcenaria 1:2"),
("en", "Woodworking mockup 1:2"),
("es_419", "Maqueta de carpintería 1:2"),
("en_US", "Woodworking mockup 1:2"),
] {
benchup_core::i18n::set_language(language);
let output = ctx.run_test_ui(
Expand Down Expand Up @@ -401,7 +402,7 @@ mod tests {
app.project.export_scale_open = true;
for (language, title, actual, custom, preview, error) in [
(
"en",
"en_US",
"Export STL",
"Actual size (1:1)",
"Custom ratio",
Expand All @@ -425,7 +426,15 @@ mod tests {
"Insira dois números finitos maiores que zero.",
),
(
"en",
"es_419",
"Exportar STL",
"Tamaño real (1:1)",
"Proporción personalizada",
"Escala: 50.000000%",
"Introduce dos números finitos mayores que cero.",
),
(
"en_US",
"Export STL",
"Actual size (1:1)",
"Custom ratio",
Expand Down
36 changes: 22 additions & 14 deletions crates/benchup-app/src/language.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,16 @@ fn supported_language(locale: &str) -> Option<&'static str> {
.find_map(|&(code, _)| (code == "zh_CN").then_some(code));
}

// Brazilian Portuguese is the only Portuguese catalog, so it also serves
// bare pt and other Portuguese regions instead of falling back to English.
if parts.first() == Some(&"pt") {
return supported
.iter()
.find_map(|&(code, _)| (code == "pt_BR").then_some(code));
// Each remaining language has a single regional catalog (en_US, pt_BR,
// es_419), so it also serves the bare language and its other regions
// instead of falling back to English. Chinese is resolved above.
let primary = *parts.first()?;
if primary == "zh" {
return None;
}
None
supported
.iter()
.find_map(|&(code, _)| (code.split('_').next() == Some(primary)).then_some(code))
}

#[cfg(test)]
Expand All @@ -67,7 +69,7 @@ mod tests {
#[test]
fn matches_supported_browser_and_os_locale_variants() {
for locale in ["en", "en-US", "EN_gb.UTF-8", "en-CA", "en-US-u-hc-h12"] {
assert_eq!(preferred_language([locale]), "en", "{locale}");
assert_eq!(preferred_language([locale]), "en_US", "{locale}");
}
for locale in [
"zh",
Expand All @@ -84,7 +86,10 @@ mod tests {
assert_eq!(preferred_language([locale]), "pt_BR", "{locale}");
}
for locale in ["vi", "vi-VN", "vi_VN", "vi_VN.UTF-8"] {
assert_eq!(preferred_language([locale]), "vi", "{locale}");
assert_eq!(preferred_language([locale]), "vi_VN", "{locale}");
}
for locale in ["es", "es_ES", "es-ES", "es_MX.UTF-8", "es-419", "es-CO", "ES-ar"] {
assert_eq!(preferred_language([locale]), "es_419", "{locale}");
}
}

Expand All @@ -101,18 +106,21 @@ mod tests {
"C",
"",
] {
assert_eq!(preferred_language([locale]), "en", "{locale}");
assert_eq!(preferred_language([locale]), "en_US", "{locale}");
}
assert_eq!(preferred_language(std::iter::empty::<&str>()), "en");
assert_eq!(preferred_language(std::iter::empty::<&str>()), "en_US");
}

#[test]
fn uses_the_first_supported_language_in_preference_order() {
assert_eq!(preferred_language(["fr-CA", "zh-Hans", "en-US"]), "zh_CN");
assert_eq!(preferred_language(["en-CA", "zh-CN"]), "en");
assert_eq!(preferred_language(["en-CA", "zh-CN"]), "en_US");
assert_eq!(preferred_language(["zh-Hant", "zh-Hans"]), "zh_CN");
assert_eq!(preferred_language(["fr-CA", "pt-BR", "en-US"]), "pt_BR");
assert_eq!(preferred_language(["fr-CA", "vi-VN", "en-US"]), "vi");
assert_eq!(preferred_language(["en-US", "pt-BR"]), "en");
assert_eq!(preferred_language(["fr-CA", "vi-VN", "en-US"]), "vi_VN");
assert_eq!(preferred_language(["en-US", "pt-BR"]), "en_US");
assert_eq!(preferred_language(["fr-CA", "es-MX", "en-US"]), "es_419");
assert_eq!(preferred_language(["es-419", "pt-BR"]), "es_419");
assert_eq!(preferred_language(["en-US", "es-ES"]), "en_US");
}
}
81 changes: 69 additions & 12 deletions crates/benchup-app/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,20 +151,21 @@ pub(super) const TOOLS: [(Tool, &str); 18] = [
(Tool::Image, "J"),
];

const DEFAULT_LANGUAGE: &str = "en";
const DEFAULT_LANGUAGE: &str = "en_US";
#[path = "language.rs"]
mod language;

pub(super) fn detected_language() -> &'static str {
language::detected_language()
}

fn languages() -> [(&'static str, &'static str); 4] {
fn languages() -> [(&'static str, &'static str); 5] {
[
(DEFAULT_LANGUAGE, tr!("settings.language_english")),
("zh_CN", tr!("settings.language_simplified_chinese")),
("pt_BR", tr!("settings.language_brazilian_portuguese")),
("vi", tr!("settings.language_vietnamese")),
(DEFAULT_LANGUAGE, tr!("settings.language.english")),
("zh_CN", tr!("settings.language.simplified_chinese")),
("pt_BR", tr!("settings.language.brazilian_portuguese")),
("vi_VN", tr!("settings.language.vietnamese")),
("es_419", tr!("settings.language.spanish")),
]
}

Expand Down Expand Up @@ -291,6 +292,13 @@ impl GeneralSettings {
self.space_mouse.normalize();
self.stock.normalize();
self.export_scale.normalize();
// Earlier builds saved bare codes; every catalog now uses language_REGION.
match self.language.as_str() {
"en" => self.language = "en_US".into(),
"vi" => self.language = "vi_VN".into(),
"es" => self.language = "es_419".into(),
_ => {}
}
if !languages().iter().any(|&(code, _)| code == self.language.as_str()) {
self.language = DEFAULT_LANGUAGE.into();
}
Expand Down Expand Up @@ -1192,11 +1200,59 @@ mod tests {

#[test]
fn supported_languages_include_vietnamese() {
assert!(languages().contains(&("vi", "Vietnamese")));
assert!(languages().contains(&("vi_VN", "Vietnamese")));
let mut settings = GeneralSettings::default();
settings.language = "vi".into();
settings.language = "vi_VN".into();
settings.normalize();
assert_eq!(settings.language, "vi");
assert_eq!(settings.language, "vi_VN");
}

#[test]
fn supported_languages_include_spanish() {
assert!(languages().contains(&("es_419", "Spanish")));
let mut settings = GeneralSettings::default();
settings.language = "es_419".into();
settings.normalize();
assert_eq!(settings.language, "es_419");
}

#[test]
fn every_language_code_uses_language_and_region() {
for (code, _) in languages() {
let (language, region) = code.split_once('_').expect(code);
assert!(!language.is_empty() && !region.is_empty(), "{code}");
}
}

#[test]
fn saved_bare_language_codes_migrate_to_regional_codes() {
for (saved, expected) in [("en", "en_US"), ("vi", "vi_VN"), ("es", "es_419"), ("unsupported", "en_US")] {
let mut settings = GeneralSettings::default();
settings.language = saved.into();
settings.normalize();
assert_eq!(settings.language, expected, "{saved}");
}
assert_eq!(GeneralSettings::default().language, "en_US");
}

#[test]
fn language_picker_shows_native_names_in_translated_catalogs() {
struct Reset;
impl Drop for Reset {
fn drop(&mut self) { benchup_core::i18n::set_language("en"); }
}
let _reset = Reset;
let native = [
("en_US", "English"),
("zh_CN", "简体中文"),
("pt_BR", "Português (Brasil)"),
("vi_VN", "Tiếng Việt"),
("es_419", "Español"),
];
for (language, _) in &native[1..] {
benchup_core::i18n::set_language(language);
assert_eq!(languages(), native, "{language}");
}
}

#[test]
Expand All @@ -1210,11 +1266,12 @@ mod tests {
let mut app = BenchUpApp::with_document(Document::default(), None, true);
app.settings_open = true;
for (language, title, group, export_scales) in [
("en", "Settings", "Workspace", "Export scales"),
("en_US", "Settings", "Workspace", "Export scales"),
("zh_CN", "设置", "工作区", "导出比例"),
("pt_BR", "Configurações", "Área de trabalho", "Escalas de exportação"),
("vi", "Cài đặt", "Không gian làm việc", "Tỷ lệ xuất"),
("en", "Settings", "Workspace", "Export scales"),
("vi_VN", "Cài đặt", "Không gian làm việc", "Tỷ lệ xuất"),
("es_419", "Configuración", "Espacio de trabajo", "Escalas de exportación"),
("en_US", "Settings", "Workspace", "Export scales"),
] {
app.general_settings.language = language.into();
let input = || egui::RawInput {
Expand Down
Loading
Loading