From 2b2302a279cdcdeee4bb1822ece1ce652ab7fa3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Tue, 6 Oct 2026 21:50:11 -0400 Subject: [PATCH] Translate the remaining shell text at display and keep translated values out of logic --- shell/Commons/I18n.qml | 4 ++ shell/Commons/I18nZh.js | 34 ++++++++++++++++ shell/Commons/Tr.js | 19 +++++++-- shell/plugins/agents/Panel.qml | 40 ++++++++++++------- shell/plugins/bar/widgets/Tray.qml | 3 ++ shell/plugins/panels/audio/Panel.qml | 1 + shell/plugins/panels/bluetooth/Panel.qml | 1 + shell/plugins/panels/clock/Panel.qml | 3 ++ shell/plugins/panels/dropbox/Panel.qml | 1 + shell/plugins/panels/elsewhen/CitySearch.qml | 4 +- shell/plugins/panels/monitor/Panel.qml | 3 +- shell/plugins/panels/power/Panel.qml | 3 +- shell/plugins/panels/tailscale/Panel.qml | 5 +++ shell/plugins/panels/weather/Panel.qml | 6 ++- shell/plugins/panels/wifiqr/Panel.qml | 2 + .../shell.d/agent-usage-claude-limits-test.sh | 4 +- test/shell.d/agents-panel-test.sh | 4 +- 17 files changed, 112 insertions(+), 25 deletions(-) diff --git a/shell/Commons/I18n.qml b/shell/Commons/I18n.qml index e1d38416..857b21d4 100644 --- a/shell/Commons/I18n.qml +++ b/shell/Commons/I18n.qml @@ -9,4 +9,8 @@ QtObject { function tr() { return Tr.tr.apply(null, arguments) } + + function trMatch(text, templates) { + return Tr.trMatch(text, templates) + } } diff --git a/shell/Commons/I18nZh.js b/shell/Commons/I18nZh.js index 9fe36650..384a3975 100644 --- a/shell/Commons/I18nZh.js +++ b/shell/Commons/I18nZh.js @@ -69,6 +69,34 @@ var table = { "Saturday": "星期六", "%1: %2% of its %3 allowance": "%1:占%3额度的 %2%", "Subscriptions": "订阅", + "next expires in %1": "下一次%1后过期", + "Installing the %1 CLI…": "正在安装 %1 命令行…", + "Sign in to %1 again in the browser window that opens.": "在打开的浏览器窗口中重新登录 %1。", + "Sign in to %1 in the browser window that opens.": "在打开的浏览器窗口中登录 %1。", + "Sign in again as this account in the private window that opens.": "在打开的隐私窗口中以这个账户重新登录。", + "Sign in as the account you're adding in the private window that opens.": "在打开的隐私窗口中登录要添加的账户。", + "Signed in to %1 again.": "已重新登录 %1。", + "Signed in to %1.": "已登录 %1。", + "The %1 sign-in didn't finish.": "%1 登录没有完成。", + "Added %1.": "已添加 %1。", + "Starting…": "正在开始…", + "Checking the code…": "正在核对代码…", + "Run `claude auth login` to restore authoritative usage.": "运行 `claude auth login` 恢复准确的用量。", + "Run `codex login` to authenticate.": "运行 `codex login` 完成认证。", + "Set FIREWORKS_API_KEY, run `firectl set-api-key`, or sign in to Fireworks in pi or opencode.": "设置 FIREWORKS_API_KEY,运行 `firectl set-api-key`,或在 pi 或 opencode 中登录 Fireworks。", + "Start Grok, or run `grok login`, to sign in.": "启动 Grok 或运行 `grok login` 登录。", + "Start Grok to see its limits; signing in again is not needed.": "启动 Grok 查看额度,不需要重新登录。", + "Grok's saved sign-in expired. Start Grok to refresh it.": "Grok 保存的登录已过期,启动 Grok 刷新。", + "Fireworks unavailable": "Fireworks 不可用", + "Fireworks usage scan failed": "Fireworks 用量扫描失败", + "Balance unavailable": "余额不可用", + "Claude limits unavailable": "Claude 额度不可用", + "Codex limits unavailable": "Codex 额度不可用", + "Codex unavailable": "Codex 不可用", + "Grok limits unavailable": "Grok 额度不可用", + "Limits paused": "额度已暂停", + "Sign-in expired": "登录已过期", + "Waiting for auth": "等待认证", // Network panel "Wiring bits": "接线中", "Handling packets": "处理数据包", @@ -152,6 +180,7 @@ var table = { "Lamp light": "灯光", "Candlelit": "烛光", "Night owl": "夜猫子", + "focused": "当前", // Audio panel "Audio": "音频", "OUTPUT": "输出", @@ -225,6 +254,9 @@ var table = { "Battery state": "电池状态", "Discharging": "正在放电", "Holding": "保持中", + "Performance": "性能", + "Balanced": "均衡", + "Power-saver": "省电", // Tailscale panel "Encrypting connections": "加密连接", "Sending secrets": "传递秘密", @@ -449,6 +481,8 @@ var table = { "Sunset": "日落", "It's %1 here": "这里现在是 %1", "Jump to a city": "跳到一个城市", + "Add a city": "添加城市", + "Loading zones…": "正在加载时区…", // Media "Pause": "暂停", "Play": "播放" diff --git a/shell/Commons/Tr.js b/shell/Commons/Tr.js index 862dcbbb..1cf6fc1a 100644 --- a/shell/Commons/Tr.js +++ b/shell/Commons/Tr.js @@ -5,7 +5,20 @@ var chinese = Qt.locale().name.indexOf("zh_CN") === 0 function tr(text) { - var out = chinese && Zh.table[text] !== undefined ? Zh.table[text] : text - for (var i = 1; i < arguments.length; i++) out = out.split("%" + i).join(String(arguments[i])) - return out + var args = arguments + var template = chinese && Object.prototype.hasOwnProperty.call(Zh.table, text) ? Zh.table[text] : text + // One pass, so an argument that itself contains %2 stays as given. + return String(template).replace(/%(\d)/g, function(match, n) { + return n > 0 && n < args.length ? String(args[n]) : match + }) +} + +// Text built elsewhere around a name: the first English template that matches is translated with what %1.. captured. +function trMatch(text, templates) { + for (var i = 0; i < templates.length; i++) { + var pattern = "^" + templates[i].replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/%\d/g, "(.+?)") + "$" + var match = String(text).match(new RegExp(pattern)) + if (match) return tr.apply(null, [templates[i]].concat(match.slice(1))) + } + return tr(text) } diff --git a/shell/plugins/agents/Panel.qml b/shell/plugins/agents/Panel.qml index 82992fe4..0f98f143 100644 --- a/shell/plugins/agents/Panel.qml +++ b/shell/plugins/agents/Panel.qml @@ -449,12 +449,18 @@ Panel { function resetCreditsText(credits) { if (!credits || !(Number(credits.available) > 0)) return "" var count = Number(credits.available) - var text = count + " free reset" + (count === 1 ? "" : "s") + var text = I18n.tr(count === 1 ? "%1 free reset" : "%1 free resets", count) var expires = new Date(String(credits.nextExpiresAt || "")).getTime() - if (isFinite(expires) && expires > nowMs) text += " · next expires in " + formatDuration(expires - nowMs) + if (isFinite(expires) && expires > nowMs) text += " · " + I18n.tr("next expires in %1", formatDuration(expires - nowMs)) return text } + // omarchy-agent-account-add reports progress in English around the agent's name. + readonly property var addMessageTemplates: [ + "Installing the %1 CLI…", "Sign in to %1 again in the browser window that opens.", "Sign in to %1 in the browser window that opens.", + "Signed in to %1 again.", "Signed in to %1.", "The %1 sign-in didn't finish.", "Added %1." + ] + function planLabel(p) { var tier = String(p && p.tierLabel || "") return tier === "" ? "" : tier.charAt(0).toUpperCase() + tier.slice(1) @@ -491,11 +497,11 @@ Panel { function windowTitle(label) { var text = String(label || "").toLowerCase() - if (text.indexOf("month") >= 0) return I18n.tr("Monthly") - if (windowIsLong(text)) return I18n.tr("Weekly") - if (text.indexOf("session") >= 0 || windowSpanMs(label) > 0) return I18n.tr("Session") + if (text.indexOf("month") >= 0) return "Monthly" + if (windowIsLong(text)) return "Weekly" + if (text.indexOf("session") >= 0 || windowSpanMs(label) > 0) return "Session" var plain = String(label || "").replace(/\s*\(.*\)\s*/, "").trim() - return plain === "" ? I18n.tr("Limit") : plain + return plain === "" ? "Limit" : plain } // A collector that already knows which window a limit belongs to says so, @@ -1064,6 +1070,7 @@ Panel { Text { visible: root.blankSlate && root.picking width: parent.width + textFormat: Text.PlainText text: I18n.tr("Sign in to an AI coding subscription, and this panel keeps track of how much of it you have left.") color: root.dim font.family: root.fontFamily @@ -1136,6 +1143,7 @@ Panel { Text { width: parent.width + textFormat: Text.PlainText text: I18n.tr("Name this account. It signs in through a private window, so your browser's current account isn't picked up.") color: root.dim font.family: root.fontFamily @@ -1165,7 +1173,7 @@ Panel { Text { width: parent.width textFormat: Text.PlainText - text: root.addStatus + text: I18n.trMatch(root.addStatus, root.addMessageTemplates) color: root.foreground font.family: root.fontFamily font.pixelSize: Style.font.bodySmall @@ -1178,6 +1186,7 @@ Panel { spacing: Style.space(4) Text { + textFormat: Text.PlainText text: I18n.tr("Confirm this code in your browser") color: root.dim font.family: root.fontFamily @@ -1202,6 +1211,7 @@ Panel { Text { width: parent.width + textFormat: Text.PlainText text: I18n.tr("If the page shows a code instead of finishing, paste it here.") color: root.dim font.family: root.fontFamily @@ -1233,7 +1243,7 @@ Panel { visible: root.addStage === "done" || root.addStage === "error" width: parent.width textFormat: Text.PlainText - text: root.addResult + text: I18n.trMatch(root.addResult, root.addMessageTemplates) color: root.addStage === "error" ? root.urgent : root.foreground font.family: root.fontFamily font.pixelSize: Style.font.bodySmall @@ -1340,7 +1350,7 @@ Panel { width: parent.width textFormat: Text.PlainText // Shown for the status, so a record with no help to offer says the status rather than nothing. - text: section.provider ? String(section.provider.authHelpText || section.provider.usageStatusText || "") : "" + text: section.provider ? I18n.tr(String(section.provider.authHelpText || section.provider.usageStatusText || "")) : "" color: root.urgent font.family: root.fontFamily font.pixelSize: Style.font.caption @@ -1809,6 +1819,7 @@ Panel { id: headActive visible: head.isActive anchors.right: parent.right + textFormat: Text.PlainText text: I18n.tr("ACTIVE") color: Color.accent font.family: root.fontFamily @@ -1857,6 +1868,7 @@ Panel { Text { id: unavailableLabel + textFormat: Text.PlainText text: I18n.tr("Usage unavailable") color: root.dim font.family: root.fontFamily @@ -1867,7 +1879,7 @@ Panel { PanelToolTip { visible: unavailableHover.hovered && unavailable.visible - text: unavailable.record ? String(unavailable.record.authHelpText || I18n.tr("Usage has not been updated yet.")) : "" + text: unavailable.record ? I18n.tr(String(unavailable.record.authHelpText || "Usage has not been updated yet.")) : "" } } @@ -1892,7 +1904,7 @@ Panel { text: { var lines = [] if (compact.window) { - lines.push(I18n.tr("%1: %2% used", compact.window.title, Math.round(compact.window.percent * 100)) + lines.push(I18n.tr("%1: %2% used", I18n.tr(compact.window.title), Math.round(compact.window.percent * 100)) + (compact.resetMs > 0 ? " · " + I18n.tr("resets in %1", root.formatDuration(compact.resetMs)) : "")) } if (compact.stale) @@ -1900,8 +1912,8 @@ Panel { ? I18n.tr("Last updated %1 ago", root.formatDuration(root.nowMs - compact.fetchedAt)) : compact.fetchedAt > 0 ? I18n.tr("Last updated less than a minute ago") : I18n.tr("Last updated time unavailable")) for (var i = 0; i < compact.scoped.length; i++) - lines.push(I18n.tr("%1: %2% of its %3 allowance", compact.scoped[i].title, Math.round(compact.scoped[i].percent * 100), - String(compact.window ? compact.window.title : "").toLowerCase())) + lines.push(I18n.tr("%1: %2% of its %3 allowance", I18n.tr(compact.scoped[i].title), Math.round(compact.scoped[i].percent * 100), + I18n.tr(String(compact.window ? compact.window.title : "")).toLowerCase())) return lines.join("\n") } } @@ -1911,7 +1923,7 @@ Panel { textFormat: Text.PlainText width: parent.width * 0.3 anchors.verticalCenter: parent.verticalCenter - text: compact.window ? compact.window.title : "" + text: compact.window ? I18n.tr(compact.window.title) : "" color: root.foreground font.family: root.fontFamily font.pixelSize: Style.font.bodySmall diff --git a/shell/plugins/bar/widgets/Tray.qml b/shell/plugins/bar/widgets/Tray.qml index f6b5c012..d1c7bb0b 100644 --- a/shell/plugins/bar/widgets/Tray.qml +++ b/shell/plugins/bar/widgets/Tray.qml @@ -410,6 +410,7 @@ BarWidget { spacing: Style.space(8) Text { + textFormat: Text.PlainText text: I18n.tr("Tray icons") color: root.foreground font.family: root.fontFamily @@ -418,6 +419,7 @@ BarWidget { } Text { + textFormat: Text.PlainText text: I18n.tr("Pinned icons stay visible. Hidden icons never show.") color: Qt.darker(root.foreground, 1.4) font.family: root.fontFamily @@ -428,6 +430,7 @@ BarWidget { Text { visible: root.allItems.length === 0 + textFormat: Text.PlainText text: I18n.tr("No tray items reporting.") color: Qt.darker(root.foreground, 1.5) font.family: root.fontFamily diff --git a/shell/plugins/panels/audio/Panel.qml b/shell/plugins/panels/audio/Panel.qml index 3b78f54c..54cc879f 100644 --- a/shell/plugins/panels/audio/Panel.qml +++ b/shell/plugins/panels/audio/Panel.qml @@ -768,6 +768,7 @@ Panel { spacing: Style.space(2) Text { + textFormat: Text.PlainText text: I18n.tr("Audio") color: root.bar.foreground font.family: root.bar.fontFamily diff --git a/shell/plugins/panels/bluetooth/Panel.qml b/shell/plugins/panels/bluetooth/Panel.qml index 696c664c..72276c12 100644 --- a/shell/plugins/panels/bluetooth/Panel.qml +++ b/shell/plugins/panels/bluetooth/Panel.qml @@ -738,6 +738,7 @@ Panel { spacing: Style.space(2) Text { + textFormat: Text.PlainText text: I18n.tr("Bluetooth") color: root.bar.foreground font.family: root.bar.fontFamily diff --git a/shell/plugins/panels/clock/Panel.qml b/shell/plugins/panels/clock/Panel.qml index 227bcf59..610cf42f 100644 --- a/shell/plugins/panels/clock/Panel.qml +++ b/shell/plugins/panels/clock/Panel.qml @@ -369,6 +369,7 @@ Panel { Text { anchors.verticalCenter: parent.verticalCenter + textFormat: Text.PlainText text: I18n.tr("BORN") color: Qt.darker(root.contentForeground, 1.5) font.family: root.contentFontFamily @@ -392,6 +393,7 @@ Panel { anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 0 leftPadding: Style.space(6) + textFormat: Text.PlainText text: I18n.tr("LIVE TO") color: Qt.darker(root.contentForeground, 1.5) font.family: root.contentFontFamily @@ -479,6 +481,7 @@ Panel { id: lifeLabel anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter + textFormat: Text.PlainText text: I18n.tr("LIFE") color: Qt.darker(root.contentForeground, 1.5) font.family: root.contentFontFamily diff --git a/shell/plugins/panels/dropbox/Panel.qml b/shell/plugins/panels/dropbox/Panel.qml index a050ce27..d585d3a2 100644 --- a/shell/plugins/panels/dropbox/Panel.qml +++ b/shell/plugins/panels/dropbox/Panel.qml @@ -327,6 +327,7 @@ Panel { Text { visible: dropbox.files.length === 0 width: parent.width + textFormat: Text.PlainText text: I18n.tr("No synced files found.") color: root.dim font.family: root.fontFamily diff --git a/shell/plugins/panels/elsewhen/CitySearch.qml b/shell/plugins/panels/elsewhen/CitySearch.qml index f0c1c89a..06421a1e 100644 --- a/shell/plugins/panels/elsewhen/CitySearch.qml +++ b/shell/plugins/panels/elsewhen/CitySearch.qml @@ -16,9 +16,9 @@ Column { // Zone id -> offset text; read inside a binding, so its dependencies track. property var offsetLabel: function(zoneId) { return "" } property bool loading: false - property string buttonText: "+ Add a city" + property string buttonText: "+ " + I18n.tr("Add a city") property string placeholderText: I18n.tr("Search cities…") - property string loadingText: "Loading zones\u2026" + property string loadingText: I18n.tr("Loading zones…") property real fontSize: Style.font.bodySmall property color foreground: Color.foreground property color dim: Qt.darker(foreground, 1.55) diff --git a/shell/plugins/panels/monitor/Panel.qml b/shell/plugins/panels/monitor/Panel.qml index b703b719..dffd10c7 100644 --- a/shell/plugins/panels/monitor/Panel.qml +++ b/shell/plugins/panels/monitor/Panel.qml @@ -555,6 +555,7 @@ Panel { spacing: Style.space(2) Text { + textFormat: Text.PlainText text: I18n.tr("Display") color: root.bar.foreground font.family: root.bar.fontFamily @@ -899,7 +900,7 @@ Panel { Text { textFormat: Text.PlainText - text: monitorRow.display.name + (monitorRow.display.focused ? " · focused" : "") + text: monitorRow.display.name + (monitorRow.display.focused ? " · " + I18n.tr("focused") : "") color: root.bar.foreground font.family: root.bar.fontFamily font.pixelSize: Style.font.body diff --git a/shell/plugins/panels/power/Panel.qml b/shell/plugins/panels/power/Panel.qml index c20b5bce..c08ea206 100644 --- a/shell/plugins/panels/power/Panel.qml +++ b/shell/plugins/panels/power/Panel.qml @@ -346,6 +346,7 @@ Panel { spacing: Style.space(2) Text { + textFormat: Text.PlainText text: I18n.tr("Battery") color: root.bar.foreground font.family: root.bar.fontFamily @@ -483,7 +484,7 @@ Panel { width: profileRow.cellWidth iconText: root.profileIcon(String(modelData)) iconSize: Style.font.title - text: String(modelData).charAt(0).toUpperCase() + String(modelData).slice(1) + text: I18n.tr(String(modelData).charAt(0).toUpperCase() + String(modelData).slice(1)) fontSize: Style.font.bodySmall foreground: root.bar.foreground fontFamily: root.bar.fontFamily diff --git a/shell/plugins/panels/tailscale/Panel.qml b/shell/plugins/panels/tailscale/Panel.qml index 99941acd..c23ab77b 100644 --- a/shell/plugins/panels/tailscale/Panel.qml +++ b/shell/plugins/panels/tailscale/Panel.qml @@ -520,6 +520,7 @@ Panel { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Style.space(12) + textFormat: Text.PlainText text: I18n.tr("Tailscale CLI is not installed or not on PATH.") color: root.dim font.family: root.fontFamily @@ -638,6 +639,7 @@ Panel { Text { visible: root.filteredMullvadRegions.length === 0 width: parent.width + textFormat: Text.PlainText text: I18n.tr("No Mullvad regions found.") color: root.dim font.family: root.fontFamily @@ -684,6 +686,7 @@ Panel { Text { visible: tailscale.installed && tailscale.active && tailscale.peers.length === 0 width: parent.width + textFormat: Text.PlainText text: I18n.tr("No machines found on this tailnet.") color: root.dim font.family: root.fontFamily @@ -777,6 +780,7 @@ Panel { Text { Layout.fillWidth: true + textFormat: Text.PlainText text: I18n.tr("Authorize Tailscale operator") color: root.foreground font.family: root.fontFamily @@ -786,6 +790,7 @@ Panel { Text { Layout.fillWidth: true + textFormat: Text.PlainText text: I18n.tr("Allow this user to operate this Tailscale profile") color: root.dim font.family: root.fontFamily diff --git a/shell/plugins/panels/weather/Panel.qml b/shell/plugins/panels/weather/Panel.qml index cc75ab8c..9bbbc246 100644 --- a/shell/plugins/panels/weather/Panel.qml +++ b/shell/plugins/panels/weather/Panel.qml @@ -308,7 +308,7 @@ Panel { } function dayName(dateString) { - return Model.dayName(dateString, function(date) { return Qt.formatDate(date, "dddd") }) + return Model.dayName(dateString, function(date) { return I18n.chinese ? Qt.locale().toString(date, "dddd") : Qt.formatDate(date, "dddd") }) } // Bare degree value (no unit letter), used in the forecast row. @@ -683,6 +683,7 @@ Panel { Column { spacing: Style.space(5) Text { + textFormat: Text.PlainText text: I18n.tr("FEELS") color: Qt.darker(root.bar.foreground, 1.5) font.family: root.bar.fontFamily @@ -701,6 +702,7 @@ Panel { Column { spacing: Style.space(5) Text { + textFormat: Text.PlainText text: I18n.tr("WIND") color: Qt.darker(root.bar.foreground, 1.5) font.family: root.bar.fontFamily @@ -719,6 +721,7 @@ Panel { Column { spacing: Style.space(5) Text { + textFormat: Text.PlainText text: I18n.tr("HUMID") color: Qt.darker(root.bar.foreground, 1.5) font.family: root.bar.fontFamily @@ -792,6 +795,7 @@ Panel { Text { visible: !root.current + textFormat: Text.PlainText text: I18n.tr("Fetching forecast…") color: Qt.darker(root.bar.foreground, 1.5) font.family: root.bar.fontFamily diff --git a/shell/plugins/panels/wifiqr/Panel.qml b/shell/plugins/panels/wifiqr/Panel.qml index af05a1f7..d67e6dbb 100644 --- a/shell/plugins/panels/wifiqr/Panel.qml +++ b/shell/plugins/panels/wifiqr/Panel.qml @@ -305,6 +305,7 @@ Item { Text { visible: root.loading + textFormat: Text.PlainText text: I18n.tr("Generating QR code…") color: root.onScrimDim font.family: root.fontFamily @@ -328,6 +329,7 @@ Item { Text { visible: root.showingQr + textFormat: Text.PlainText text: I18n.tr("Scan to join this network") color: root.onScrimDim font.family: root.fontFamily diff --git a/test/shell.d/agent-usage-claude-limits-test.sh b/test/shell.d/agent-usage-claude-limits-test.sh index df39f9d8..70d2b379 100644 --- a/test/shell.d/agent-usage-claude-limits-test.sh +++ b/test/shell.d/agent-usage-claude-limits-test.sh @@ -320,10 +320,12 @@ pass "Claude collector caches a successful probe" # The panel reads a window out of a label, and that guess cannot survive a # model name — "Opus 5 (1M context)" parses as a one-minute window. A collector # that states the title outright is taken at its word. -run_node_test <<'JS' +run_node_test <<'JS' || exit 1 const fs = require('fs') const source = fs.readFileSync(root + '/shell/plugins/agents/Panel.qml', 'utf8') const panelRoot = { nowMs: Date.now() } +// The panel's text goes through I18n; English here, with its %1 arguments filled. +const I18n = { tr: (text, ...args) => text.replace(/%(\d)/g, (m, n) => (n > 0 && n <= args.length ? String(args[n - 1]) : m)) } const start = source.indexOf('function windowIsLong') const end = source.indexOf('// The window that decides') assert(start > 0 && end > start, 'agents panel exposes its limit-window helpers') diff --git a/test/shell.d/agents-panel-test.sh b/test/shell.d/agents-panel-test.sh index e79ca340..5449aaff 100755 --- a/test/shell.d/agents-panel-test.sh +++ b/test/shell.d/agents-panel-test.sh @@ -27,11 +27,11 @@ assert(/target\.kind === "signin"\) signInAgain\(/.test(panelSource) && /target\ assert(/target\.kind === "autoswitch"\) setSwitchMode\(/.test(panelSource), 'Enter on Autoswitch flips the switch mode') assert(/keyColumn = use >= 0 \? use : /.test(panelSource), 'moving up or down onto an account lands on Use') assert(/Qt\.callLater\(function\(\) \{ if \(picking\) pointAt\("choice", 0\) \}\)/.test(panelSource), 'picking an agent to add starts with the first one focused') -assert(!/opacity: stale \?/.test(panelSource) && /"Last updated " \+ root\.formatDuration/.test(panelSource), 'stale limits say how old they are only on hover') +assert(!/opacity: stale \?/.test(panelSource) && /I18n\.tr\("Last updated %1 ago", root\.formatDuration/.test(panelSource), 'stale limits say how old they are only on hover') assert(/onPickingChanged: resetKeys\(\)/.test(panelSource), 'the cursor starts over when the agent list comes or goes') assert(!/t === "a" \|\| t === "A"/.test(panelSource), 'adding an account has no hotkey; the + is the way in') assert(/if \(!accounts\[a\]\.active\) \{/.test(panelSource) && /if \(row\.length > 0\) rows\.push\(row\)/.test(panelSource), 'the active account with nothing to fix is not a keyboard stop') -assert(/text: section\.provider \? String\(section\.provider\.authHelpText \|\| section\.provider\.usageStatusText \|\| ""\) : ""/.test(panelSource), 'the trouble line falls back to the usage status when a record has no auth help') +assert(/text: section\.provider \? I18n\.tr\(String\(section\.provider\.authHelpText \|\| section\.provider\.usageStatusText \|\| ""\)\) : ""/.test(panelSource), 'the trouble line falls back to the usage status when a record has no auth help') const mainSource = fs.readFileSync(root + '/shell/plugins/agents/Main.qml', 'utf8') assert(/var from = Math\.min\(0\.8, \(threshold - 15\) \/ 100\)/.test(mainSource), 'faster checks start 15 points below the switch threshold') assert(/rows\.push\(\[\{ kind: "provider", index: p \}\]\)/.test(panelSource), "every agent's header is a keyboard stop")