diff --git a/frontend/src/pages/xray/routing/RuleCardList.tsx b/frontend/src/pages/xray/routing/RuleCardList.tsx
index ffa43c267..50f2e925c 100644
--- a/frontend/src/pages/xray/routing/RuleCardList.tsx
+++ b/frontend/src/pages/xray/routing/RuleCardList.tsx
@@ -152,9 +152,7 @@ export default function RuleCardList({
→
- {rule.balancerTag
- ? t('pages.xray.balancer') || 'Balancer'
- : t('pages.xray.Outbounds')}
+ {rule.balancerTag ? t('pages.xray.rules.balancer') : t('pages.xray.Outbounds')}
{rule.outboundTag ? (
diff --git a/frontend/src/test/i18n-dead-keys.test.ts b/frontend/src/test/i18n-dead-keys.test.ts
index a8b595f64..002849c57 100644
--- a/frontend/src/test/i18n-dead-keys.test.ts
+++ b/frontend/src/test/i18n-dead-keys.test.ts
@@ -5,7 +5,7 @@ import { join, resolve } from 'node:path';
import { describe, expect, it } from 'vitest';
/*
- * Guards the 14-locale translation set two ways: every key in en-US must be
+ * Guards the 14-locale translation set: every key in en-US must be
* referenced somewhere in the frontend or Go sources (dead keys accumulate
* silently — this test deleted over two hundred of them when it was
* introduced), and every locale must carry exactly the en-US key set
@@ -82,6 +82,17 @@ describe('i18n keys', () => {
);
});
+ it('no literal t() key resolves to an object', () => {
+ const objectKeys = Array.from(
+ blob.matchAll(/\bt\(\s*(['"`])([\w.-]+)\1\s*[,)]/g),
+ (m) => m[2],
+ ).filter((key) => enKeys.some((leaf) => leaf.startsWith(`${key}.`)));
+ expect(
+ objectKeys,
+ `t() keys naming a group, rendered as "key '…' returned an object instead of string.":\n ${objectKeys.join('\n ')}`,
+ ).toEqual([]);
+ });
+
it('every locale carries exactly the en-US key set', () => {
const enSet = new Set(enKeys);
for (const file of readdirSync(translationDir)) {