From b7152a29cdd78b4c553cc9f5427d8f704376bdbb Mon Sep 17 00:00:00 2001 From: Abeautifulsnow Date: Tue, 22 Sep 2026 10:57:00 +0800 Subject: [PATCH] fix(mqtt): localize max packet size label in connection dialog --- .../connection/ConnectionDialog.vue | 2 +- .../mqttConnectionLabelParity.spec.ts | 104 ++++++++++++++++++ apps/desktop/src/i18n/locales/az.ts | 1 + apps/desktop/src/i18n/locales/en.ts | 1 + apps/desktop/src/i18n/locales/es.ts | 1 + apps/desktop/src/i18n/locales/it.ts | 1 + apps/desktop/src/i18n/locales/ja.ts | 1 + apps/desktop/src/i18n/locales/ko.ts | 1 + apps/desktop/src/i18n/locales/pt-BR.ts | 1 + apps/desktop/src/i18n/locales/tr.ts | 1 + apps/desktop/src/i18n/locales/zh-CN.ts | 1 + apps/desktop/src/i18n/locales/zh-TW.ts | 1 + 12 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 apps/desktop/src/i18n/__tests__/mqttConnectionLabelParity.spec.ts diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index 9eef79123a..f1decc5dd6 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -7798,7 +7798,7 @@ function openExternalUrl(url: string) {
- +
diff --git a/apps/desktop/src/i18n/__tests__/mqttConnectionLabelParity.spec.ts b/apps/desktop/src/i18n/__tests__/mqttConnectionLabelParity.spec.ts new file mode 100644 index 0000000000..607b67b655 --- /dev/null +++ b/apps/desktop/src/i18n/__tests__/mqttConnectionLabelParity.spec.ts @@ -0,0 +1,104 @@ +import { readFileSync } from "node:fs"; +import { createI18n } from "vue-i18n"; +import { describe, expect, it } from "vitest"; +import az from "@/i18n/locales/az"; +import en from "@/i18n/locales/en"; +import es from "@/i18n/locales/es"; +import itLocale from "@/i18n/locales/it"; +import ja from "@/i18n/locales/ja"; +import ko from "@/i18n/locales/ko"; +import ptBR from "@/i18n/locales/pt-BR"; +import tr from "@/i18n/locales/tr"; +import zhCN from "@/i18n/locales/zh-CN"; +import zhTW from "@/i18n/locales/zh-TW"; + +// These assertions read the locale SOURCES as text instead of importing the modules. +// Every non-English locale is `export default withEnglishFallback({ ... })`, and +// withEnglishFallback deep-merges `en` UNDER the locale at import time, so the imported +// default export already carries every english key — including the key this spec exists +// to guard. Asserting on those objects would pass even if a locale silently dropped the +// label (same trap documented in docsNamespaceParity.spec.ts), so the check has to happen +// before the merge does. +function localeSource(name: string): string { + return readFileSync(new URL(`../locales/${name}.ts`, import.meta.url), "utf8"); +} + +function declaredString(source: string, key: string): string | undefined { + return source.match(new RegExp(String.raw`^\s*${key}:\s*"((?:[^"\\]|\\.)*)",\s*$`, "m"))?.[1]; +} + +// The locales that carry the connection.mqtt* form cluster (led by mqttKeepAlive). +const localesCarryingMqttCluster = ["en", "zh-CN", "zh-TW", "ja", "it", "es", "pt-BR", "tr", "az"]; +// Every locale FILE must declare the key in source. This is the assertion that actually +// guards drift: the imported modules cannot show a removed source key, because +// withEnglishFallback deep-merges english underneath at import time (see header). +const allLocaleFiles = [...localesCarryingMqttCluster, "ko"]; + +describe("mqtt max packet size label i18n parity", () => { + it("english declares the label with its byte unit", () => { + expect(declaredString(localeSource("en"), "mqttMaxPacketSize")).toContain("bytes"); + }); + + it.each(allLocaleFiles)("%s declares the label in its source file", (name) => { + const label = declaredString(localeSource(name), "mqttMaxPacketSize"); + expect(label, `${name}.ts must declare connection.mqttMaxPacketSize`).toBeTruthy(); + expect(label?.trim()).not.toBe(""); + }); + + it.each(localesCarryingMqttCluster)("%s declares the label alongside its mqtt cluster", (name) => { + const source = localeSource(name); + // Anchor: only locales that already declare the cluster get the new key. + expect(declaredString(source, "mqttKeepAlive"), `${name}.ts must declare connection.mqttKeepAlive`).toBeTruthy(); + // Semantic-cluster anchoring (i18n-locales.md): the label belongs next to mqttConnectTimeout. + expect(source.indexOf("mqttMaxPacketSize:"), `${name}.ts must anchor the key after mqttConnectTimeout`).toBeGreaterThan(source.indexOf("mqttConnectTimeout:")); + }); + + it("ko declares the label without taking on the rest of the mqtt form cluster", () => { + const source = localeSource("ko"); + // ko translates a few mqtt console strings but none of the form fields, so the form + // renders in english there via withEnglishFallback. This one label is the deliberate + // exception: it is declared in source so the Korean text exists in the file, and it is + // the only form key ko owns — do not grow the cluster here. + expect(declaredString(source, "mqttMaxPacketSize")).toBeTruthy(); + expect(declaredString(source, "mqttKeepAlive")).toBeUndefined(); + expect(declaredString(source, "mqttConnectTimeout")).toBeUndefined(); + }); +}); + +// Layer (a) from the PRD: the label must also resolve through vue-i18n at runtime. This is +// weaker than the source-level check above — the deep-merge hands a locale that drops the +// key the english text, so it cannot see drift — but it is what catches a locale module +// that fails to declare or parse the key at all, and the "key path leaks into the UI" +// failure mode that a missing translation produces. +const localeModules: Array<[string, Record]> = [ + ["az", az], + ["en", en], + ["es", es], + ["it", itLocale], + ["ja", ja], + ["ko", ko], + ["pt-BR", ptBR], + ["tr", tr], + ["zh-CN", zhCN], + ["zh-TW", zhTW], +]; + +describe("mqtt max packet size label resolution", () => { + it.each(localeModules)("%s resolves the label to a translated string", (name, messages) => { + const connection = (messages.connection ?? {}) as typeof en.connection; + const i18n = createI18n({ legacy: false, locale: name, messages: { [name]: { connection } } }); + const label = i18n.global.t("connection.mqttMaxPacketSize"); + expect(label).not.toBe("connection.mqttMaxPacketSize"); + expect(label.trim()).not.toBe(""); + i18n.dispose(); + }); +}); + +describe("connection dialog mqtt label call site", () => { + const componentSource = readFileSync(new URL("../../components/connection/ConnectionDialog.vue", import.meta.url), "utf8"); + + it("renders the label through vue-i18n instead of a hardcoded chinese string", () => { + expect(componentSource).toContain('{{ t("connection.mqttMaxPacketSize") }}'); + expect(componentSource).not.toContain("最大报文"); + }); +}); diff --git a/apps/desktop/src/i18n/locales/az.ts b/apps/desktop/src/i18n/locales/az.ts index 095742ad36..a85f8cbb19 100644 --- a/apps/desktop/src/i18n/locales/az.ts +++ b/apps/desktop/src/i18n/locales/az.ts @@ -950,6 +950,7 @@ export default withEnglishFallback({ mqttWsPath: "WebSocket yolu", mqttWsPathPlaceholder: "/mqtt", mqttConnectTimeout: "Əlaqə üçün vaxt limiti (saniyə)", + mqttMaxPacketSize: "Maksimal paket ölçüsü (bayt)", mqttConsoleTitle: "MQTT konsolu", mqttTopics: "Mövzular", mqttMessages: "Mesajlar", diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index fdf48dd9d7..3ef46e82be 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -1118,6 +1118,7 @@ export default { mqttWsPath: "WebSocket Path", mqttWsPathPlaceholder: "/mqtt", mqttConnectTimeout: "Connect Timeout (seconds)", + mqttMaxPacketSize: "Max Packet Size (bytes)", mqttConsoleTitle: "MQTT Console", mqttTopics: "Topics", mqttMessages: "Messages", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 4ba5d9863a..46ea234a9e 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -982,6 +982,7 @@ export default withEnglishFallback({ mqttWsPath: "WebSocket Path", mqttWsPathPlaceholder: "/mqtt", mqttConnectTimeout: "Connect Timeout (seconds)", + mqttMaxPacketSize: "Max Packet Size (bytes)", nacosMetrics: "Métricas Prometheus", nacosMetricsAuto: "Detección automática", nacosMetricsDisabled: "Desactivado", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index cb69619c93..5a2ecb9ef1 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -980,6 +980,7 @@ export default withEnglishFallback({ mqttWsPath: "WebSocket Path", mqttWsPathPlaceholder: "/mqtt", mqttConnectTimeout: "Connect Timeout (seconds)", + mqttMaxPacketSize: "Max Packet Size (bytes)", nacosMetrics: "Metriche Prometheus", nacosMetricsAuto: "Rilevamento automatico", nacosMetricsDisabled: "Disabilitato", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 17bb8af84a..3a566e2ea8 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -946,6 +946,7 @@ export default withEnglishFallback({ mqttWsPath: "WebSocket Path", mqttWsPathPlaceholder: "/mqtt", mqttConnectTimeout: "Connect Timeout (seconds)", + mqttMaxPacketSize: "Max Packet Size (bytes)", nacosMetrics: "Prometheus メトリクス", nacosMetricsAuto: "自動検出", nacosMetricsDisabled: "無効", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index 6197274aff..6aa0f1d4fa 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -1400,6 +1400,7 @@ export default withEnglishFallback({ pluginSshKeyPickerEmpty: "키를 찾을 수 없음", pluginSshKeyEncrypted: "암호화됨", objectCacheRefreshFailed: "개체 캐시 새로 고침 실패: {message}", + mqttMaxPacketSize: "최대 패킷 크기(바이트)", }, editor: { duckdbDraining: "이전 DuckDB 쿼리가 아직 중지 중입니다. 잠시 후 다시 시도해 주세요.", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 28530025c5..30b2068119 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -981,6 +981,7 @@ export default withEnglishFallback({ mqttWsPath: "WebSocket Path", mqttWsPathPlaceholder: "/mqtt", mqttConnectTimeout: "Connect Timeout (seconds)", + mqttMaxPacketSize: "Max Packet Size (bytes)", nacosMetrics: "Métricas Prometheus", nacosMetricsAuto: "Detecção automática", nacosMetricsDisabled: "Desativado", diff --git a/apps/desktop/src/i18n/locales/tr.ts b/apps/desktop/src/i18n/locales/tr.ts index ded4a0936b..79480ea50d 100644 --- a/apps/desktop/src/i18n/locales/tr.ts +++ b/apps/desktop/src/i18n/locales/tr.ts @@ -952,6 +952,7 @@ export default withEnglishFallback({ mqttWsPath: "WebSocket Yolu", mqttWsPathPlaceholder: "/mqtt", mqttConnectTimeout: "Bağlantı Zaman Aşımı (saniye)", + mqttMaxPacketSize: "Maksimum Paket Boyutu (bayt)", mqttConsoleTitle: "MQTT Konsolu", mqttTopics: "Konular", mqttMessages: "Mesajlar", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index fdcc45efba..4a38456c8d 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -1032,6 +1032,7 @@ export default withEnglishFallback({ mqttWsPath: "WebSocket 路径", mqttWsPathPlaceholder: "/mqtt", mqttConnectTimeout: "连接超时(秒)", + mqttMaxPacketSize: "最大报文(字节)", mqttConsoleTitle: "MQTT 控制台", mqttTopics: "Topics", mqttMessages: "消息", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 8f85bf60e1..83f912d1a8 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -982,6 +982,7 @@ export default withEnglishFallback({ mqttWsPath: "WebSocket Path", mqttWsPathPlaceholder: "/mqtt", mqttConnectTimeout: "Connect Timeout (seconds)", + mqttMaxPacketSize: "Max Packet Size (bytes)", nacosMetrics: "Prometheus 指標", nacosMetricsAuto: "自動偵測", nacosMetricsDisabled: "關閉",