This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new f0a578a75 test(web): fail when a translation key is missing or
incomplete (#4809)
f0a578a75 is described below
commit f0a578a75c651eed93678926f56369200fb61cf2
Author: Wang1rrr <[email protected]>
AuthorDate: Thu Sep 24 17:44:28 2026 +0800
test(web): fail when a translation key is missing or incomplete (#4809)
test(web): fail when a translation key is missing or incomplete
t() returns the key itself when the dictionary has no entry, so a
misspelled, renamed or
dropped key renders as raw text such as "audit.exportFailed" in the console
while the whole
suite still passes. Nothing in the type system connects a t('...') call to
an entry, because
the dictionary is keyed by string.
Add the two invariants that make that failure loud:
- every entry carries a non-empty zh and en value, so a half-added key
cannot ship a blank
label in one locale
- every literal key that any non-test source hands to t() resolves. The
sources are scanned
as text rather than walked as modules, so branches no test renders are
covered too.
Template keys are out of scope for a static scan; the composed families
they draw from are
already covered by theme.test.ts and the audit presentation lookups.
Both cases were verified to fail when broken: pointing a probe file at
'definitely.not.a.real.key' fails the second, and blanking the en value of
common.close
fails the first.
The scan currently covers 1881 literal t() call sites across 183 non-test
source files
against 2177 dictionary entries, and all of them resolve.
---
web/src/i18n/__tests__/translations.test.ts | 65 +++++++++++++++++++++++++++++
1 file changed, 65 insertions(+)
diff --git a/web/src/i18n/__tests__/translations.test.ts
b/web/src/i18n/__tests__/translations.test.ts
new file mode 100644
index 000000000..5f3c1988d
--- /dev/null
+++ b/web/src/i18n/__tests__/translations.test.ts
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { describe, expect, it } from 'vitest';
+import translations from '../translations';
+
+/**
+ * The dictionary is looked up by string, so nothing in the type system
connects a `t('...')`
+ * call to an entry. A key that is misspelled, renamed or dropped during a
refactor renders as
+ * the raw key - `t` falls back to returning it - and the whole suite still
passes. These two
+ * invariants are what makes that failure loud.
+ *
+ * The sources are read as text rather than walked as modules because the
point is to see the
+ * keys a file *asks for*, including in branches no test renders.
+ */
+const sources = import.meta.glob('../../**/*.{ts,tsx}', {
+ query: '?raw',
+ import: 'default',
+ eager: true,
+}) as Record<string, string>;
+
+/** A literal key handed to `t`. Template keys such as ``
t(`audit.op.${code}`) `` are composed
+ * from values that only exist at runtime, so they are out of scope for a
static scan. */
+const LITERAL_KEY = /\bt\(\s*'([^']+)'/g;
+
+describe('translations dictionary', () => {
+ it('gives every entry a non-empty Chinese and an English value', () => {
+ const incomplete = Object.entries(translations)
+ .filter(([, value]) => !value.zh.trim() || !value.en.trim())
+ .map(([key]) => key);
+
+ expect(incomplete, 'entries missing a locale').toEqual([]);
+ });
+
+ it('resolves every literal key the sources ask t() for', () => {
+ const missing = new Map<string, string[]>();
+
+ for (const [path, source] of Object.entries(sources)) {
+ // Test files mock the language context and invent keys on purpose.
+ if (path.includes('.test.')) continue;
+ for (const match of source.matchAll(LITERAL_KEY)) {
+ const key = match[1];
+ if (key in translations) continue;
+ missing.set(key, [...(missing.get(key) ?? []), path]);
+ }
+ }
+
+ const detail = [...missing].map(([key, paths]) => `${key} <-
${paths.join(', ')}`);
+ expect(detail, 'literal keys with no dictionary entry').toEqual([]);
+ });
+});