Start page: translate its buttons, links, headings and samples (#1479)

Checked locally on main merged with this: cargo check and the ui-egui tests, including the new i18n ones.
This commit is contained in:
rlartot authored and GitHub committed 2026-10-15 15:38:46 -04:00
1 parent b56648c055
commit 7934edcad4
10 files changed
+309 -27

No files matched your search

+25
View File
@@ -26,6 +26,31 @@
"dialog.selection_guidance": "Wählen Sie eine Eingabe aus, um sie zu ändern. Klicken Sie auf ausgewählte Objekte, um sie zu entfernen; Strg/Cmd-Klick schaltet sie ebenfalls um. Prüfen Sie die Vorschau und drücken Sie dann OK.",
"document.save_changes": "Änderungen in {name} speichern?",
"document.unsaved_changes": "{name} enthält ungespeicherte Änderungen. Wenn Sie nicht speichern, gehen sie verloren.",
"home.documentation": "Dokumentation",
"home.in_this_browser": "in diesem Browser",
"home.missing": "fehlt",
"home.open": "Öffnen…",
"home.opened_days_ago": "Vor {count} Tag(en) geöffnet",
"home.opened_hours_ago": "Vor {count} Std. geöffnet",
"home.opened_just_now": "Gerade eben geöffnet",
"home.opened_minutes_ago": "Vor {count} Min. geöffnet",
"home.opening": "wird geöffnet…",
"home.recent": "Zuletzt verwendet",
"home.report_issue": "Problem melden",
"home.samples": "Beispiele",
"home.whats_new": "Neuigkeiten",
"sample.bracket.about": "Grundflansch mit Kantenflanschen und einer Umkantung nach einer 2-mm-Blechregel",
"sample.bracket.name": "Blechwinkel",
"sample.enclosure.about": "Ausgehöhlter Block mit Lippe, Schraubenerhöhungen, Auflage und Rastverbindung",
"sample.enclosure.name": "Kunststoffgehäuse",
"sample.flange.about": "Nabe und Flansch mit einem parametergesteuerten radialen Muster",
"sample.flange.name": "Schraubflansch",
"sample.hinge.about": "Zwei Komponenten und ein Drehgelenk, auf 60° angetrieben",
"sample.hinge.name": "Scharnierbaugruppe",
"sample.plate.about": "Parametrische Platte mit Vollrundungen, Bohrungen und einer aufgebohrten Erhöhung",
"sample.plate.name": "Beispielplatte",
"sample.vents.about": "Langlöcher in einem Rechteckmuster: Zeilen, Spalten und Abstand sind Parameter",
"sample.vents.name": "Lüftungsplatte",
"settings.msaa_applies_on_restart": "Die Kantenglättung wird beim nächsten Start von SolidCraft übernommen",
"settings.msaa_reset": "{setting}: {requested}× wird von diesem Grafikgerät nicht unterstützt; jetzt {reset}× (unterstützt: {supported}).",
"settings.msaa_unsupported": "Von diesem Grafikgerät nicht unterstützt",
+25
View File
@@ -306,6 +306,31 @@
"dialog.selection_guidance": "Select an input to change it. Click selected objects to remove them; Ctrl/Cmd-click also toggles them. Review the preview, then press OK.",
"document.save_changes": "Save changes to {name}?",
"document.unsaved_changes": "{name} has unsaved changes. If you don't save, they are lost.",
"home.documentation": "Documentation",
"home.in_this_browser": "in this browser",
"home.missing": "missing",
"home.open": "Open…",
"home.opened_days_ago": "Opened {count} days ago",
"home.opened_hours_ago": "Opened {count} h ago",
"home.opened_just_now": "Opened just now",
"home.opened_minutes_ago": "Opened {count} min ago",
"home.opening": "opening…",
"home.recent": "Recent",
"home.report_issue": "Report an Issue",
"home.samples": "Samples",
"home.whats_new": "What's new",
"sample.bracket.about": "Base flange with edge flanges and a hem on a 2 mm rule",
"sample.bracket.name": "Sheet Metal Bracket",
"sample.enclosure.about": "Shelled box with a lip, screw bosses, a rest and a snap fit",
"sample.enclosure.name": "Plastic Enclosure",
"sample.flange.about": "Hub and flange with a circular pattern driven by a parameter",
"sample.flange.name": "Bolted Flange",
"sample.hinge.about": "Two components and a revolute joint, driven to 60°",
"sample.hinge.name": "Hinge Assembly",
"sample.plate.about": "Parametric plate with fillets, holes and a bored boss",
"sample.plate.name": "Sample Plate",
"sample.vents.about": "Slots in a rectangular pattern: rows, columns and pitch are parameters",
"sample.vents.name": "Vent Plate",
"settings.msaa_applies_on_restart": "Anti-aliasing applies the next time SolidCraft starts",
"settings.msaa_reset": "{setting}: {requested}× isn't supported by this graphics device; it is now {reset}× (supported: {supported}).",
"settings.msaa_unsupported": "Not supported by this graphics device",
+25
View File
@@ -306,6 +306,31 @@
"dialog.selection_guidance": "Sélectionner une entrée pour la modifier. Cliquer sur un objet sélectionné pour le retirer ; Ctrl/Cmd-clic le bascule aussi. Vérifier l'aperçu, puis appuyer sur OK.",
"document.save_changes": "Enregistrer les modifications de {name} ?",
"document.unsaved_changes": "{name} contient des modifications non enregistrées. Si vous n'enregistrez pas, elles seront perdues.",
"home.documentation": "Documentation",
"home.in_this_browser": "dans ce navigateur",
"home.missing": "introuvable",
"home.open": "Ouvrir…",
"home.opened_days_ago": "Ouvert il y a {count} j",
"home.opened_hours_ago": "Ouvert il y a {count} h",
"home.opened_just_now": "Ouvert à l'instant",
"home.opened_minutes_ago": "Ouvert il y a {count} min",
"home.opening": "ouverture…",
"home.recent": "Récents",
"home.report_issue": "Signaler un problème",
"home.samples": "Exemples",
"home.whats_new": "Nouveautés",
"sample.bracket.about": "Tôle de base avec bords tombés et un ourlet, selon une règle de 2 mm",
"sample.bracket.name": "Équerre en tôle",
"sample.enclosure.about": "Boîte en coque avec lèvre, bossages de vis, appui et clip",
"sample.enclosure.name": "Boîtier plastique",
"sample.flange.about": "Moyeu et bride avec un réseau circulaire piloté par un paramètre",
"sample.flange.name": "Bride boulonnée",
"sample.hinge.about": "Deux composants et une liaison pivot, entraînée à 60°",
"sample.hinge.name": "Assemblage de charnière",
"sample.plate.about": "Plaque paramétrique avec congés, perçages et un bossage alésé",
"sample.plate.name": "Plaque d'exemple",
"sample.vents.about": "Rainures en réseau rectangulaire : lignes, colonnes et pas sont des paramètres",
"sample.vents.name": "Plaque d'aération",
"settings.msaa_applies_on_restart": "L'anticrénelage s'appliquera au prochain démarrage de SolidCraft",
"settings.msaa_reset": "{setting} : {requested}× n'est pas pris en charge par ce périphérique graphique ; la valeur est désormais {reset}× (pris en charge : {supported}).",
"settings.msaa_unsupported": "Non pris en charge par ce périphérique graphique",
+25
View File
@@ -306,6 +306,31 @@
"dialog.selection_guidance": "入力欄を選んで変更します。選択済みのオブジェクトをクリックすると解除できます。Ctrl/Cmd を押しながらクリックしても切り替えられます。プレビューを確認してから OK を押してください。",
"document.save_changes": "{name} への変更を保存しますか?",
"document.unsaved_changes": "{name} には保存されていない変更があります。保存しない場合、変更内容は失われます。",
"home.documentation": "ドキュメント",
"home.in_this_browser": "このブラウザ内",
"home.missing": "見つかりません",
"home.open": "開く…",
"home.opened_days_ago": "{count} 日前に開きました",
"home.opened_hours_ago": "{count} 時間前に開きました",
"home.opened_just_now": "たった今開きました",
"home.opened_minutes_ago": "{count} 分前に開きました",
"home.opening": "開いています…",
"home.recent": "最近使用した項目",
"home.report_issue": "問題を報告",
"home.samples": "サンプル",
"home.whats_new": "新機能",
"sample.bracket.about": "2 mm の板金ルールによるベース フランジ、エッジ フランジ、ヘミング",
"sample.bracket.name": "板金ブラケット",
"sample.enclosure.about": "リップ、ねじボス、レスト、スナップ フィットを備えたシェル化した直方体",
"sample.enclosure.name": "プラスチック筐体",
"sample.flange.about": "パラメータで制御する円形パターンを持つハブとフランジ",
"sample.flange.name": "ボルト締めフランジ",
"sample.hinge.about": "2 つのコンポーネントと回転ジョイント、60° まで駆動",
"sample.hinge.name": "ヒンジ アセンブリ",
"sample.plate.about": "フィレット、穴、穴あきボスを備えたパラメトリック プレート",
"sample.plate.name": "サンプル プレート",
"sample.vents.about": "矩形パターンに並んだスロット: 行数、列数、ピッチはパラメータ",
"sample.vents.name": "通気プレート",
"settings.msaa_applies_on_restart": "アンチエイリアスは次回 SolidCraft を起動したときに適用されます",
"settings.msaa_reset": "{setting}: {requested}× はこのグラフィックスデバイスではサポートされていないため、{reset}× に変更しました(サポート: {supported})。",
"settings.msaa_unsupported": "このグラフィックスデバイスではサポートされていません",
+25
View File
@@ -26,6 +26,31 @@
"dialog.selection_guidance": "변경할 입력을 선택하세요. 선택한 객체를 클릭하면 선택이 해제되며, Ctrl/Cmd를 누른 채 클릭해도 전환됩니다. 미리 보기를 확인한 다음 확인을 누르세요.",
"document.save_changes": "{name}의 변경 사항을 저장하시겠습니까?",
"document.unsaved_changes": "{name}에 저장되지 않은 변경 사항이 있습니다. 저장하지 않으면 변경 사항이 사라집니다.",
"home.documentation": "문서",
"home.in_this_browser": "이 브라우저에 저장됨",
"home.missing": "찾을 수 없음",
"home.open": "열기…",
"home.opened_days_ago": "{count}일 전에 열림",
"home.opened_hours_ago": "{count}시간 전에 열림",
"home.opened_just_now": "방금 열림",
"home.opened_minutes_ago": "{count}분 전에 열림",
"home.opening": "여는 중…",
"home.recent": "최근 항목",
"home.report_issue": "문제 신고",
"home.samples": "샘플",
"home.whats_new": "새로운 기능",
"sample.bracket.about": "2 mm 판금 규칙을 적용한 베이스 플랜지, 에지 플랜지, 헴",
"sample.bracket.name": "판금 브래킷",
"sample.enclosure.about": "리프, 나사 보스, 레스트, 스냅핏이 있는 쉘 처리된 박스",
"sample.enclosure.name": "플라스틱 인클로저",
"sample.flange.about": "매개변수로 제어되는 원형 패턴이 있는 허브와 플랜지",
"sample.flange.name": "볼트 플랜지",
"sample.hinge.about": "두 구성 요소와 회전 관절, 60°까지 구동",
"sample.hinge.name": "힌지 어셈블리",
"sample.plate.about": "필렛, 구멍, 보어 가공된 보스가 있는 매개변수 기반 플레이트",
"sample.plate.name": "샘플 플레이트",
"sample.vents.about": "직사각형 패턴의 슬롯: 행, 열, 피치가 매개변수",
"sample.vents.name": "통풍 플레이트",
"settings.msaa_applies_on_restart": "안티앨리어싱은 다음에 SolidCraft를 시작할 때 적용됩니다",
"settings.msaa_reset": "{setting}: {requested}×는 이 그래픽 장치에서 지원되지 않아 {reset}×로 변경했습니다(지원: {supported}).",
"settings.msaa_unsupported": "이 그래픽 장치에서 지원되지 않음",
+25
View File
@@ -291,6 +291,31 @@
"dialog.selection_guidance": "Щёлкните поле ввода, чтобы изменить его. Щелчок по выделенному объекту убирает его; Ctrl/Cmd+щелчок тоже переключает. Проверьте предпросмотр и нажмите OK.",
"document.save_changes": "Сохранить изменения в {name}?",
"document.unsaved_changes": "В {name} есть несохранённые изменения. Если их не сохранить, они будут потеряны.",
"home.documentation": "Документация",
"home.in_this_browser": "в этом браузере",
"home.missing": "отсутствует",
"home.open": "Открыть…",
"home.opened_days_ago": "Открыт {count} дн. назад",
"home.opened_hours_ago": "Открыт {count} ч назад",
"home.opened_just_now": "Открыт только что",
"home.opened_minutes_ago": "Открыт {count} мин назад",
"home.opening": "открывается…",
"home.recent": "Недавние",
"home.report_issue": "Сообщить о проблеме",
"home.samples": "Примеры",
"home.whats_new": "Что нового",
"sample.bracket.about": "Базовый фланец с краевыми фланцами и кромкой по правилу листового металла 2 мм",
"sample.bracket.name": "Кронштейн из листового металла",
"sample.enclosure.about": "Тонкостенный бокс с выступом, бобышками под винты, опорой и защёлкой",
"sample.enclosure.name": "Пластиковый корпус",
"sample.flange.about": "Ступица и фланец с круговым массивом, управляемым параметром",
"sample.flange.name": "Болтовой фланец",
"sample.hinge.about": "Два компонента и вращательное сопряжение, повёрнутое на 60°",
"sample.hinge.name": "Сборка шарнира",
"sample.plate.about": "Параметрическая пластина со скруглениями, отверстиями и расточенной бобышкой",
"sample.plate.name": "Образец пластины",
"sample.vents.about": "Пазы в прямоугольном массиве: число рядов, столбцов и шаг заданы параметрами",
"sample.vents.name": "Вентиляционная пластина",
"settings.msaa_applies_on_restart": "Сглаживание вступит в силу при следующем запуске SolidCraft",
"settings.msaa_reset": "{setting}: {requested}× не поддерживается этим графическим устройством; теперь {reset}× (поддерживается: {supported}).",
"settings.msaa_unsupported": "Не поддерживается этим графическим устройством",
+25
View File
@@ -306,6 +306,31 @@
"dialog.selection_guidance": "选择输入项进行修改。点击已选对象可取消选择;按住 Ctrl/Cmd 点击也可切换选择。检查预览后,再按“确定”。",
"document.save_changes": "保存对 {name} 的更改?",
"document.unsaved_changes": "{name} 有未保存的更改。如果不保存,将丢失这些更改。",
"home.documentation": "文档",
"home.in_this_browser": "在此浏览器中",
"home.missing": "文件不存在",
"home.open": "打开…",
"home.opened_days_ago": "{count} 天前打开",
"home.opened_hours_ago": "{count} 小时前打开",
"home.opened_just_now": "刚刚打开",
"home.opened_minutes_ago": "{count} 分钟前打开",
"home.opening": "正在打开…",
"home.recent": "最近使用",
"home.report_issue": "报告问题",
"home.samples": "示例",
"home.whats_new": "新功能",
"sample.bracket.about": "基于 2 mm 钣金规则的基础法兰、边线法兰和卷边",
"sample.bracket.name": "钣金支架",
"sample.enclosure.about": "带凸缘、螺钉凸台、垫块和卡扣的抽壳长方体",
"sample.enclosure.name": "塑料外壳",
"sample.flange.about": "带有由参数驱动的圆形阵列的轮毂和法兰",
"sample.flange.name": "螺栓法兰",
"sample.hinge.about": "两个组件和一个旋转接头,驱动到 60°",
"sample.hinge.name": "铰链装配体",
"sample.plate.about": "带圆角、孔和镗孔凸台的参数化板",
"sample.plate.name": "示例板",
"sample.vents.about": "按矩形阵列排列的槽:行数、列数和间距均为参数",
"sample.vents.name": "通风板",
"settings.msaa_applies_on_restart": "抗锯齿将在下次启动 SolidCraft 时生效",
"settings.msaa_reset": "{setting}: {requested}× 不受此图形设备支持;已重置为 {reset}× (支持: {supported})。",
"settings.msaa_unsupported": "此图形设备不支持",
+29 -27
View File
@@ -362,7 +362,12 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
ui.add_space(16.0);
ui.horizontal(|ui| {
let big = |ui: &mut egui::Ui, icon: &str, label: &str, primary: bool| -> bool {
let (r, resp) = ui.allocate_exact_size(vec2(170.0, 64.0), Sense::click());
// The label sets the width: a translation longer than the English one widens
// the button instead of running past its edge.
let ink_text = if primary { Color32::WHITE } else { t.text };
let galley = ui.painter().layout_no_wrap(label.to_owned(), egui::FontId::proportional(15.0), ink_text);
let width = (56.0 + galley.size().x + 20.0).max(170.0);
let (r, resp) = ui.allocate_exact_size(vec2(width, 64.0), Sense::click());
let fill = if primary {
t.accent
} else if resp.hovered() {
@@ -380,20 +385,14 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
t.icon_fill,
ink,
);
ui.painter().text(
r.left_center() + vec2(56.0, 0.0),
egui::Align2::LEFT_CENTER,
label,
egui::FontId::proportional(15.0),
if primary { Color32::WHITE } else { t.text },
);
ui.painter().galley(r.left_center() + vec2(56.0, -galley.size().y / 2.0), galley, ink_text);
resp.clicked()
};
if big(ui, "new", "New Design", true) {
if big(ui, "new", app.tr("ui.new_design"), true) {
action = Some(Box::new(|app: &mut SolveApp| crate::documents::new_design(app)));
}
ui.add_space(10.0);
if big(ui, "open", "Open…", false) {
if big(ui, "open", app.tr("home.open"), false) {
action = Some(Box::new(|app: &mut SolveApp| {
app.pick_open(|app, path| {
if let Some(path) = path {
@@ -406,19 +405,19 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
});
ui.add_space(24.0);
ui.horizontal_wrapped(|ui| {
ui.hyperlink_to("Documentation", crate::help::DOCS_URL);
ui.hyperlink_to("Report an Issue", "https://github.com/bherbruck/solidcraft/issues/new");
ui.hyperlink_to("What's new", "https://github.com/bherbruck/solidcraft/releases/latest");
if ui.link("Keyboard Shortcuts").clicked() {
ui.hyperlink_to(app.tr("home.documentation"), crate::help::DOCS_URL);
ui.hyperlink_to(app.tr("home.report_issue"), "https://github.com/bherbruck/solidcraft/issues/new");
ui.hyperlink_to(app.tr("home.whats_new"), "https://github.com/bherbruck/solidcraft/releases/latest");
if ui.link(app.tr("ui.keyboard_shortcuts")).clicked() {
app.keymap.open = true;
app.keymap.read_only = true;
}
if ui.link("Preferences").clicked() {
if ui.link(app.tr("ui.preferences")).clicked() {
crate::prefs::open_at(app, 0);
}
});
ui.add_space(16.0);
section(ui, "Recent");
section(ui, app.tr("home.recent"));
if recent.is_empty() {
ui.label(RichText::new(crate::i18n::tr_label("Designs you open or save appear here.")).color(t.text_dim));
} else {
@@ -428,26 +427,27 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
let name = path.file_stem().map(|x| x.to_string_lossy().to_string()).unwrap_or_else(|| p.clone());
let missing = !solidcraft_engine::io::vfs::exists(p);
let mut sub = if missing {
"missing".to_string()
app.tr("home.missing").to_string()
} else if p.starts_with("/opfs/") {
// The web build keeps designs in the browser's own storage.
"in this browser".to_string()
app.tr("home.in_this_browser").to_string()
} else {
p.clone()
};
if let Some(time) = app.home.opened_at.get(p) {
let now = web_time::SystemTime::now().duration_since(web_time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(*time);
let seconds = now.saturating_sub(*time);
let ago = if seconds < 60 {
"just now".to_string()
let opened = if seconds < 60 {
app.tr("home.opened_just_now").to_string()
} else if seconds < 3600 {
format!("{} min ago", seconds / 60)
crate::i18n::format("home.opened_minutes_ago", &[("count", &(seconds / 60).to_string())])
} else if seconds < 86400 {
format!("{} h ago", seconds / 3600)
crate::i18n::format("home.opened_hours_ago", &[("count", &(seconds / 3600).to_string())])
} else {
format!("{} days ago", seconds / 86400)
crate::i18n::format("home.opened_days_ago", &[("count", &(seconds / 86400).to_string())])
};
sub.push_str(&format!("\nOpened {ago}"));
sub.push('\n');
sub.push_str(&opened);
}
let response = card(ui, Pic::Ready(app.home.thumbs.get(p.as_str()).and_then(Option::as_ref)), &name, &sub, missing)
.on_hover_text(p.as_str());
@@ -472,17 +472,19 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
});
}
ui.add_space(24.0);
section(ui, "Samples");
section(ui, app.tr("home.samples"));
grid(ui, samples.len(), |ui, i| {
let Some(s) = samples.get(i) else { return };
let key = format!("sample:{i}");
let busy = app.home.wanted == Some(i);
let sub = if busy { "opening…" } else { s.about };
// The engine's sample table holds English text; the opened design keeps it as its name.
let about = app.tr_label(s.about);
let sub = if busy { app.tr("home.opening") } else { about };
let tex = match app.home.thumbs.get(&key) {
Some(t) => Pic::Ready(t.as_ref()),
None => Pic::Icon("box"),
};
if card(ui, tex, s.name, sub, false).on_hover_text(s.about).clicked() {
if card(ui, tex, app.tr_label(s.name), sub, false).on_hover_text(about).clicked() {
action = Some(Box::new(move |app: &mut SolveApp| open_sample(app, i)));
}
});
+12
View File
@@ -267,6 +267,17 @@ mod tests {
);
}
/// The start page shows each built-in sample's name and description through `label()`, so a
/// sample added to the engine without English catalog entries would stay English everywhere.
#[test]
fn every_sample_name_and_description_is_in_the_english_catalog() {
let texts = english_texts();
let samples = solidcraft_engine::sample::samples();
let missing: Vec<&str> = samples.iter().flat_map(|s| [s.name, s.about]).filter(|text| !texts.contains(text)).collect();
assert!(missing.is_empty(), "sample texts absent from en.json: {missing:?}");
assert_eq!(Locale::for_language("ja").label("Sample Plate"), "サンプル プレート");
}
/// The whole engine registry now reaches the catalogs. Sample command labels from areas that
/// had no entry at all before (sheet metal, inspect, joints, motion) must resolve to their
/// translation rather than echo the English source text — `label()` returns its argument when
@@ -317,6 +328,7 @@ mod tests {
include_str!("browser.rs"),
include_str!("sketch_palette.rs"),
include_str!("design_properties.rs"),
include_str!("home.rs"),
] {
for call in [".tr(\"", "i18n::tr(\"", "i18n::format(\""] {
for tail in source.split(call).skip(1) {
+93
View File
@@ -84,6 +84,99 @@ fn window_chrome_renders_in_the_active_language() {
}
}
/// The start page's two big buttons had a fixed width, so a translation longer than the English
/// label ran past the edge ("Nouvelle conception"). Every registered language's label must stay
/// inside its button.
#[test]
fn home_page_buttons_fit_their_translated_labels() {
fn collect(shape: &egui::Shape, rects: &mut Vec<egui::Rect>, texts: &mut Vec<(String, egui::Rect)>) {
match shape {
egui::Shape::Rect(r) => rects.push(r.rect),
egui::Shape::Text(t) => texts.push((t.galley.job.text.clone(), egui::Rect::from_min_size(t.pos, t.galley.size()))),
egui::Shape::Vec(shapes) => shapes.iter().for_each(|s| collect(s, rects, texts)),
_ => {}
}
}
for catalog in crate::i18n::LOCALES {
let mut h = Harness::new();
h.app.preferences.language = catalog.id.into();
let rev = h.app.session.revision;
h.app.home.show(rev);
h.frames(2);
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1600.0, 1000.0));
let mut out = h.ctx.run_ui(egui::RawInput { screen_rect: Some(screen), ..Default::default() }, |ui| h.app.ui(ui));
out.textures_delta.clear();
let (mut rects, mut texts) = (Vec::new(), Vec::new());
out.shapes.iter().for_each(|shape| collect(&shape.shape, &mut rects, &mut texts));
let locale = crate::i18n::Locale::for_language(catalog.id);
for id in ["ui.new_design", "home.open"] {
let label = locale.text(id);
let (_, text) = texts.iter().find(|(t, _)| t == label).unwrap_or_else(|| panic!("{}: {label:?} is not painted", catalog.id));
let button = rects
.iter()
.filter(|r| (r.height() - 64.0).abs() < 1.0 && r.contains(text.left_center()))
.min_by(|a, b| a.area().total_cmp(&b.area()))
.unwrap_or_else(|| panic!("{}: no button around {label:?}", catalog.id));
assert!(text.right() <= button.right(), "{}: {label:?} ends at {} past its button's edge {}", catalog.id, text.right(), button.right());
}
}
}
/// The start page is the first thing a user sees. Its buttons, links, headings and cards were
/// English literals, mostly painted rather than laid out as widgets, so check the text actually
/// painted in each complete language: a broken message id falls back to English silently.
#[test]
fn home_page_renders_in_the_active_language() {
fn painted(shape: &egui::Shape, out: &mut Vec<String>) {
match shape {
egui::Shape::Text(text) => out.push(text.galley.job.text.clone()),
egui::Shape::Vec(shapes) => shapes.iter().for_each(|s| painted(s, out)),
_ => {}
}
}
let cases = [
(
"ja",
[
"新規デザイン",
"開く…",
"ドキュメント",
"問題を報告",
"新機能",
"キーボードショートカット",
"環境設定",
"最近使用した項目",
"サンプル",
"サンプル プレート",
],
"見つかりません\n2 時間前に開きました",
),
("zh", ["新建设计", "打开…", "文档", "报告问题", "新功能", "键盘快捷键", "首选项", "最近使用", "示例", "示例板"], "文件不存在\n2 小时前打开"),
];
for (lang, words, missing_recent) in cases {
let mut h = Harness::new();
h.app.preferences.language = lang.into();
let gone = h.dir.join("gone.solidcraft").to_string_lossy().into_owned();
h.app.home.add_recent(&gone);
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
h.app.home.opened_at.insert(gone, now - 2 * 3600);
let rev = h.app.session.revision;
h.app.home.show(rev);
h.frames(2);
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1600.0, 1000.0));
let mut out = h.ctx.run_ui(egui::RawInput { screen_rect: Some(screen), ..Default::default() }, |ui| h.app.ui(ui));
out.textures_delta.clear();
let mut texts = Vec::new();
out.shapes.iter().for_each(|shape| painted(&shape.shape, &mut texts));
for word in words.into_iter().chain([missing_recent]) {
assert!(texts.iter().any(|t| t == word), "{lang}: {word:?} must be painted, got {texts:?}");
}
for english in ["New Design", "Open…", "Documentation", "Report an Issue", "What's new", "Recent", "Samples", "Sample Plate"] {
assert!(!texts.iter().any(|t| t == english), "{lang}: {english:?} is still painted in English");
}
}
}
/// The command palette's hint embeds a JSON example, so its braces must not be mistaken for a
/// placeholder name by the catalog's parser, and the example must survive interpolation whole.
#[test]