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:
1 parent
b56648c055
commit
7934edcad4
10 files changed
+309
-27
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "このグラフィックスデバイスではサポートされていません",
|
||||
|
||||
@@ -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": "이 그래픽 장치에서 지원되지 않음",
|
||||
|
||||
@@ -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": "Не поддерживается этим графическим устройством",
|
||||
|
||||
@@ -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
@@ -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)));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in new issue
Block a user