diff --git a/resources/web/dialog/SpeedDial/speeddial.js b/resources/web/dialog/SpeedDial/speeddial.js index bb5c8a167b..4090f26dc0 100644 --- a/resources/web/dialog/SpeedDial/speeddial.js +++ b/resources/web/dialog/SpeedDial/speeddial.js @@ -9,6 +9,8 @@ // - action.input "percent"/"tab" -> NativeCommands catalog (phases handled in activateEntry) // - action.icon SVG base name -> AppAction::icon / resources/images/.svg // - action.desc/wiki -> AppAction::tooltip / help_url (footer detail strip) +// - action.*_en / tab.title_en -> ActionRegistry action_json / tab_options (English search aliases, +// sent only when they differ from the displayed text) // - payload.tooltip_expanded -> ActionRegistry::tooltip_expanded (persisted footer state) // - action list is frecency-sorted -> ActionRegistry::snapshot() @@ -97,45 +99,49 @@ var ghostEl = null, ghostTypedEl = null, ghostSuffixEl = null; var activeCompletion = null; // ---- pure helpers (no DOM; unit-tested) ------------------------------------- -// Pre-normalized haystacks, cached on the action object. The fold is length-preserving (1:1 per -// char) so the ranges FuzzyRangesNorm returns slice the ORIGINAL title/group/source text correctly. -// The action objects arrive from C++ and are stable for the dialog's lifetime, so we compute these once. -function titleNorm(a) { - if (a._tn === undefined) - a._tn = NormText(a.title, false); - return a._tn; -} -// The eyebrow (header) line shows group when present, else source. Settings keep an empty group so -// their source path is the eyebrow; commands / plates / recents carry a non-empty group. Splitting the -// two lets a match range stay aligned to whichever string the eyebrow actually renders. -function groupNorm(a) { - if (a._gn === undefined) - a._gn = NormText(a.group || "", false); - return a._gn; -} -function sourceNorm(a) { - if (a._sn === undefined) - a._sn = NormText(a.source || "", false); - return a._sn; -} -function pluginCategoryNorm(a) { - if (a._pcn === undefined) - a._pcn = a.kind === "plugin" ? NormText(T("sd_plugins", "Plugins"), false) : ""; - return a._pcn; -} -// Plugin type is searchable metadata, so typing "plugin" can find runnable plugin actions even -// when neither their capability nor plugin name contains that word. Keep both category forms. -function pluginTypeNorm(a) { - if (a._pn === undefined) - a._pn = a.kind === "plugin" ? NormText("plugin plugins", false) : ""; - return a._pn; -} -// Search-only alias for the descriptive name when the title differs (e.g. "Reverse on even" vs -// "Overhang reversal"). Never rendered, so no highlight ranges. -function fullNorm(a) { - if (a._fn === undefined) - a._fn = NormText(a.full_label || "", false); - return a._fn; +// Every searchable field of an action: its text, its ranking weight (title > group/category > +// source/aliases) and the matchIndex key its highlight ranges go to (null for a search-only field). +// - group vs source: the eyebrow (header) line shows group when present, else source. Settings keep +// an empty group so their source path is the eyebrow; commands / plates / recents carry a non-empty +// group. Splitting the two lets a match range stay aligned to whichever string the eyebrow renders. +// - full_label: search-only alias for the descriptive name when the title differs (e.g. "Reverse on +// even" vs "Overhang reversal"). Never rendered, so no highlight ranges. +// - plugin category / type: the visible "Plugins" category, plus the plugin kind as searchable +// metadata, so typing "plugin" finds runnable plugin actions even when neither their capability +// nor plugin name contains that word. Keep both category forms. +// - *_en: English counterparts of the translated fields, sent by C++ only when they differ from the +// displayed text (a non-English UI), so the page can still be searched in English. Each carries the +// weight of the localized field it mirrors, so a match ranks the same in either language. Search-only, +// except title_en, whose ranges drive the row's English hint. +var SEARCH_FIELDS = [ + { text: function (a) { return a.title; }, weight: SCORE_TITLE, range: "title" }, + { text: function (a) { return a.group; }, weight: SCORE_GROUP, range: "group" }, + { text: function (a) { return a.source; }, weight: 0, range: "source" }, + { text: function (a) { return a.full_label; }, weight: 0, range: null }, + { text: function (a) { return a.kind === "plugin" ? T("sd_plugins", "Plugins") : ""; }, weight: SCORE_GROUP, range: "plugin" }, + { text: function (a) { return a.kind === "plugin" ? "plugin plugins" : ""; }, weight: SCORE_GROUP, range: null }, + { text: function (a) { return a.title_en; }, weight: SCORE_TITLE, range: "title_en" }, + { text: function (a) { return a.group_en; }, weight: SCORE_GROUP, range: null }, + { text: function (a) { return a.source_en; }, weight: 0, range: null }, + { text: function (a) { return a.full_label_en; }, weight: 0, range: null } +]; +var MATCH_RANGE_KEYS = SEARCH_FIELDS.map(function (f) { return f.range; }).filter(Boolean); + +// The action's non-empty SEARCH_FIELDS as pre-normalized haystacks ({norm, weight, range}), cached on +// the action object. The action objects arrive from C++ and are stable for the dialog's lifetime, so +// this runs once per action; skipping empty fields keeps the per-keystroke scan to the text an action +// actually has (an English UI pays nothing for the aliases it never receives). The fold is +// length-preserving (1:1 per char) so the ranges FuzzyRangesNorm returns slice the ORIGINAL text. +function searchFields(a) { + if (a._sf === undefined) { + a._sf = []; + for (var i = 0; i < SEARCH_FIELDS.length; i++) { + var norm = NormText(SEARCH_FIELDS[i].text(a) || "", false); + if (norm) + a._sf.push({ norm: norm, weight: SEARCH_FIELDS[i].weight, range: SEARCH_FIELDS[i].range }); + } + } + return a._sf; } // Match one pre-normalized field vs one pre-normalized needle. Returns {score, ranges, contiguous} @@ -175,26 +181,22 @@ function tokenWordRe(token) { return new RegExp("\\b" + EscapeRegExp(token) + "\\b"); } -// One token's best match across an action's searchable fields, keeping per-field ranges for -// highlighting. Returns {score, title, group, source}, or null if no field matches. -function tokenMatch(a, token, wwRe) { - var t = fieldMatchScore(titleNorm(a), token, wwRe); - var g = fieldMatchScore(groupNorm(a), token, wwRe); - var s = fieldMatchScore(sourceNorm(a), token, wwRe); - var f = fieldMatchScore(fullNorm(a), token, wwRe); - var p = fieldMatchScore(pluginCategoryNorm(a), token, wwRe); - var typeMatch = fieldMatchScore(pluginTypeNorm(a), token, wwRe); - if (!t && !g && !s && !f && !p && !typeMatch) return null; - var score = Math.max( - t ? (t.contiguous ? SCORE_CONTIGUOUS : 0) + SCORE_TITLE + t.score : -Infinity, - g ? (g.contiguous ? SCORE_CONTIGUOUS : 0) + SCORE_GROUP + g.score : -Infinity, - s ? (s.contiguous ? SCORE_CONTIGUOUS : 0) + s.score : -Infinity, - f ? (f.contiguous ? SCORE_CONTIGUOUS : 0) + f.score : -Infinity, - p ? (p.contiguous ? SCORE_CONTIGUOUS : 0) + SCORE_GROUP + p.score : -Infinity, - typeMatch ? (typeMatch.contiguous ? SCORE_CONTIGUOUS : 0) + SCORE_GROUP + typeMatch.score : -Infinity - ); - return { score: score, title: t ? t.ranges : null, group: g ? g.ranges : null, source: s ? s.ranges : null, - plugin: p ? p.ranges : null }; +// One needle (the whole query, or one token of it) against every SEARCH_FIELDS field of an action. +// Returns {score, ranges}: the best weighted field score (contiguous > fuzzy, then field weight, then +// start/gaps) and the ranges of each matching highlighted field keyed by its range name. Null when no +// field matches. +function matchFields(a, needle, wwRe) { + var fields = searchFields(a); + var best = null, ranges = null; // allocated on the first match: most actions match nothing + for (var i = 0; i < fields.length; i++) { + var field = fields[i]; + var m = fieldMatchScore(field.norm, needle, wwRe); + if (!m) continue; + var v = (m.contiguous ? SCORE_CONTIGUOUS : 0) + field.weight + m.score; + if (best === null || v > best) best = v; + if (field.range) (ranges || (ranges = {}))[field.range] = m.ranges; + } + return best === null ? null : { score: best, ranges: ranges || {} }; } // Merge per-field token ranges into sorted, coalesced ranges for highlighting. Overlapping or adjacent @@ -217,24 +219,6 @@ function mergeRanges(ranges) { return out; } -// Combine per-field scores into one value, or null when nothing matched. -// Ranking: contiguous > fuzzy, then title > group/category > source/full alias, then start/gaps. -function scoreFields(t, g, s, f, p, typeMatch) { - var best = null; - function consider(m, weight) { - if (!m) return; - var v = (m.contiguous ? SCORE_CONTIGUOUS : 0) + weight + m.score; - best = best === null ? v : Math.max(best, v); - } - consider(t, SCORE_TITLE); - consider(g, SCORE_GROUP); - consider(s, 0); - consider(f, 0); - consider(p, SCORE_GROUP); - consider(typeMatch, SCORE_GROUP); - return best; -} - // The unified main-phase search: every action (command/plugin/setting) matching the query, ranked // by relevance (not by action type). Sets matchIndex so rows highlight their match ranges. The query // is normalized ONCE per pass - FuzzyRangesNorm then runs against each action's pre-normalized @@ -245,8 +229,8 @@ function scoreFields(t, g, s, f, p, typeMatch) { // - tokens: every whitespace-separated word must match SOME field, but different words may match // different fields. This is what lets "speed acceleration inner" find "Inner wall" whose path is // "Process : Speed : Acceleration" (title + source breadcrumb together). -// full_label and canonical plugin kind are searchable aliases. The visible category is also searchable -// so its own match can be highlighted in plugin rows. +// full_label, canonical plugin kind and the English counterparts (*_en) are searchable aliases. The +// visible category is also searchable so its own match can be highlighted in plugin rows. // A phrase match always outranks a distributed token match. function searchActions(actions, query) { var q = (query || "").trim(); @@ -262,50 +246,43 @@ function searchActions(actions, query) { // Compiled once per pass, reused over every field: non-global so no exec()/lastIndex state leaks // between fields, and EscapeRegExp keeps regex metachars in the query literal. var wwRe = new RegExp("\\b" + EscapeRegExp(searchNeedle) + "\\b"); + // A one-word query's only token is the needle itself, so its token pass would repeat the failed + // phrase match exactly; such an action is dropped without it. + var tokenPass = searchTokens.length !== 1 || searchTokens[0] !== searchNeedle; var scored = []; var seen = {}; for (var i = 0; i < list.length; i++) { var a = list[i]; - var t = fieldMatchScore(titleNorm(a), searchNeedle, wwRe); - var g = fieldMatchScore(groupNorm(a), searchNeedle, wwRe); - var s = fieldMatchScore(sourceNorm(a), searchNeedle, wwRe); - var f = fieldMatchScore(fullNorm(a), searchNeedle, wwRe); - var p = fieldMatchScore(pluginCategoryNorm(a), searchNeedle, wwRe); - var typeMatch = fieldMatchScore(pluginTypeNorm(a), searchNeedle, wwRe); - var phrase = scoreFields(t, g, s, f, p, typeMatch); - var score, ranges; - if (phrase !== null) { - score = phrase + SCORE_PHRASE; - ranges = { title: t ? t.ranges : null, group: g ? g.ranges : null, source: s ? s.ranges : null, - plugin: p ? p.ranges : null }; + var phrase = matchFields(a, searchNeedle, wwRe); + var score, perField; + if (phrase) { + score = phrase.score + SCORE_PHRASE; } else { + if (!tokenPass) continue; // Require every token; a token that matches nothing drops the action immediately. Ranges // from all matching tokens are merged per field so each matched word highlights. - var sum = 0, titleR = null, groupR = null, sourceR = null, pluginR = null, all = true; + var sum = 0, all = true; + perField = {}; for (var tokenIndex = 0; tokenIndex < searchTokens.length; tokenIndex++) { - var m = tokenMatch(a, searchTokens[tokenIndex], searchTokenRes[tokenIndex]); + var m = matchFields(a, searchTokens[tokenIndex], searchTokenRes[tokenIndex]); if (!m) { all = false; break; } sum += m.score; - if (m.title) (titleR || (titleR = [])).push(m.title); - if (m.group) (groupR || (groupR = [])).push(m.group); - if (m.source) (sourceR || (sourceR = [])).push(m.source); - if (m.plugin) (pluginR || (pluginR = [])).push(m.plugin); + for (var key in m.ranges) + (perField[key] || (perField[key] = [])).push(m.ranges[key]); } if (!all) continue; score = sum; - ranges = { title: mergeRanges(titleR), group: mergeRanges(groupR), source: mergeRanges(sourceR), - plugin: mergeRanges(pluginR) }; } - // Ranges are per-field against the ACTUAL text drawn: title for the row-name, and group (or - // source when group is empty) for the eyebrow - so highlight offsets stay aligned to the label. - matchIndex[a.id] = { - title: ranges.title, - group: ranges.group, - source: ranges.source, - plugin: ranges.plugin, - useEyebrowGroup: !!(a.group) - }; + // Ranges are per-field against the ACTUAL text drawn: title for the row-name, group (or source + // when group is empty) for the eyebrow, and title_en for the English hint - so highlight offsets + // stay aligned to the label. + var entry = { useEyebrowGroup: !!(a.group) }; + for (var k = 0; k < MATCH_RANGE_KEYS.length; k++) { + var rangeKey = MATCH_RANGE_KEYS[k]; + entry[rangeKey] = phrase ? phrase.ranges[rangeKey] || null : mergeRanges(perField[rangeKey]); + } + matchIndex[a.id] = entry; scored.push({ a: a, s: score }); seen[a.id] = true; } @@ -350,7 +327,8 @@ function completionFor(query, list) { var top = (list || []).slice(0, 10); for (var i = 0; i < top.length; i++) { var a = top[i]; - var fields = [a.title, a.group, a.source, a.full_label]; + // English aliases after the displayed text, so English typing completes in a localized UI. + var fields = [a.title, a.group, a.source, a.full_label, a.title_en, a.group_en, a.source_en, a.full_label_en]; for (var f = 0; f < fields.length; f++) { var words = completionWords(fields[f]); for (var w = 0; w < words.length; w++) { @@ -427,13 +405,15 @@ function tabTitle(t) { return title || prettySource((t && t.id) || ""); } -// Filter the tab list by a fuzzy title/id match. Pure so the node-vm test can exercise it. +// Filter the tab list by a fuzzy title/id/English-title match (title_en is sent only when the page +// label is translated). Pure so the node-vm test can exercise it. function filterTabs(tabs, query) { var q = (query || "").trim(); if (!q) return (tabs || []).slice(0); return (tabs || []).filter(function (t) { - return FuzzyRanges(tabTitle(t), q, false) || FuzzyRanges(t.id, q, false); + return FuzzyRanges(tabTitle(t), q, false) || FuzzyRanges(t.id, q, false) || + (t.title_en && FuzzyRanges(t.title_en, q, false)); }); } @@ -973,6 +953,10 @@ function renderActionRow(a, i) { var eyebrowMatch = mi ? (showPluginCategory ? mi.plugin : (mi.useEyebrowGroup ? mi.group : mi.source)) : null; shell.left.insertBefore(markedText("row-eyebrow", eyebrow, eyebrowMatch), shell.line); shell.line.appendChild(markedText("row-name", a.title, mi ? mi.title : null)); + // A row found only through its English title shows that title, highlighted, so the match is + // explained even when nothing in the translated name resembles the query. + if (mi && !mi.title && mi.title_en) + shell.line.appendChild(markedText("row-alias", a.title_en, mi.title_en)); var badge = modeBadge(a, USER_MODE); if (badge) { var tag = document.createElement("span"); diff --git a/resources/web/dialog/SpeedDial/speeddial.test.js b/resources/web/dialog/SpeedDial/speeddial.test.js index 1fda833801..f614554ae7 100644 --- a/resources/web/dialog/SpeedDial/speeddial.test.js +++ b/resources/web/dialog/SpeedDial/speeddial.test.js @@ -50,6 +50,15 @@ assert.equal(ctx.filterTabs(tabOptions, "Device").length, 1, "a typed query matches a tab title"); assert.deepEqual(ctx.filterTabs(tabOptions, "zzz"), [], "a typed query with no match returns an empty list"); +// A translated tab whose id is not its English name carries title_en, so English still finds it. +const localizedTabs = [ + { id: "monitor", title: "设备", title_en: "Device" }, + { id: "prepare", title: "准备" } +]; +assert.deepEqual(ctx.filterTabs(localizedTabs, "device").map(function (t) { return t.id; }), ["monitor"], + "a localized tab matches its English title"); +assert.deepEqual(ctx.filterTabs(localizedTabs, "设备").map(function (t) { return t.id; }), ["monitor"], + "a localized tab still matches its translated title"); // tabTitle: pages added with an empty title (e.g. MainFrame's Home tab) fall back to the id. assert.equal(ctx.tabTitle({ id: "home", title: "" }), "Home", @@ -192,6 +201,48 @@ assert.equal(ctx.completionFor("", compPool), null, "an empty query has no compl assert.equal(ctx.completionFor("zzz", ctx.searchActions(compPool, "zzz")), null, "a query with no match has no completion"); +// English aliases: in a non-English UI each translated field may carry its English counterpart +// (*_en), searched with the weight of the field it mirrors. An English UI sends none. +const zhPool = [ + { id: "rev", title: "反转偶数层悬垂方向", full_label: "悬垂反转", source: "工艺 : 质量 : 悬垂", + title_en: "Reverse on even", full_label_en: "Overhang reversal", source_en: "Process : Quality : Overhangs", + group: "", input: "" }, + { id: "slice", title: "切片并预览", source: "OrcaSlicer", group: "切片和导出", + title_en: "Slice and Preview", group_en: "Slice & Export", input: "" }, + { id: "brim", title: "Brim宽度", source: "工艺 : 其他 : Brim", source_en: "Process : Others : Brim", + group: "", input: "" } +]; +assert.deepEqual(ctx.searchActions(zhPool, "reverse on even").map(function (a) { return a.id; }), ["rev"], + "an English query finds a translated setting through its English title"); +assert.equal(ctx.matchIndex.rev.title, null, "an English-only match does not highlight the translated title"); +assert.deepEqual(ctx.matchIndex.rev.title_en, [[0, 15]], "the English title keeps its ranges for the hint"); +assert.deepEqual(ctx.searchActions(zhPool, "偶数层").map(function (a) { return a.id; }), ["rev"], + "the translated title is still searchable"); +assert.equal(ctx.matchIndex.rev.title_en, null, "a translated-title match leaves no English hint ranges"); +assert.deepEqual(ctx.searchActions(zhPool, "reverse 悬垂").map(function (a) { return a.id; }), ["rev"], + "a mixed query may match one token in English and another in the translation"); +assert.deepEqual(ctx.searchActions(zhPool, "overhang reversal").map(function (a) { return a.id; }), ["rev"], + "the English full_label alias is searchable"); +assert.deepEqual(ctx.searchActions(zhPool, "export").map(function (a) { return a.id; }), ["slice"], + "the English group alias is searchable"); +assert.deepEqual(ctx.searchActions(zhPool, "brim").map(function (a) { return a.id; }), ["brim"], + "a translation that keeps the English word matches the displayed title"); +assert.deepEqual(ctx.matchIndex.brim.title, [[0, 4]], "that match highlights the displayed title, so no hint is drawn"); +// Weights mirror the localized fields: an English title match outranks an English breadcrumb match. +const zhRankPool = [ + { id: "src", title: "内壁", source: "工艺 : 速度", source_en: "Process : Speed", group: "", input: "" }, + { id: "ttl", title: "速度", source: "工艺 : 其他", title_en: "Speed", group: "", input: "" } +]; +assert.deepEqual(ctx.searchActions(zhRankPool, "speed").map(function (a) { return a.id; }), ["ttl", "src"], + "an English title match outranks an English breadcrumb match, as their localized fields do"); +// Absent aliases (an English UI) leave no English hint; every pool above has none and ranks as before. +ctx.searchActions(pool, "layer"); +assert.equal(ctx.matchIndex.c1.title_en, null, "an action without English aliases has no English hint ranges"); +assert.equal(ctx.completionFor("reverse o", ctx.searchActions(zhPool, "reverse o")).suffix, "n", + "completion extends an English word from title_en"); +assert.equal(ctx.completionFor("quality overh", ctx.searchActions(zhPool, "quality overh")).suffix, "angs", + "completion also reaches the English breadcrumb"); + // actionCategory: a command/dynamic action's group is its category; a setting uses the top-level // source segment; every plugin shares one header; a category-less action falls back to "Other". assert.equal(ctx.actionCategory({ id: "c", group: "Help", source: "OrcaSlicer", kind: "command" }), "Help", @@ -456,9 +507,9 @@ assert.equal(ctx.stateFromPayload({}).tooltipExpanded, true, "expansion defaults assert.equal(ctx.stateFromPayload({ tooltip_expanded: false }).tooltipExpanded, false, "a collapsed payload is honored"); assert.equal(ctx.stateFromPayload({ tooltip_expanded: true }).tooltipExpanded, true, "an expanded payload is honored"); -// resultCountText: a search counts the shown matches only ("Showing N actions"); the total is used +// resultCountText: a search counts the shown matches only ("Matching actions: N"); the total is used // solely for the empty-query count. -assert.equal(ctx.resultCountText(100, 3, "lay"), "Showing 3 actions", "a search reports the shown match count only"); -assert.equal(ctx.resultCountText(100, 100, ""), "100 actions", "an empty query reports the total"); +assert.equal(ctx.resultCountText(100, 3, "lay"), "Matching actions: 3", "a search reports the shown match count only"); +assert.equal(ctx.resultCountText(100, 100, ""), "Actions: 100", "an empty query reports the total"); console.log("ok"); diff --git a/resources/web/dialog/SpeedDial/style.css b/resources/web/dialog/SpeedDial/style.css index 3aaeddb460..06c7d96632 100644 --- a/resources/web/dialog/SpeedDial/style.css +++ b/resources/web/dialog/SpeedDial/style.css @@ -414,8 +414,20 @@ body { white-space: nowrap; } +/* English title of a row found only through it (non-English UI); gives way to the title first. */ +.row-alias { + flex: 0 100 auto; + min-width: 0; + font-size: 11px; + color: var(--muted, var(--orca-muted, #6b7280)); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .row-name mark, -.row-eyebrow mark { +.row-eyebrow mark, +.row-alias mark { background: var(--plugin-status-warn-bg); color: var(--plugin-status-warn); border-radius: 2px; diff --git a/src/slic3r/GUI/ActionRegistry.cpp b/src/slic3r/GUI/ActionRegistry.cpp index 5b1e6f5957..6956423bc6 100644 --- a/src/slic3r/GUI/ActionRegistry.cpp +++ b/src/slic3r/GUI/ActionRegistry.cpp @@ -206,19 +206,52 @@ constexpr const char* kPlateGotoPrefix = "orca_plate_goto"; constexpr const char* kRecentProjectPrefix = "orca_recent_project"; // Display context for a setting action's eyebrow, e.g. the "Process" in "Process : Quality : Layers". -// Keyed by the option's preset type so the palette reads like the settings sidebar tabs. -std::string setting_type_context(Preset::Type type) +// Keyed by the option's preset type so the palette reads like the settings sidebar tabs. Untranslated: +// the caller translates it for the displayed breadcrumb and keeps it for the English one. +const char* setting_type_context(Preset::Type type) { switch (type) { case Preset::TYPE_FILAMENT: - case Preset::TYPE_SLA_MATERIAL: return _u8L("Filament"); - case Preset::TYPE_PRINTER: return _u8L("Printer"); + case Preset::TYPE_SLA_MATERIAL: return L("Filament"); + case Preset::TYPE_PRINTER: return L("Printer"); case Preset::TYPE_PRINT: case Preset::TYPE_SLA_PRINT: - default: return _u8L("Process"); + default: return L("Process"); } } +// A setting's text in one language. The displayed text and its English search aliases both come from +// here, so in an English UI the two are equal and no alias is sent. +struct SettingText +{ + std::string title; // the label the settings row draws + std::string full_label; // the descriptive ConfigOptionDef name when it differs from title: a + // search-only alias ("overhang reversal" still finds "Reverse on even") + std::string source; // the full settings path "Process : Quality : Layers". The JS renders + // group || source and searches every field, so putting the whole path in + // source both displays it and makes it matchable by any segment. +}; + +SettingText setting_text(const std::wstring& def_label, const std::string& display_label, const wxString& row_label, bool row_multi, + const std::string& type_name, const std::wstring& category, const std::wstring& group) +{ + SettingText text; + const std::string def = boost::nowide::narrow(def_label); + text.title = into_u8(Search::resolve_setting_title(from_u8(display_label), row_label, row_multi)); + if (text.title.empty()) + text.title = def; + if (text.title != def) + text.full_label = def; + + std::wstring path = boost::nowide::widen(type_name); + if (!category.empty()) + path += L" : " + category; + if (!group.empty()) + path += L" : " + group; + text.source = boost::nowide::narrow(path); + return text; +} + // Stable, non-localized mode token for the webview, which maps it to a badge ("Developer" etc.). const char* mode_key(ConfigOptionMode mode) { @@ -305,8 +338,9 @@ struct PlateAction : AppAction PlateAction(int index, std::string title, std::string source_name) : AppAction(AppActionId{id_for(index)}, std::move(title), kOrcaSourceKey, std::move(source_name)), plate_index(index) { - this->kind = AppActionKind::Command; - this->group = _u8L("Plate"); + this->kind = AppActionKind::Command; + this->group_en = L("Plate"); + this->group = I18N::translate_utf8(this->group_en); } AppActionRunResult run(const std::string& /*param*/) const override @@ -328,8 +362,9 @@ struct RecentProjectAction : AppAction RecentProjectAction(std::string path, std::string title, std::string source) : AppAction(AppActionId{id_for(path)}, std::move(title), kOrcaSourceKey, std::move(source)), file_path(std::move(path)) { - this->kind = AppActionKind::Command; - this->group = _u8L("Recent Projects"); + this->kind = AppActionKind::Command; + this->group_en = L("Recent Projects"); + this->group = I18N::translate_utf8(this->group_en); } AppActionRunResult run(const std::string& /*param*/) const override @@ -342,6 +377,30 @@ struct RecentProjectAction : AppAction } // namespace +nlohmann::json action_json(const AppAction& a) +{ + nlohmann::json j({{"id", a.id()}, + {"title", a.title()}, + {"full_label", a.full_label}, + {"source", a.source_name()}, + {"group", a.group}, + {"kind", a.kind == AppActionKind::Plugin ? "plugin" : "command"}, + {"input", a.input}, + {"icon", a.icon}, + {"mode", mode_key(a.required_mode)}, + {"desc", a.tooltip}, + {"wiki", !a.help_url.empty()}}); + auto add_alias = [&j](const char* key, const std::string& english, const std::string& local) { + if (!english.empty() && english != local) + j[key] = english; + }; + add_alias("title_en", a.title_en, a.title()); + add_alias("group_en", a.group_en, a.group); + add_alias("source_en", a.source_en, a.source_name()); + add_alias("full_label_en", a.full_label_en, a.full_label); + return j; +} + ActionRegistry::~ActionRegistry() = default; void ActionRegistry::init() @@ -430,7 +489,8 @@ void ActionRegistry::relocalize_builtins() // Drop only the built-in commands; plugins and the dynamically materialised families are // either unlocalized or rebuilt per snapshot. remove() only erases the map entry, and upsert() - // re-seeds favourites/stats from config, so key-based ids keep their pinned state. + // re-seeds favourites/stats from config, so key-based ids keep their pinned state. make_action() + // translates in the new locale. std::vector stale; for (const auto& [id, action] : m_actions) if (action->source_key() == kOrcaSourceKey && action->kind == AppActionKind::Command) @@ -438,7 +498,6 @@ void ActionRegistry::relocalize_builtins() for (const std::string& id : stale) remove(id); - NativeCommands::rebuild_catalog(); for (const NativeCommand& c : NativeCommands::catalog()) upsert(NativeCommands::make_action(c)); } @@ -672,28 +731,22 @@ void ActionRegistry::materialize_setting_actions() const std::string id = SettingAction::id_for(opt.opt_key(), opt.type); seen.insert(id); - // The page draws Line::label; the descriptive ConfigOptionDef name stays a search-only alias - // ("overhang reversal" still finds "Reverse on even"). - const std::string search_label = boost::nowide::narrow(opt.label_local.empty() ? opt.label : opt.label_local); - std::string title = into_u8(Search::resolve_setting_title(from_u8(opt.display_label), row.label, row.multi)); - if (title.empty()) - title = search_label; + // The localized text the page shows, and its English search aliases from the untranslated row + // label, def name and breadcrumb. + const char* type_context = setting_type_context(opt.type); + SettingText local = setting_text(opt.label_local.empty() ? opt.label : opt.label_local, opt.display_label, row.label, row.multi, + I18N::translate_utf8(type_context), opt.category_local, opt.group_local); + SettingText english = setting_text(opt.label, opt.display_label_en, row.label_en, row.multi, type_context, + Tab::translate_category(opt.category, opt.type, false).ToStdWstring(), opt.group); - // Eyebrow/source = the full settings path "Process : Quality : Layers" (localized). The JS - // renders group || source and searches source + " " + group, so putting the whole path in - // source both displays it and makes it matchable by any segment (e.g. a "quality" query). - std::wstring path = boost::nowide::widen(setting_type_context(opt.type)); - if (!opt.category_local.empty()) - path += L" : " + opt.category_local; - if (!opt.group_local.empty()) - path += L" : " + opt.group_local; - - // title = the label the settings row draws; group stays empty so the source path (above) is - // the single display/search breadcrumb rather than being duplicated. - auto action = std::make_unique(opt.opt_key(), opt.type, title, std::string(), opt.category, - boost::nowide::narrow(path), opt.mode); - if (title != search_label) - action->full_label = search_label; + // group stays empty so the source path is the single display/search breadcrumb rather than + // being duplicated. + auto action = std::make_unique(opt.opt_key(), opt.type, local.title, std::string(), opt.category, local.source, + opt.mode); + action->full_label = std::move(local.full_label); + action->title_en = std::move(english.title); + action->full_label_en = std::move(english.full_label); + action->source_en = std::move(english.source); // Tile pictogram = the icon of the setting's own group header (e.g. Advanced -> param_advanced), // the one shown next to it in the page. Fall back to the page/category icon for groups @@ -751,13 +804,14 @@ void ActionRegistry::materialize_plate_actions() seen.insert(id); // "Go to Plate N" + " (name)" when the plate is named, matching the object-list label. - std::string title(_u8L("Go to Plate")); - title += " " + std::to_string(i + 1); - const std::string name = plate->get_plate_name(); + const char* go_to_plate = L("Go to Plate"); + std::string suffix = " " + std::to_string(i + 1); + const std::string name = plate->get_plate_name(); if (!name.empty()) - title += " (" + name + ")"; + suffix += " (" + name + ")"; - auto action = std::make_unique(int(i), title, kOrcaSourceName); + auto action = std::make_unique(int(i), I18N::translate_utf8(go_to_plate) + suffix, kOrcaSourceName); + action->title_en = go_to_plate + suffix; seed_from(stats, favs, id, *action); auto const action_id = action->id(); auto const app_action = std::shared_ptr(std::move(action)); @@ -868,23 +922,9 @@ nlohmann::json ActionRegistry::snapshot() return a->id() < b->id(); }); - auto action_to_json = [](const AppAction* a) { - return nlohmann::json({{"id", a->id()}, - {"title", a->title()}, - {"full_label", a->full_label}, - {"source", a->source_name()}, - {"group", a->group}, - {"kind", a->kind == AppActionKind::Plugin ? "plugin" : "command"}, - {"input", a->input}, - {"icon", a->icon}, - {"mode", mode_key(a->required_mode)}, - {"desc", a->tooltip}, - {"wiki", !a->help_url.empty()}}); - }; - nlohmann::json actions = nlohmann::json::array(); for (const AppAction* a : sorted) - actions.push_back(action_to_json(a)); + actions.push_back(action_json(*a)); // why: favourites is the ORDERED pin list - it must come from favourite_actions // as stored, not be re-derived from the frecency-sorted actions (that would @@ -917,7 +957,7 @@ nlohmann::json ActionRegistry::snapshot() recent.resize(recent_limit); nlohmann::json recent_json = nlohmann::json::array(); for (const AppAction* a : recent) - recent_json.push_back(action_to_json(a)); + recent_json.push_back(action_json(*a)); return {{"actions", std::move(actions)}, {"favourites", std::move(favourites)}, @@ -937,14 +977,21 @@ nlohmann::json ActionRegistry::tab_options() const MainFrame* mf = wxGetApp().mainframe; if (!mf || !mf->m_tabpanel) return out; + // English labels of the built-in pages whose id is not already their English name, so the tab + // picker can be searched in English in any UI language. + static const std::unordered_map english_labels{ + {TAB_ID_MONITOR, L("Device")}, {TAB_ID_MONITOR_WEB, L("Device (Web)")}, {TAB_ID_MULTI_DEVICE, L("Multi-device")}}; + Notebook* notebook = mf->m_tabpanel; for (size_t i = 0; i < notebook->GetPageCount(); ++i) { const wxString id = notebook->GetPageName(i); if (id.empty()) continue; - out.push_back({{"id", id.ToStdString()}, - {"title", notebook->GetPageLabel(i).ToStdString()}, - {"icon", notebook->GetPageIcon(i)}}); + const std::string title = notebook->GetPageLabel(i).ToStdString(); + nlohmann::json tab({{"id", id.ToStdString()}, {"title", title}, {"icon", notebook->GetPageIcon(i)}}); + if (auto it = english_labels.find(id.ToStdString()); it != english_labels.end() && title != it->second) + tab["title_en"] = it->second; + out.push_back(std::move(tab)); } return out; } diff --git a/src/slic3r/GUI/ActionRegistry.hpp b/src/slic3r/GUI/ActionRegistry.hpp index 4d5800907f..60bb2089ec 100644 --- a/src/slic3r/GUI/ActionRegistry.hpp +++ b/src/slic3r/GUI/ActionRegistry.hpp @@ -88,6 +88,14 @@ struct AppAction // Search-only alias when the title differs from the descriptive ConfigOptionDef name (e.g. title // "Reverse on even", full_label "Overhang reversal"). Empty when the two agree. std::string full_label; + // Search-only English counterparts of title(), group, source_name() and full_label, so a + // localized UI can still be searched in English. Each is derived exactly like the localized + // text it mirrors, minus the translation, so in an English UI it equals that text. Empty when + // there is no translated counterpart. + std::string title_en; + std::string group_en; + std::string source_en; + std::string full_label_en; // Full wiki URL, when the action has one (SettingActions whose row declared a label_path). std::string help_url; @@ -138,6 +146,11 @@ inline bool requires_mode_switch(ConfigOptionMode setting_mode, ConfigOptionMode // hand-edited config must never grow the quick-launch bar past `limit`, and a duplicated id must collapse to its first pin. std::vector cap_favourites(const std::vector& ids, size_t limit); +// One action as the Speed Dial page receives it. Each English alias (title_en, group_en, source_en, +// full_label_en) is sent only when it is non-empty and differs from its localized counterpart, so an +// English UI receives none and searches exactly the displayed text. +nlohmann::json action_json(const AppAction& a); + // Self-contained sink and single owner of runnable actions for the app session. // // Workflow: diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index cfd985b64b..da5acf8272 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -1020,7 +1020,7 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in const BrimType brim_type = config->opt_enum("brim_type"); const bool have_auto_brim_ear = brim_type == btEar && !is_belt_tilted; const bool have_painted_brim_ear = brim_type == btPainted && !is_belt_tilted; - set_option_label("brim_width", have_auto_brim_ear ? _L("Brim ear radius") : _L("Brim width")); + set_option_label("brim_width", have_auto_brim_ear ? L("Brim ear radius") : L("Brim width")); const auto brim_width = config->opt_float("brim_width"); // Automatic brim ear settings require a non-zero brim width. toggle_field("brim_ears_max_angle", brim_width > 0.0f); diff --git a/src/slic3r/GUI/ConfigManipulation.hpp b/src/slic3r/GUI/ConfigManipulation.hpp index d2316e69bb..320987621d 100644 --- a/src/slic3r/GUI/ConfigManipulation.hpp +++ b/src/slic3r/GUI/ConfigManipulation.hpp @@ -80,6 +80,8 @@ public: t_config_option_keys const &applying_keys() const; void toggle_field(const std::string& field_key, const bool toggle, int opt_index = -1); void toggle_line(const std::string& field_key, const bool toggle, int opt_index = -1); + // Renames an option's row. `label` is the untranslated English msgid (an L("...") string); the + // row translates it for display. void set_option_label(const std::string& field_key, const wxString& label, int opt_index = -1); // FFF print diff --git a/src/slic3r/GUI/NativeCommands.cpp b/src/slic3r/GUI/NativeCommands.cpp index e93a1a4635..334f2aad9b 100644 --- a/src/slic3r/GUI/NativeCommands.cpp +++ b/src/slic3r/GUI/NativeCommands.cpp @@ -142,19 +142,24 @@ struct CommandAction : AppAction AppActionRunResult run(const std::string& param) const override { return NativeCommands::run(command_key, param); } explicit CommandAction(const NativeCommand& c) - : AppAction(AppActionId{AppAction::compose_id(kCommandPrefix, c.key, kOrcaSourceKey)}, c.title, kOrcaSourceKey, kOrcaSourceName) + : AppAction(AppActionId{AppAction::compose_id(kCommandPrefix, c.key, kOrcaSourceKey)}, I18N::translate_utf8(c.title), kOrcaSourceKey, + kOrcaSourceName) , command_key(c.key) { - this->kind = AppActionKind::Command; - this->group = c.group; - this->input = c.input; - this->icon = c.icon; + this->kind = AppActionKind::Command; + this->group = I18N::translate_utf8(c.group); + this->title_en = c.title; + this->group_en = c.group; + this->input = c.input; + this->icon = c.icon; } }; std::vector build_command_catalog() { std::vector out; + // `title` and `group` are the untranslated English msgids, L("...")-marked at each call so xgettext + // extracts them. auto add = [&](std::string key, std::string title, std::string group, std::function runner, std::string input = {}, std::string icon = {}) { out.push_back({std::move(key), std::move(title), std::move(group), std::move(input), std::move(icon), std::move(runner)}); @@ -166,7 +171,7 @@ std::vector build_command_catalog() }; // ---- Slice & Export ---- - add_with_icon("slice_and_preview", _u8L("Slice and Preview"), _u8L("Slice & Export"), "media_play", [](const std::string&) { + add_with_icon("slice_and_preview", L("Slice and Preview"), L("Slice & Export"), "media_play", [](const std::string&) { Plater* plater = wxGetApp().plater(); if (plater) { plater->reslice(); @@ -178,7 +183,7 @@ std::vector build_command_catalog() }); add_with_icon( - "go_to_layer", _u8L("Go to layer (percent)"), _u8L("Commands"), "height_range_layer", + "go_to_layer", L("Go to layer (percent)"), L("Commands"), "height_range_layer", [](const std::string& param) { Plater* plater = wxGetApp().plater(); if (plater) { @@ -194,7 +199,7 @@ std::vector build_command_catalog() // "go_to_tab" is two-phase: the palette collects the tab after activating it, then hands the tab // id back as `param` (same contract as go_to_layer's percent). add( - "go_to_tab", _u8L("Go to tab..."), _u8L("Commands"), + "go_to_tab", L("Go to tab..."), L("Commands"), [](const std::string& param) { if (MainFrame* mf = wxGetApp().mainframe; mf && !param.empty()) mf->select_tab(from_u8(param)); @@ -202,41 +207,41 @@ std::vector build_command_catalog() }, "tab"); - add_with_icon("load_project", _u8L("Load Project"), _u8L("Commands"), "menu_open", [](const std::string&) { + add_with_icon("load_project", L("Load Project"), L("Commands"), "menu_open", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->load_project(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("save_project", _u8L("Save Project"), _u8L("Commands"), "menu_save", [](const std::string&) { + add_with_icon("save_project", L("Save Project"), L("Commands"), "menu_save", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->save_project(false); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("save_project_as", _u8L("Save Project As"), _u8L("Commands"), "menu_save", [](const std::string&) { + add_with_icon("save_project_as", L("Save Project As"), L("Commands"), "menu_save", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->save_project(true); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("open_preferences", _u8L("Preferences"), _u8L("Commands"), "cog", [](const std::string&) { + add_with_icon("open_preferences", L("Preferences"), L("Commands"), "cog", [](const std::string&) { wxGetApp().open_preferences(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); // ---- Mode ---- - add_with_icon("mode_simple", _u8L("Mode: Simple"), _u8L("Mode"), "advanced", [](const std::string&) { + add_with_icon("mode_simple", L("Mode: Simple"), L("Mode"), "advanced", [](const std::string&) { wxGetApp().set_mode(comSimple); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("mode_advanced", _u8L("Mode: Advanced"), _u8L("Mode"), "advanced", [](const std::string&) { + add_with_icon("mode_advanced", L("Mode: Advanced"), L("Mode"), "advanced", [](const std::string&) { wxGetApp().set_mode(comAdvanced); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("mode_expert", _u8L("Mode: Expert"), _u8L("Mode"), "advanced", [](const std::string&) { + add_with_icon("mode_expert", L("Mode: Expert"), L("Mode"), "advanced", [](const std::string&) { wxGetApp().set_mode(comExpert); return AppActionRunResult{AppActionRunResult::Level::Success}; }); // Mirrors Preferences > Developer > Developer mode: flip the flag, persist, refresh the UI. - add_with_icon("toggle_developer_mode", _u8L("Toggle Developer Mode"), _u8L("Mode"), "advanced", [](const std::string&) { + add_with_icon("toggle_developer_mode", L("Toggle Developer Mode"), L("Mode"), "advanced", [](const std::string&) { GUI_App& app = wxGetApp(); const bool on = !app.app_config->get_bool("developer_mode"); app.app_config->set_bool("developer_mode", on); @@ -246,27 +251,27 @@ std::vector build_command_catalog() }); // ---- Export pipeline ---- - add_with_icon("export_gcode", _u8L("Export G-code"), _u8L("Slice & Export"), "custom-gcode_gcode", [](const std::string&) { + add_with_icon("export_gcode", L("Export G-code"), L("Slice & Export"), "custom-gcode_gcode", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->export_gcode(false); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("export_stl", _u8L("Export STL"), _u8L("Slice & Export"), "save", [](const std::string&) { + add_with_icon("export_stl", L("Export STL"), L("Slice & Export"), "save", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->export_stl(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("export_3mf", _u8L("Export 3MF"), _u8L("Slice & Export"), "menu_save", [](const std::string&) { + add_with_icon("export_3mf", L("Export 3MF"), L("Slice & Export"), "menu_save", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->export_core_3mf(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("export_sliced_file", _u8L("Export Sliced File"), _u8L("Slice & Export"), "save", [](const std::string&) { + add_with_icon("export_sliced_file", L("Export Sliced File"), L("Slice & Export"), "save", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->export_gcode_3mf(false); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("export_all_sliced_file", _u8L("Export All Sliced Files"), _u8L("Slice & Export"), "save", [](const std::string&) { + add_with_icon("export_all_sliced_file", L("Export All Sliced Files"), L("Slice & Export"), "save", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->export_gcode_3mf(true); return AppActionRunResult{AppActionRunResult::Level::Success}; @@ -275,39 +280,39 @@ std::vector build_command_catalog() // ---- Calibration ---- // The tab-strip calib_sf glyph is drawn white for the dark tab bar and vanishes on the palette's // light tile, so each wizard borrows the matching settings-group icon instead (gray + accent green). - add_with_icon("calib_temperature", _u8L("Temperature Calibration"), _u8L("Calibration"), "param_temperature", + add_with_icon("calib_temperature", L("Temperature Calibration"), L("Calibration"), "param_temperature", [](const std::string&) { return calib_command(CalibKind::Temperature); }); - add_with_icon("calib_max_volumetric", _u8L("Max Volumetric Speed Calibration"), _u8L("Calibration"), "param_volumetric_speed", + add_with_icon("calib_max_volumetric", L("Max Volumetric Speed Calibration"), L("Calibration"), "param_volumetric_speed", [](const std::string&) { return calib_command(CalibKind::MaxVolumetric); }); - add_with_icon("calib_pressure_advance", _u8L("Pressure Advance Calibration"), _u8L("Calibration"), "param_flow_ratio_and_pressure_advance", + add_with_icon("calib_pressure_advance", L("Pressure Advance Calibration"), L("Calibration"), "param_flow_ratio_and_pressure_advance", [](const std::string&) { return calib_command(CalibKind::PressureAdvance); }); - add_with_icon("calib_flow_ratio", _u8L("Flow Ratio Calibration"), _u8L("Calibration"), "param_flow_ratio_and_pressure_advance", + add_with_icon("calib_flow_ratio", L("Flow Ratio Calibration"), L("Calibration"), "param_flow_ratio_and_pressure_advance", [](const std::string&) { return calib_command(CalibKind::FlowRatio); }); - add_with_icon("calib_retraction", _u8L("Retraction Calibration"), _u8L("Calibration"), "param_retraction", + add_with_icon("calib_retraction", L("Retraction Calibration"), L("Calibration"), "param_retraction", [](const std::string&) { return calib_command(CalibKind::Retraction); }); - add_with_icon("calib_cornering", _u8L("Cornering Calibration"), _u8L("Calibration"), "param_precision", + add_with_icon("calib_cornering", L("Cornering Calibration"), L("Calibration"), "param_precision", [](const std::string&) { return calib_command(CalibKind::Cornering); }); - add_with_icon("calib_input_shaping_freq", _u8L("Input Shaping Frequency Calibration"), _u8L("Calibration"), "param_resonance_avoidance", + add_with_icon("calib_input_shaping_freq", L("Input Shaping Frequency Calibration"), L("Calibration"), "param_resonance_avoidance", [](const std::string&) { return calib_command(CalibKind::InputShapingFreq); }); - add_with_icon("calib_input_shaping_damp", _u8L("Input Shaping Damping Calibration"), _u8L("Calibration"), "param_resonance_avoidance", + add_with_icon("calib_input_shaping_damp", L("Input Shaping Damping Calibration"), L("Calibration"), "param_resonance_avoidance", [](const std::string&) { return calib_command(CalibKind::InputShapingDamp); }); - add_with_icon("calib_vfa", _u8L("VFA Calibration"), _u8L("Calibration"), "param_speed", [](const std::string&) { return calib_command(CalibKind::VFA); }); + add_with_icon("calib_vfa", L("VFA Calibration"), L("Calibration"), "param_speed", [](const std::string&) { return calib_command(CalibKind::VFA); }); // ---- View ---- - // Titles are built with _u8L here (not via a variable) so xgettext can extract them. + // Titles are L("...")-marked here (not via a variable) so xgettext can extract them. for (auto [key, dir, title] : - std::initializer_list>{{"view_top", "top", _u8L("View: Top")}, - {"view_bottom", "bottom", _u8L("View: Bottom")}, - {"view_front", "front", _u8L("View: Front")}, - {"view_rear", "rear", _u8L("View: Rear")}, - {"view_left", "left", _u8L("View: Left")}, - {"view_right", "right", _u8L("View: Right")}, - {"view_iso", "iso", _u8L("View: Isometric")}}) { + std::initializer_list>{{"view_top", "top", L("View: Top")}, + {"view_bottom", "bottom", L("View: Bottom")}, + {"view_front", "front", L("View: Front")}, + {"view_rear", "rear", L("View: Rear")}, + {"view_left", "left", L("View: Left")}, + {"view_right", "right", L("View: Right")}, + {"view_iso", "iso", L("View: Isometric")}}) { std::string k = key, d = dir; - add(k, title, _u8L("View"), + add(k, title, L("View"), [d](const std::string&) { return view_command(wxGetApp().plater(), d); }); } - add("view_default", _u8L("View: Default"), _u8L("View"), [](const std::string&) { + add("view_default", L("View: Default"), L("View"), [](const std::string&) { Plater* plater = wxGetApp().plater(); if (plater) { plater->select_view("plate"); @@ -316,75 +321,75 @@ std::vector build_command_catalog() } return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("view_fit_bed", _u8L("Fit Bed to View"), _u8L("View"), [](const std::string&) { + add("view_fit_bed", L("Fit Bed to View"), L("View"), [](const std::string&) { if (Plater* plater = wxGetApp().plater()) if (GLCanvas3D* canvas = plater->get_current_canvas3D()) canvas->zoom_to_bed(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("view_toggle_perspective", _u8L("Toggle Perspective"), _u8L("View"), [](const std::string&) { + add("view_toggle_perspective", L("Toggle Perspective"), L("View"), [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->get_camera().select_next_type(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("reset_window_layout", _u8L("Reset Window Layout"), _u8L("View"), "toolbar_reset", [](const std::string&) { + add_with_icon("reset_window_layout", L("Reset Window Layout"), L("View"), "toolbar_reset", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->reset_window_layout(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); // ---- Object ---- - add_with_icon("obj_delete", _u8L("Delete Selected"), _u8L("Object"), "delete", [](const std::string&) { + add_with_icon("obj_delete", L("Delete Selected"), L("Object"), "delete", [](const std::string&) { return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->remove_selected(); }); }); - add_with_icon("obj_delete_all", _u8L("Delete All Objects"), _u8L("Object"), "delete", [](const std::string&) { + add_with_icon("obj_delete_all", L("Delete All Objects"), L("Object"), "delete", [](const std::string&) { return object_op( wxGetApp().plater(), [](Plater* p) { return p->can_delete_all(); }, [](Plater* p) { p->delete_all_objects_from_model(); }); }); - add_with_icon("obj_mirror_x", _u8L("Mirror X"), _u8L("Object"), "menu_mirror_x", [](const std::string&) { + add_with_icon("obj_mirror_x", L("Mirror X"), L("Object"), "menu_mirror_x", [](const std::string&) { return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::X); }); }); - add_with_icon("obj_mirror_y", _u8L("Mirror Y"), _u8L("Object"), "menu_mirror_y", [](const std::string&) { + add_with_icon("obj_mirror_y", L("Mirror Y"), L("Object"), "menu_mirror_y", [](const std::string&) { return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::Y); }); }); - add_with_icon("obj_mirror_z", _u8L("Mirror Z"), _u8L("Object"), "menu_mirror_z", [](const std::string&) { + add_with_icon("obj_mirror_z", L("Mirror Z"), L("Object"), "menu_mirror_z", [](const std::string&) { return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::Z); }); }); - add_with_icon("obj_split_objects", _u8L("Split to Objects"), _u8L("Object"), "menu_split_objects", [](const std::string&) { + add_with_icon("obj_split_objects", L("Split to Objects"), L("Object"), "menu_split_objects", [](const std::string&) { return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_split_to_objects(); }, [](Plater* p) { p->split_object(true); }); }); - add_with_icon("obj_split_parts", _u8L("Split to Parts"), _u8L("Object"), "menu_split_parts", [](const std::string&) { + add_with_icon("obj_split_parts", L("Split to Parts"), L("Object"), "menu_split_parts", [](const std::string&) { return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_split_to_volumes(); }, [](Plater* p) { p->split_volume(); }); }); - add("obj_center", _u8L("Center Selected on Plate"), _u8L("Object"), [](const std::string&) { + add("obj_center", L("Center Selected on Plate"), L("Object"), [](const std::string&) { return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->center_selection(); }); }); - add_with_icon("obj_drop", _u8L("Drop to Bed"), _u8L("Object"), "toolbar_flatten", [](const std::string&) { + add_with_icon("obj_drop", L("Drop to Bed"), L("Object"), "toolbar_flatten", [](const std::string&) { return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->drop_selection(); }); }); - add("obj_fit_volume", _u8L("Scale to Fit Print Volume"), _u8L("Object"), [](const std::string&) { + add("obj_fit_volume", L("Scale to Fit Print Volume"), L("Object"), [](const std::string&) { return object_op( wxGetApp().plater(), [](Plater* p) { return p->can_scale_to_print_volume(); }, [](Plater* p) { p->scale_selection_to_fit_print_volume(); }); }); - add_with_icon("obj_instances_up", _u8L("Increase Instances"), _u8L("Object"), "instance_add", [](const std::string&) { + add_with_icon("obj_instances_up", L("Increase Instances"), L("Object"), "instance_add", [](const std::string&) { return object_op( wxGetApp().plater(), [](Plater* p) { return p->can_increase_instances(); }, [](Plater* p) { p->increase_instances(); }); }); - add_with_icon("obj_instances_down", _u8L("Decrease Instances"), _u8L("Object"), "instance_remove", [](const std::string&) { + add_with_icon("obj_instances_down", L("Decrease Instances"), L("Object"), "instance_remove", [](const std::string&) { return object_op( wxGetApp().plater(), [](Plater* p) { return p->can_decrease_instances(); }, [](Plater* p) { p->decrease_instances(); }); }); - add_with_icon("obj_arrange", _u8L("Auto-Arrange"), _u8L("Object"), "toolbar_arrange", [](const std::string&) { + add_with_icon("obj_arrange", L("Auto-Arrange"), L("Object"), "toolbar_arrange", [](const std::string&) { return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_arrange(); }, [](Plater* p) { p->arrange(); }); }); - add_with_icon("obj_orient", _u8L("Auto-Orient"), _u8L("Object"), "toolbar_orient", [](const std::string&) { + add_with_icon("obj_orient", L("Auto-Orient"), L("Object"), "toolbar_orient", [](const std::string&) { return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_arrange(); }, [](Plater* p) { p->orient(); }); }); // ---- Add Primitive ---- (the Add > Add Primitive submenu; creates a new object) auto add_primitive = [&](std::string key, std::string title, std::string icon, const char* type_name) { - add_with_icon(std::move(key), std::move(title), _u8L("Add Primitive"), std::move(icon), [type_name](const std::string&) { + add_with_icon(std::move(key), std::move(title), L("Add Primitive"), std::move(icon), [type_name](const std::string&) { Plater* plater = wxGetApp().plater(); if (plater) { ensure_3d_view(plater); @@ -394,13 +399,13 @@ std::vector build_command_catalog() return AppActionRunResult{AppActionRunResult::Level::Success}; }); }; - add_primitive("add_primitive_cube", _u8L("Cube"), "menu_obj_cube", "Cube"); - add_primitive("add_primitive_cylinder", _u8L("Cylinder"), "menu_obj_cylinder", "Cylinder"); - add_primitive("add_primitive_sphere", _u8L("Sphere"), "menu_obj_sphere", "Sphere"); - add_primitive("add_primitive_cone", _u8L("Cone"), "menu_obj_cone", "Cone"); - add_primitive("add_primitive_disc", _u8L("Disc"), "menu_obj_disc", "Disc"); - add_primitive("add_primitive_torus", _u8L("Torus"), "menu_obj_torus", "Torus"); - add_with_icon("add_primitive_text", _u8L("Text"), _u8L("Add Primitive"), "menu_obj_text", [](const std::string&) { + add_primitive("add_primitive_cube", L("Cube"), "menu_obj_cube", "Cube"); + add_primitive("add_primitive_cylinder", L("Cylinder"), "menu_obj_cylinder", "Cylinder"); + add_primitive("add_primitive_sphere", L("Sphere"), "menu_obj_sphere", "Sphere"); + add_primitive("add_primitive_cone", L("Cone"), "menu_obj_cone", "Cone"); + add_primitive("add_primitive_disc", L("Disc"), "menu_obj_disc", "Disc"); + add_primitive("add_primitive_torus", L("Torus"), "menu_obj_torus", "Torus"); + add_with_icon("add_primitive_text", L("Text"), L("Add Primitive"), "menu_obj_text", [](const std::string&) { Plater* plater = wxGetApp().plater(); if (plater) { ensure_3d_view(plater); @@ -410,7 +415,7 @@ std::vector build_command_catalog() } return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("add_primitive_svg", _u8L("SVG"), _u8L("Add Primitive"), "menu_obj_svg", [](const std::string&) { + add_with_icon("add_primitive_svg", L("SVG"), L("Add Primitive"), "menu_obj_svg", [](const std::string&) { Plater* plater = wxGetApp().plater(); if (plater) { ensure_3d_view(plater); @@ -424,7 +429,7 @@ std::vector build_command_catalog() // ---- Add Handy models ---- (the Add > Add Handy models submenu) const std::vector& handy = MenuFactory::handy_models(); for (std::size_t i = 0; i < handy.size(); ++i) { - add("add_handy_" + std::string(handy[i].key), Slic3r::GUI::I18N::translate_utf8(handy[i].label), _u8L("Add Handy models"), + add("add_handy_" + std::string(handy[i].key), handy[i].label, L("Add Handy models"), [i](const std::string&) { if (Plater* plater = wxGetApp().plater()) ensure_3d_view(plater); @@ -434,7 +439,7 @@ std::vector build_command_catalog() } // ---- Plate ---- - add_with_icon("plate_add", _u8L("Add Plate"), _u8L("Plate"), "toolbar_add_plate", [](const std::string&) { + add_with_icon("plate_add", L("Add Plate"), L("Plate"), "toolbar_add_plate", [](const std::string&) { Plater* plater = wxGetApp().plater(); if (!is_fff_plater(plater)) return plate_unavailable(); @@ -443,7 +448,7 @@ std::vector build_command_catalog() plater->add_plate(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("plate_duplicate", _u8L("Duplicate Plate"), _u8L("Plate"), "menu_copy", [](const std::string&) { + add_with_icon("plate_duplicate", L("Duplicate Plate"), L("Plate"), "menu_copy", [](const std::string&) { Plater* plater = wxGetApp().plater(); if (!is_fff_plater(plater)) return plate_unavailable(); @@ -452,7 +457,7 @@ std::vector build_command_catalog() plater->duplicate_plate(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("plate_delete", _u8L("Delete Plate"), _u8L("Plate"), "delete", [](const std::string&) { + add_with_icon("plate_delete", L("Delete Plate"), L("Plate"), "delete", [](const std::string&) { Plater* plater = wxGetApp().plater(); if (!is_fff_plater(plater)) return plate_unavailable(); @@ -461,7 +466,7 @@ std::vector build_command_catalog() plater->delete_plate(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("plate_rename", _u8L("Rename Plate"), _u8L("Plate"), "plate_name_edit", [](const std::string&) { + add_with_icon("plate_rename", L("Rename Plate"), L("Plate"), "plate_name_edit", [](const std::string&) { Plater* plater = wxGetApp().plater(); if (!is_fff_plater(plater)) return plate_unavailable(); @@ -472,7 +477,7 @@ std::vector build_command_catalog() curr->set_plate_name(dlg.get_plate_name().ToUTF8().data()); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("plate_toggle_lock", _u8L("Toggle Plate Lock"), _u8L("Plate"), "lock_normal", [](const std::string&) { + add_with_icon("plate_toggle_lock", L("Toggle Plate Lock"), L("Plate"), "lock_normal", [](const std::string&) { Plater* plater = wxGetApp().plater(); if (!is_fff_plater(plater)) return plate_unavailable(); @@ -482,7 +487,7 @@ std::vector build_command_catalog() plates.lock_plate(index, !plates.is_locked(index)); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("plate_goto", _u8L("Go to Plate"), _u8L("Plate"), "go_next_plate", [](const std::string& param) { + add_with_icon("plate_goto", L("Go to Plate"), L("Plate"), "go_next_plate", [](const std::string& param) { Plater* plater = wxGetApp().plater(); if (!is_fff_plater(plater)) return plate_unavailable(); @@ -500,7 +505,7 @@ std::vector build_command_catalog() }); // ---- Printer / device connection ---- - add_with_icon("sync_ams", _u8L("Synchronize Filament List from AMS"), _u8L("Printer"), "ams_fila_sync", [](const std::string&) { + add_with_icon("sync_ams", L("Synchronize Filament List from AMS"), L("Printer"), "ams_fila_sync", [](const std::string&) { Plater* plater = wxGetApp().plater(); DeviceManager* dev = wxGetApp().getDeviceManager(); if (dev && dev->get_selected_machine() && plater) { @@ -511,11 +516,11 @@ std::vector build_command_catalog() }); // ---- Presets / cloud ---- - add_with_icon("preset_bundle", _u8L("Open Preset Bundle"), _u8L("Presets"), "menu_edit_preset", [](const std::string&) { + add_with_icon("preset_bundle", L("Open Preset Bundle"), L("Presets"), "menu_edit_preset", [](const std::string&) { wxGetApp().open_presetbundledialog(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("sync_presets", _u8L("Sync Presets"), _u8L("Presets"), "printer_sync_ok", [](const std::string&) { + add_with_icon("sync_presets", L("Sync Presets"), L("Presets"), "printer_sync_ok", [](const std::string&) { if (!wxGetApp().is_user_login()) return AppActionRunResult{AppActionRunResult::Level::Info, _L("Sign in to sync presets.")}; wxGetApp().restart_sync_user_preset(); @@ -523,7 +528,7 @@ std::vector build_command_catalog() }); // ---- Import ---- - add_with_icon("import_file", _u8L("Import 3MF/STL/STEP/SVG/OBJ/AMF"), _u8L("Import"), "menu_open", [](const std::string&) { + add_with_icon("import_file", L("Import 3MF/STL/STEP/SVG/OBJ/AMF"), L("Import"), "menu_open", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) { #ifdef __APPLE__ plater->add_model(); @@ -533,71 +538,71 @@ std::vector build_command_catalog() } return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("import_zip_archive", _u8L("Import ZIP Archive"), _u8L("Import"), "menu_open", [](const std::string&) { + add_with_icon("import_zip_archive", L("Import ZIP Archive"), L("Import"), "menu_open", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->import_zip_archive(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("import_configs", _u8L("Import Configs"), _u8L("Import"), "menu_open", [](const std::string&) { + add_with_icon("import_configs", L("Import Configs"), L("Import"), "menu_open", [](const std::string&) { if (MainFrame* mf = wxGetApp().mainframe) mf->load_config_file(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); // ---- Export extras ---- - add_with_icon("export_stl_multi", _u8L("Export All Objects as STLs"), _u8L("Export"), "save", [](const std::string&) { + add_with_icon("export_stl_multi", L("Export All Objects as STLs"), L("Export"), "save", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->export_stl(false, false, true); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("export_drc_single", _u8L("Export All Objects as DRC (one file)"), _u8L("Export"), "save", [](const std::string&) { + add_with_icon("export_drc_single", L("Export All Objects as DRC (one file)"), L("Export"), "save", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->export_stl(false, false, false, FT_DRC); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("export_drc_multi", _u8L("Export All Objects as DRCs"), _u8L("Export"), "save", [](const std::string&) { + add_with_icon("export_drc_multi", L("Export All Objects as DRCs"), L("Export"), "save", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->export_stl(false, false, true, FT_DRC); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("export_toolpaths_obj", _u8L("Export Toolpaths as OBJ"), _u8L("Export"), "custom-gcode_gcode", [](const std::string&) { + add_with_icon("export_toolpaths_obj", L("Export Toolpaths as OBJ"), L("Export"), "custom-gcode_gcode", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->export_toolpaths_to_obj(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("export_config", _u8L("Export Preset Bundle"), _u8L("Export"), "save", [](const std::string&) { + add_with_icon("export_config", L("Export Preset Bundle"), L("Export"), "save", [](const std::string&) { if (MainFrame* mf = wxGetApp().mainframe) mf->export_config(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); // ---- Help ---- (mirrors the top-bar Help menu, plus the wiki/YouTube links) - add("help_keyboard_shortcuts", _u8L("Keyboard Shortcuts"), _u8L("Help"), [](const std::string&) { + add("help_keyboard_shortcuts", L("Keyboard Shortcuts"), L("Help"), [](const std::string&) { wxGetApp().keyboard_shortcuts(ShortcutContext::Global); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("help_setup_wizard", _u8L("Setup Wizard"), _u8L("Help"), [](const std::string&) { + add("help_setup_wizard", L("Setup Wizard"), L("Help"), [](const std::string&) { wxGetApp().ShowUserGuide(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("help_open_config_folder", _u8L("Show Configuration Folder"), _u8L("Help"), "open_project", [](const std::string&) { + add_with_icon("help_open_config_folder", L("Show Configuration Folder"), L("Help"), "open_project", [](const std::string&) { Slic3r::GUI::desktop_open_datadir_folder(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("help_troubleshoot", _u8L("Troubleshoot Center"), _u8L("Help"), [](const std::string&) { + add("help_troubleshoot", L("Troubleshoot Center"), L("Help"), [](const std::string&) { wxGetApp().troubleshoot(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("help_network_test", _u8L("Open Network Test"), _u8L("Help"), [](const std::string&) { + add("help_network_test", L("Open Network Test"), L("Help"), [](const std::string&) { NetworkTestDialog dlg(wxGetApp().mainframe); dlg.ShowModal(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("help_benchmark_3d_scene", _u8L("Benchmark 3D Scene"), _u8L("Help"), [](const std::string&) { + add("help_benchmark_3d_scene", L("Benchmark 3D Scene"), L("Help"), [](const std::string&) { run_scene_benchmark(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("help_tip_of_the_day", _u8L("Show Tip of the Day"), _u8L("Help"), "info", [](const std::string&) { + add_with_icon("help_tip_of_the_day", L("Show Tip of the Day"), L("Help"), "info", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) { plater->get_dailytips()->open(); if (GLCanvas3D* canvas = plater->get_current_canvas3D()) @@ -605,37 +610,37 @@ std::vector build_command_catalog() } return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("help_check_updates", _u8L("Check for Updates"), _u8L("Help"), "refresh", [](const std::string&) { + add_with_icon("help_check_updates", L("Check for Updates"), L("Help"), "refresh", [](const std::string&) { wxGetApp().check_new_version_sf(true, 1); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("help_about", _u8L("About OrcaSlicer"), _u8L("Help"), "OrcaSlicer_gradient_circle", [](const std::string&) { + add_with_icon("help_about", L("About OrcaSlicer"), L("Help"), "OrcaSlicer_gradient_circle", [](const std::string&) { Slic3r::GUI::about(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("open_wiki", _u8L("Open Wiki"), _u8L("Help"), "link_wiki_img", [](const std::string&) { + add_with_icon("open_wiki", L("Open Wiki"), L("Help"), "link_wiki_img", [](const std::string&) { wxLaunchDefaultBrowser("https://www.orcaslicer.com/wiki/", wxBROWSER_NEW_WINDOW); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("open_youtube", _u8L("Open YouTube Channel"), _u8L("Help"), [](const std::string&) { + add("open_youtube", L("Open YouTube Channel"), L("Help"), [](const std::string&) { wxLaunchDefaultBrowser("https://www.youtube.com/@OfficialOrcaSlicer/videos", wxBROWSER_NEW_WINDOW); return AppActionRunResult{AppActionRunResult::Level::Success}; }); // ---- Plugins ---- - add("open_plugins", _u8L("Open Plugins"), _u8L("Plugins"), [](const std::string&) { + add("open_plugins", L("Open Plugins"), L("Plugins"), [](const std::string&) { wxGetApp().open_plugins_dialog(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("refresh_plugins", _u8L("Refresh Plugins"), _u8L("Plugins"), [](const std::string&) { + add("refresh_plugins", L("Refresh Plugins"), L("Plugins"), [](const std::string&) { wxGetApp().refresh_plugins(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("install_plugin", _u8L("Install Plugin"), _u8L("Plugins"), [](const std::string&) { + add("install_plugin", L("Install Plugin"), L("Plugins"), [](const std::string&) { open_plugin_hub(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("install_local_plugin", _u8L("Install Local Plugin"), _u8L("Plugins"), [](const std::string&) { + add("install_local_plugin", L("Install Local Plugin"), L("Plugins"), [](const std::string&) { wxGetApp().install_local_plugin(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); @@ -643,24 +648,12 @@ std::vector build_command_catalog() return out; } -std::vector& catalog_storage() -{ - static std::vector commands = build_command_catalog(); - return commands; -} - } // namespace const std::vector& NativeCommands::catalog() { - return catalog_storage(); -} - -void NativeCommands::rebuild_catalog() -{ - // build_command_catalog() re-runs _u8L under the current locale, so replacing the storage - // refreshes every translated title/group after a language switch. - catalog_storage() = build_command_catalog(); + static const std::vector commands = build_command_catalog(); + return commands; } std::unique_ptr NativeCommands::make_action(const NativeCommand& command) diff --git a/src/slic3r/GUI/NativeCommands.hpp b/src/slic3r/GUI/NativeCommands.hpp index 993b37f564..908011450a 100644 --- a/src/slic3r/GUI/NativeCommands.hpp +++ b/src/slic3r/GUI/NativeCommands.hpp @@ -16,27 +16,25 @@ namespace Slic3r { namespace GUI { struct NativeCommand { std::string key; - std::string title; - std::string group; + std::string title; // untranslated English msgid; make_action() translates it for display + std::string group; // likewise std::string input; // "percent"/"tab" or "" for immediate run std::string icon; // SVG base name, or "" to render a blank tile std::function runner; }; namespace NativeCommands { -// The full built-in command catalog. Built on first use and reused; call rebuild_catalog() after a -// live UI language switch so the translated titles/groups match the new locale. UI thread only. +// The full built-in command catalog. Built on first use and reused; it holds no translated text, so +// it stays valid across a UI language switch. UI thread only. const std::vector& catalog(); -// Rebuilds the catalog in the current locale. UI thread only. -void rebuild_catalog(); - // Dispatches `key` to its runner (unknown keys return a quiet Info). UI thread only. AppActionRunResult run(const std::string& key, const std::string& param = {}); -// Materialises one catalog entry as a runnable AppAction. Keeps the catalog's identity, -// presentation and behaviour as the single source of truth; ActionRegistry only stores and -// dispatches the result. UI thread only. +// Materialises one catalog entry as a runnable AppAction, titled in the current UI language with the +// English title/group kept as search aliases. Keeps the catalog's identity, presentation and +// behaviour as the single source of truth; ActionRegistry only stores and dispatches the result. +// UI thread only. std::unique_ptr make_action(const NativeCommand& command); } // namespace NativeCommands diff --git a/src/slic3r/GUI/OptionsGroup.cpp b/src/slic3r/GUI/OptionsGroup.cpp index 27cdf31050..8af4cfeef8 100644 --- a/src/slic3r/GUI/OptionsGroup.cpp +++ b/src/slic3r/GUI/OptionsGroup.cpp @@ -275,7 +275,8 @@ void OptionsGroup::append_line(const Line& line) const wxString leaf = (leaf_src == L_CONTEXT("Top", "Layers") || leaf_src == L_CONTEXT("Bottom", "Layers")) ? _L_CONTEXT(leaf_src, "Layers") : _(leaf_src); - index.set_line_label(opt.opt_id, type, Search::compose_display_label(line.label, leaf, multi)); + index.set_line_label(opt.opt_id, type, Search::compose_display_label(line.label, leaf, multi), + Search::compose_display_label(line.label_en, from_u8(leaf_src), multi)); } } @@ -650,7 +651,7 @@ Line OptionsGroup::create_single_option_line(const Option& option, const std::st { wxString tooltip = _(get_formatted_tooltip_text(option.opt, option.opt_id)); - Line retval{_(option.opt.label), tooltip}; + Line retval{from_u8(option.opt.label), tooltip}; retval.label_path = path; retval.append_option(option); diff --git a/src/slic3r/GUI/OptionsGroup.hpp b/src/slic3r/GUI/OptionsGroup.hpp index 0943922925..ae629f943e 100644 --- a/src/slic3r/GUI/OptionsGroup.hpp +++ b/src/slic3r/GUI/OptionsGroup.hpp @@ -61,6 +61,7 @@ class Line : public UndoValueUIManager bool m_is_separator{ false }; public: wxString label; + wxString label_en; // untranslated English of `label`, searchable by the Speed Dial in any UI language wxString label_tooltip; std::string label_path; bool undo_to_sys{false}; // BBS: object config @@ -86,12 +87,16 @@ public: void append_widget(const widget_t widget) { m_extra_widgets.push_back(widget); } + // `label` and `tooltip` are the untranslated English msgids; they are translated for display. Line(wxString label, wxString tooltip) : - label(_(label)), label_tooltip(_(tooltip)) {} + label(_(label)), label_en(label), label_tooltip(_(tooltip)) {} Line() : m_is_separator(true) {} + // `new_label` is the untranslated English msgid (an L("...") string), like the ctor's label: + // it is translated for display and kept as label_en. void set_label(const wxString& new_label) { - label = new_label; + label = _(new_label); + label_en = new_label; if (label_widget != nullptr) { label_widget->SetLabel(label + (label.IsEmpty() ? "" : ": ")); label_widget->Refresh(); diff --git a/src/slic3r/GUI/SettingsIndex.cpp b/src/slic3r/GUI/SettingsIndex.cpp index 7300924076..ffd5cdadeb 100644 --- a/src/slic3r/GUI/SettingsIndex.cpp +++ b/src/slic3r/GUI/SettingsIndex.cpp @@ -63,7 +63,7 @@ static Option make_option(const std::string &key, Preset::Type type, const wxStr GUI::Tab::translate_category(category, type).ToStdWstring(), false, mode, tooltip, gc.path, // The settings page draws Line::label; carrying it lets the Speed Dial name a setting // the way the page does. `label`/`label_local` stay the search-oriented name. - into_u8(gc.line_label)}; + into_u8(gc.line_label), into_u8(gc.line_label_en)}; return option; } @@ -250,11 +250,13 @@ void SettingsIndex::set_path(const std::string &opt_key, Preset::Type type, cons m_groups_and_categories[get_key(opt_key, type)].path = path; } -void SettingsIndex::set_line_label(const std::string &opt_key, Preset::Type type, const wxString &label) +void SettingsIndex::set_line_label(const std::string &opt_key, Preset::Type type, const wxString &label, const wxString &label_en) { if (label.IsEmpty()) return; - m_groups_and_categories[get_key(opt_key, type)].line_label = label; + GroupAndCategory &gc = m_groups_and_categories[get_key(opt_key, type)]; + gc.line_label = label; + gc.line_label_en = label_en; } } // namespace Search diff --git a/src/slic3r/GUI/SettingsIndex.hpp b/src/slic3r/GUI/SettingsIndex.hpp index 0815dfec81..fd29696a08 100644 --- a/src/slic3r/GUI/SettingsIndex.hpp +++ b/src/slic3r/GUI/SettingsIndex.hpp @@ -28,12 +28,14 @@ struct GroupAndCategory wxString group; wxString category; wxString icon; // icon of the group's own header, or empty - wxString line_label; // label the settings row actually draws (Line::label), or empty - std::string path; // wiki path (Line::label_path) of the option's line, or empty + wxString line_label; // label the settings row actually draws (Line::label), or empty + wxString line_label_en; // untranslated English of line_label, or empty + std::string path; // wiki path (Line::label_path) of the option's line, or empty }; // Title for a setting: the row label, qualified with the field leaf when the row packs several -// options (e.g. "Cool Plate \u2013 First layer"). Pure; inputs are already localized. +// options (e.g. "Cool Plate \u2013 First layer"). Pure; both inputs are in the same language (the +// localized title, or its English counterpart). inline wxString compose_display_label(const wxString& line_label, const wxString& leaf_label, bool multi) { if (line_label.empty()) @@ -73,6 +75,7 @@ struct Option std::string tooltip; // localized ConfigOptionDef::tooltip, or empty std::string wiki_path; // Line::label_path for the option's row, or empty std::string display_label; // label the settings row draws (localized); empty falls back to label + std::string display_label_en; // untranslated English of display_label, or empty std::string opt_key() const; }; @@ -98,9 +101,9 @@ public: void add_key(const std::string &opt_key, Preset::Type type, const wxString &group, const wxString &category, const wxString &icon = wxEmptyString); void set_path(const std::string &opt_key, Preset::Type type, const std::string &path); - // Record the label the option's row draws, so the Speed Dial names a setting like the page - // (ConfigOptionDef::label/full_label is a search name, not the row text). - void set_line_label(const std::string &opt_key, Preset::Type type, const wxString &label); + // Record the label the option's row draws and its untranslated English, so the Speed Dial names a + // setting like the page (ConfigOptionDef::label/full_label is a search name, not the row text). + void set_line_label(const std::string &opt_key, Preset::Type type, const wxString &label, const wxString &label_en); const std::vector