fix(mqtt): localize max packet size label in connection dialog

This commit is contained in:
Abeautifulsnow authored and GitHub committed 2026-09-22 10:57:00 +08:00
1 parent f8d7b57c19
commit b7152a29cd
12 files changed
+115 -1

No files matched your search

@@ -7798,7 +7798,7 @@ function openExternalUrl(url: string) {
<Input v-model.number="mqttConnectTimeoutSecs" type="number" class="col-span-3 w-32" min="1" max="300" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelClass">最大报文(字节)</Label>
<Label :class="connectionLabelClass">{{ t("connection.mqttMaxPacketSize") }}</Label>
<Input v-model.number="mqttMaxPacketSizeBytes" type="number" class="col-span-3 w-40" min="1024" max="268435455" />
</div>
</template>
@@ -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<string, unknown>]> = [
["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("最大报文");
});
});
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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: "無効",
+1
View File
@@ -1400,6 +1400,7 @@ export default withEnglishFallback({
pluginSshKeyPickerEmpty: "키를 찾을 수 없음",
pluginSshKeyEncrypted: "암호화됨",
objectCacheRefreshFailed: "개체 캐시 새로 고침 실패: {message}",
mqttMaxPacketSize: "최대 패킷 크기(바이트)",
},
editor: {
duckdbDraining: "이전 DuckDB 쿼리가 아직 중지 중입니다. 잠시 후 다시 시도해 주세요.",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -1032,6 +1032,7 @@ export default withEnglishFallback({
mqttWsPath: "WebSocket 路径",
mqttWsPathPlaceholder: "/mqtt",
mqttConnectTimeout: "连接超时(秒)",
mqttMaxPacketSize: "最大报文(字节)",
mqttConsoleTitle: "MQTT 控制台",
mqttTopics: "Topics",
mqttMessages: "消息",
+1
View File
@@ -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: "關閉",