fix(mqtt): localize max packet size label in connection dialog
This commit is contained in:
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("最大报文");
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "無効",
|
||||
|
||||
@@ -1400,6 +1400,7 @@ export default withEnglishFallback({
|
||||
pluginSshKeyPickerEmpty: "키를 찾을 수 없음",
|
||||
pluginSshKeyEncrypted: "암호화됨",
|
||||
objectCacheRefreshFailed: "개체 캐시 새로 고침 실패: {message}",
|
||||
mqttMaxPacketSize: "최대 패킷 크기(바이트)",
|
||||
},
|
||||
editor: {
|
||||
duckdbDraining: "이전 DuckDB 쿼리가 아직 중지 중입니다. 잠시 후 다시 시도해 주세요.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1032,6 +1032,7 @@ export default withEnglishFallback({
|
||||
mqttWsPath: "WebSocket 路径",
|
||||
mqttWsPathPlaceholder: "/mqtt",
|
||||
mqttConnectTimeout: "连接超时(秒)",
|
||||
mqttMaxPacketSize: "最大报文(字节)",
|
||||
mqttConsoleTitle: "MQTT 控制台",
|
||||
mqttTopics: "Topics",
|
||||
mqttMessages: "消息",
|
||||
|
||||
@@ -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: "關閉",
|
||||
|
||||
Reference in new issue
Block a user