@celestia-island/hikari 0.55.32 → 0.55.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/i18n/usageSurface.test.ts +66 -13
package/package.json
CHANGED
|
@@ -24,6 +24,13 @@ import { describe, expect, it } from "vitest";
|
|
|
24
24
|
* belong to the HOST-app namespace (registered at runtime via
|
|
25
25
|
* mergeMessages, or passed in as props.t) and are pinned by their own
|
|
26
26
|
* suites (timeKeys.test.ts & co.) — out of scope here by construction.
|
|
27
|
+
*
|
|
28
|
+
* Interpolated keys cannot be pinned by literal, so their CONSTRUCTION
|
|
29
|
+
* SITES are scanned wherever the backtick template appears — adjacency to
|
|
30
|
+
* t() is NOT required (F1: HkStatusBar.tsx:233 builds the key into a
|
|
31
|
+
* variable first) — and each discovered family is whitelisted and, when
|
|
32
|
+
* its enum is closed inside hikari, pinned key-by-key (see
|
|
33
|
+
* CONSTRUCTED_FAMILY_KEYS).
|
|
27
34
|
*/
|
|
28
35
|
const modules = import.meta.glob<{ default: Record<string, unknown> }>(
|
|
29
36
|
"./locales/*/*.json",
|
|
@@ -75,18 +82,23 @@ function walkSources(dir: string, acc: string[] = []): string[] {
|
|
|
75
82
|
* `t(` and the key; the character class covers ", ', and ` so a template
|
|
76
83
|
* literal WITHOUT interpolation is still a pinned literal. */
|
|
77
84
|
const STATIC_USE_RE = /\bt\(\s*(["'`])hikari::[A-Za-z0-9_.]+\1/g;
|
|
78
|
-
/**
|
|
79
|
-
* resolved statically — they are enumerated in
|
|
80
|
-
|
|
85
|
+
/** Constructed key families: a template key with `${…}` interpolation can
|
|
86
|
+
* never be resolved statically — they are enumerated in
|
|
87
|
+
* DYNAMIC_KEY_FAMILIES. The regex matches the template construction
|
|
88
|
+
* ANYWHERE in source, NOT only adjacent to `t(`: F1 (verify-hk603,
|
|
89
|
+
* 2026-09-19) — HkStatusBar.tsx builds `hikari::statusBar.tier.${tier}`
|
|
90
|
+
* into a variable and only calls t(tierLabelKey) 135 lines later, so the
|
|
91
|
+
* old adjacency-only regex never saw the family at all. */
|
|
92
|
+
const CONSTRUCTED_KEY_RE = /`hikari::[A-Za-z0-9_.]*\$\{/g;
|
|
81
93
|
|
|
82
94
|
interface Site {
|
|
83
95
|
key: string;
|
|
84
96
|
where: string;
|
|
85
97
|
}
|
|
86
98
|
|
|
87
|
-
function scanSources(): { statics: Site[];
|
|
99
|
+
function scanSources(): { statics: Site[]; constructed: Site[] } {
|
|
88
100
|
const statics: Site[] = [];
|
|
89
|
-
const
|
|
101
|
+
const constructed: Site[] = [];
|
|
90
102
|
for (const path of walkSources(SRC_ROOT)) {
|
|
91
103
|
const text = readFileSync(path, "utf8");
|
|
92
104
|
const rel = path.slice(SRC_ROOT.length + 1);
|
|
@@ -94,13 +106,14 @@ function scanSources(): { statics: Site[]; dynamics: string[] } {
|
|
|
94
106
|
const key = match[0].slice(match[0].indexOf("hikari::"), -1);
|
|
95
107
|
statics.push({ key, where: `${rel}:${text.slice(0, match.index).split("\n").length}` });
|
|
96
108
|
}
|
|
97
|
-
for (const match of text.matchAll(
|
|
109
|
+
for (const match of text.matchAll(CONSTRUCTED_KEY_RE)) {
|
|
98
110
|
// The literal portion before "${" — e.g. "hikari::context." — which
|
|
99
111
|
// is the family prefix the whitelist is keyed by.
|
|
100
|
-
|
|
112
|
+
const family = match[0].slice(1, -2);
|
|
113
|
+
constructed.push({ key: family, where: `${rel}:${text.slice(0, match.index).split("\n").length}` });
|
|
101
114
|
}
|
|
102
115
|
}
|
|
103
|
-
return { statics,
|
|
116
|
+
return { statics, constructed };
|
|
104
117
|
}
|
|
105
118
|
|
|
106
119
|
/** Interpolated key families that intentionally escape the static scan,
|
|
@@ -112,13 +125,25 @@ const DYNAMIC_KEY_FAMILIES: Record<string, string> = {
|
|
|
112
125
|
"hikari::context.": "HkContextRing section keys — closed enum, pinned by name in contextKeys.test.ts.",
|
|
113
126
|
"hikari::statusBar.backend.": "HkConnectionStatus — enumerated connection-state suffixes.",
|
|
114
127
|
"hikari::statusBar.region.": "HkStatusBar region names — OPTIONAL per-locale overrides over Intl.DisplayNames (see LOCALE_SPECIFIC_NESTED in flatKeyParity.test.ts).",
|
|
128
|
+
"hikari::statusBar.tier.": "HkStatusBar transport tiers — constructed into a variable at a distance from its t() call (F1); the four concrete ids are pinned by name in CONSTRUCTED_FAMILY_KEYS below.",
|
|
115
129
|
"hikari::theme.groups.": "Registry type: group/slot ids are registered by downstream apps; t() resolves via a resolveLocalizedText fallback, never a hikari bundle.",
|
|
116
130
|
"hikari::theme.tokens.": "HkColorSchemeEditor token enum — closed set defined in platform.json locales.",
|
|
117
131
|
};
|
|
118
132
|
|
|
133
|
+
/** Concrete suffixes each constructed family resolves to, for families whose
|
|
134
|
+
* enum is closed INSIDE hikari (the whitelist reason records which). Each
|
|
135
|
+
* listed key is asserted to exist, non-empty, in every locale — that is
|
|
136
|
+
* the tooth the adjacency regex lacked: deleting the whole family from
|
|
137
|
+
* every bundle used to leave every audit pin green (F1 blind spot).
|
|
138
|
+
* Families resolved by downstream apps (theme.groups.) or optional
|
|
139
|
+
* per-locale overrides (statusBar.region.) deliberately stay out. */
|
|
140
|
+
const CONSTRUCTED_FAMILY_KEYS: Record<string, string[]> = {
|
|
141
|
+
"hikari::statusBar.tier.": ["local", "poll", "sse", "ws"],
|
|
142
|
+
};
|
|
143
|
+
|
|
119
144
|
describe("t() usage surface vs locale bundles", () => {
|
|
120
145
|
const universes = new Map(EXPECTED_LOCALES.map((l) => [l, buildLocaleUniverse(l)]));
|
|
121
|
-
const { statics,
|
|
146
|
+
const { statics, constructed } = scanSources();
|
|
122
147
|
|
|
123
148
|
it("covers all 11 locales in the merged bundle universe", () => {
|
|
124
149
|
const found = new Set(
|
|
@@ -144,6 +169,11 @@ describe("t() usage surface vs locale bundles", () => {
|
|
|
144
169
|
expect(firstSiteOf("hikari::filePicker.currentPath")).toMatch(/HkFileBrowserDialog\.tsx/);
|
|
145
170
|
// Split-across-lines call form (t( and the key on different lines):
|
|
146
171
|
expect(firstSiteOf("hikari::phoneInput.searchCountries")).toMatch(/HkPhoneInput\.tsx/);
|
|
172
|
+
// Constructed-key scan positive (F1): the tier template is built into a
|
|
173
|
+
// variable 135 lines away from its t() call — the scan must still see it.
|
|
174
|
+
expect(
|
|
175
|
+
constructed.find((s) => s.key === "hikari::statusBar.tier.")?.where ?? "NOT FOUND",
|
|
176
|
+
).toMatch(/HkStatusBar\.tsx/);
|
|
147
177
|
});
|
|
148
178
|
|
|
149
179
|
it("resolves every statically referenced hikari:: key in every locale, non-empty", () => {
|
|
@@ -165,13 +195,36 @@ describe("t() usage surface vs locale bundles", () => {
|
|
|
165
195
|
expect(violations, "every referenced key must resolve in all 11 locales").toEqual([]);
|
|
166
196
|
});
|
|
167
197
|
|
|
168
|
-
it("enumerates exactly the whitelisted
|
|
169
|
-
const found = [...new Set(
|
|
170
|
-
expect(found, "families
|
|
198
|
+
it("enumerates exactly the whitelisted constructed key families, both directions", () => {
|
|
199
|
+
const found = [...new Set(constructed.map((s) => s.key))].sort();
|
|
200
|
+
expect(found, "families constructed in source but missing from the whitelist").toEqual(
|
|
171
201
|
Object.keys(DYNAMIC_KEY_FAMILIES).sort(),
|
|
172
202
|
);
|
|
173
203
|
for (const family of Object.keys(DYNAMIC_KEY_FAMILIES)) {
|
|
174
|
-
expect(found, `${family} is whitelisted but no longer
|
|
204
|
+
expect(found, `${family} is whitelisted but no longer constructed — prune it`).toContain(family);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("resolves every named concrete key of the constructed families in every locale", () => {
|
|
209
|
+
const violations: string[] = [];
|
|
210
|
+
for (const [family, suffixes] of Object.entries(CONSTRUCTED_FAMILY_KEYS)) {
|
|
211
|
+
expect(
|
|
212
|
+
DYNAMIC_KEY_FAMILIES[family],
|
|
213
|
+
`${family} carries named keys but has no whitelist entry`,
|
|
214
|
+
).toBeDefined();
|
|
215
|
+
for (const suffix of suffixes) {
|
|
216
|
+
const key = family + suffix;
|
|
217
|
+
for (const locale of EXPECTED_LOCALES) {
|
|
218
|
+
const value = universes.get(locale)!.get(key);
|
|
219
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
220
|
+
violations.push(`${key} is missing or empty in ${locale}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
175
224
|
}
|
|
225
|
+
expect(
|
|
226
|
+
violations,
|
|
227
|
+
"constructed families' concrete keys must resolve in all 11 locales",
|
|
228
|
+
).toEqual([]);
|
|
176
229
|
});
|
|
177
230
|
});
|