enterprise-ui-architect-cli 1.1.0 → 2.0.0
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/assets/SKILL.md +53 -0
- package/assets/commands/verify-i18n.ts +213 -0
- package/assets/commands/verify-imports.ts +68 -21
- package/assets/data/anti-patterns.csv +3 -1
- package/assets/data/charts.csv +1 -1
- package/assets/data/industries.csv +5 -5
- package/assets/data/pre-delivery-checklist.csv +3 -0
- package/assets/data/review-rubric.csv +2 -2
- package/assets/scripts/search.py +21 -9
- package/assets/src/index.ts +17 -2
- package/assets/templates/base/quick-reference.md +18 -0
- package/dist/commands/verify-i18n.d.ts +7 -0
- package/dist/commands/verify-i18n.d.ts.map +1 -0
- package/dist/commands/verify-i18n.js +175 -0
- package/dist/commands/verify-i18n.js.map +1 -0
- package/dist/commands/verify-imports.d.ts.map +1 -1
- package/dist/commands/verify-imports.js +66 -18
- package/dist/commands/verify-imports.js.map +1 -1
- package/dist/index.js +17 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/assets/SKILL.md
CHANGED
|
@@ -557,6 +557,59 @@ After install:
|
|
|
557
557
|
- Using different package managers within the same repo
|
|
558
558
|
- Adding unused dependencies "just in case"
|
|
559
559
|
|
|
560
|
+
## Translation Discipline (i18n)
|
|
561
|
+
|
|
562
|
+
When building multi-language admin dashboards with `next-intl`, `react-i18next`, or similar:
|
|
563
|
+
|
|
564
|
+
### 1. Always Use `t()` for User-Facing Text
|
|
565
|
+
Every string visible to users must go through the translation function. No hardcoded labels, buttons, placeholders, or error messages.
|
|
566
|
+
|
|
567
|
+
```tsx
|
|
568
|
+
// ✅ Correct
|
|
569
|
+
<Button>{t("form.save")}</Button>
|
|
570
|
+
<CustomTextField label={t("auth.username")} />
|
|
571
|
+
|
|
572
|
+
// ❌ Wrong
|
|
573
|
+
<Button>Save</Button>
|
|
574
|
+
<CustomTextField label="Username" />
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
### 2. Add Keys to All Locale Files
|
|
578
|
+
When you introduce a new `t("key")` call, you **must** add that key to **every** `messages/*.json` file before finishing.
|
|
579
|
+
|
|
580
|
+
```bash
|
|
581
|
+
# Verify all keys exist in every locale
|
|
582
|
+
enterprise-ui verify-i18n --src ./src
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
### 3. Namespace Convention
|
|
586
|
+
Use `useTranslations("namespace")` for page-scoped keys. This keeps locale files organized and prevents key collisions.
|
|
587
|
+
|
|
588
|
+
```tsx
|
|
589
|
+
const t = useTranslations("admin.users");
|
|
590
|
+
// keys become: admin.users.title, admin.users.column.name, etc.
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
### 4. Dynamic Keys
|
|
594
|
+
Avoid dynamic template literals for translation keys when possible:
|
|
595
|
+
|
|
596
|
+
```tsx
|
|
597
|
+
// ✅ Prefer explicit mapping
|
|
598
|
+
const statusKey = status === "active" ? "status.active" : "status.inactive";
|
|
599
|
+
<Chip label={t(statusKey)} />
|
|
600
|
+
|
|
601
|
+
// ❌ Avoid
|
|
602
|
+
<Chip label={t(`status.${status}`)} />
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
If dynamic keys are unavoidable, ensure all possible values are documented and present in locale files.
|
|
606
|
+
|
|
607
|
+
### 5. Pre-Delivery Check
|
|
608
|
+
Before marking a feature complete:
|
|
609
|
+
- [ ] All `t()` keys exist in every locale file
|
|
610
|
+
- [ ] No hardcoded user-facing strings remain
|
|
611
|
+
- [ ] `enterprise-ui verify-i18n` passes with zero missing keys
|
|
612
|
+
|
|
560
613
|
## Keyboard Navigation
|
|
561
614
|
MUI components require specific keyboard patterns:
|
|
562
615
|
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { readFileSync, existsSync, readdirSync } from "fs";
|
|
2
|
+
import { resolve, dirname, join, sep, extname } from "path";
|
|
3
|
+
|
|
4
|
+
interface VerifyOptions {
|
|
5
|
+
srcDir: string;
|
|
6
|
+
messagesDir: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface KeyLocation {
|
|
10
|
+
key: string;
|
|
11
|
+
file: string;
|
|
12
|
+
line: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function findMessagesDir(startDir: string): string | null {
|
|
16
|
+
let dir = resolve(startDir);
|
|
17
|
+
while (dir !== dirname(dir)) {
|
|
18
|
+
const messagesPath = join(dir, "messages");
|
|
19
|
+
if (existsSync(messagesPath)) return messagesPath;
|
|
20
|
+
dir = dirname(dir);
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getSourceFiles(dir: string): string[] {
|
|
26
|
+
const results: string[] = [];
|
|
27
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
28
|
+
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
const fullPath = join(dir, entry.name);
|
|
31
|
+
if (entry.isDirectory()) {
|
|
32
|
+
if (
|
|
33
|
+
entry.name === "node_modules" ||
|
|
34
|
+
entry.name === "dist" ||
|
|
35
|
+
entry.name === "build" ||
|
|
36
|
+
entry.name === ".next" ||
|
|
37
|
+
entry.name.startsWith(".")
|
|
38
|
+
) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
results.push(...getSourceFiles(fullPath));
|
|
42
|
+
} else if (entry.isFile()) {
|
|
43
|
+
const ext = extname(entry.name);
|
|
44
|
+
if (ext === ".ts" || ext === ".tsx" || ext === ".js" || ext === ".jsx") {
|
|
45
|
+
results.push(fullPath);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return results;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function loadTranslations(messagesDir: string): Record<string, Record<string, unknown>> {
|
|
54
|
+
const files = readdirSync(messagesDir).filter((f) => f.endsWith(".json"));
|
|
55
|
+
const translations: Record<string, Record<string, unknown>> = {};
|
|
56
|
+
|
|
57
|
+
for (const file of files) {
|
|
58
|
+
const locale = file.replace(".json", "");
|
|
59
|
+
const content = readFileSync(join(messagesDir, file), "utf-8");
|
|
60
|
+
translations[locale] = JSON.parse(content);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return translations;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
|
|
67
|
+
const parts = path.split(".");
|
|
68
|
+
let current: unknown = obj;
|
|
69
|
+
|
|
70
|
+
for (const part of parts) {
|
|
71
|
+
if (current === null || current === undefined) return undefined;
|
|
72
|
+
if (typeof current !== "object") return undefined;
|
|
73
|
+
current = (current as Record<string, unknown>)[part];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return current;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function extractNamespace(content: string): string | null {
|
|
80
|
+
const match = /useTranslations\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/.exec(content);
|
|
81
|
+
return match ? match[1] : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isInsideStringLiteral(line: string, matchIndex: number): boolean {
|
|
85
|
+
// Check if the character before t( is inside a string literal
|
|
86
|
+
const before = line.slice(0, matchIndex);
|
|
87
|
+
let inSingle = false;
|
|
88
|
+
let inDouble = false;
|
|
89
|
+
let escaped = false;
|
|
90
|
+
|
|
91
|
+
for (const ch of before) {
|
|
92
|
+
if (escaped) {
|
|
93
|
+
escaped = false;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (ch === "\\") {
|
|
97
|
+
escaped = true;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (ch === '"' && !inSingle) {
|
|
101
|
+
inDouble = !inDouble;
|
|
102
|
+
} else if (ch === "'" && !inDouble) {
|
|
103
|
+
inSingle = !inSingle;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return inSingle || inDouble;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function extractTranslationKeys(content: string, namespace: string | null): KeyLocation[] {
|
|
111
|
+
const lines = content.split("\n");
|
|
112
|
+
const keys: KeyLocation[] = [];
|
|
113
|
+
const seen = new Set<string>();
|
|
114
|
+
|
|
115
|
+
const regex = /\bt\s*\(\s*["'`]([a-zA-Z0-9_.-]+)["'`]/g;
|
|
116
|
+
|
|
117
|
+
for (let i = 0; i < lines.length; i++) {
|
|
118
|
+
const line = lines[i];
|
|
119
|
+
// Skip comment-only lines
|
|
120
|
+
const codePart = line.split("//")[0];
|
|
121
|
+
if (!codePart.includes("t(")) continue;
|
|
122
|
+
|
|
123
|
+
let match: RegExpExecArray | null;
|
|
124
|
+
while ((match = regex.exec(line)) !== null) {
|
|
125
|
+
if (isInsideStringLiteral(line, match.index)) continue;
|
|
126
|
+
|
|
127
|
+
const rawKey = match[1];
|
|
128
|
+
const key = namespace ? `${namespace}.${rawKey}` : rawKey;
|
|
129
|
+
if (!seen.has(key)) {
|
|
130
|
+
seen.add(key);
|
|
131
|
+
keys.push({ key, file: "", line: i + 1 });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return keys;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function verifyI18nCommand(options: Partial<VerifyOptions> = {}): void {
|
|
140
|
+
const srcDir = options.srcDir || process.cwd();
|
|
141
|
+
const messagesDir = options.messagesDir || findMessagesDir(srcDir);
|
|
142
|
+
|
|
143
|
+
if (!messagesDir) {
|
|
144
|
+
console.error("❌ messages/ directory not found. Run this from a project root.");
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const translations = loadTranslations(messagesDir);
|
|
149
|
+
const locales = Object.keys(translations);
|
|
150
|
+
|
|
151
|
+
if (locales.length === 0) {
|
|
152
|
+
console.error("❌ No translation files found in messages/ directory.");
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const files = getSourceFiles(srcDir);
|
|
157
|
+
const allKeys: Map<string, Array<{ file: string; line: number }>> = new Map();
|
|
158
|
+
|
|
159
|
+
for (const file of files) {
|
|
160
|
+
const content = readFileSync(file, "utf-8");
|
|
161
|
+
const namespace = extractNamespace(content);
|
|
162
|
+
const keys = extractTranslationKeys(content, namespace);
|
|
163
|
+
for (const { key, line } of keys) {
|
|
164
|
+
const list = allKeys.get(key) || [];
|
|
165
|
+
list.push({ file, line });
|
|
166
|
+
allKeys.set(key, list);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (allKeys.size === 0) {
|
|
171
|
+
console.log("ℹ️ No translation keys found in source files.");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const missingByLocale: Map<string, KeyLocation[]> = new Map();
|
|
176
|
+
|
|
177
|
+
for (const [key, locations] of allKeys) {
|
|
178
|
+
for (const locale of locales) {
|
|
179
|
+
const value = getNestedValue(translations[locale], key);
|
|
180
|
+
if (value === undefined) {
|
|
181
|
+
const list = missingByLocale.get(locale) || [];
|
|
182
|
+
list.push({ key, file: locations[0].file, line: locations[0].line });
|
|
183
|
+
missingByLocale.set(locale, list);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const totalMissing = Array.from(missingByLocale.values()).reduce((sum, list) => sum + list.length, 0);
|
|
189
|
+
|
|
190
|
+
if (totalMissing === 0) {
|
|
191
|
+
console.log(`✅ All ${allKeys.size} translation key(s) exist in every locale (${locales.join(", ")}).`);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
console.log(`⚠️ Found ${totalMissing} missing translation key(s):\n`);
|
|
196
|
+
|
|
197
|
+
for (const [locale, missing] of missingByLocale) {
|
|
198
|
+
if (missing.length === 0) continue;
|
|
199
|
+
console.log(` 🌐 ${locale}.json (${missing.length} missing):`);
|
|
200
|
+
for (const { key, file, line } of missing.slice(0, 10)) {
|
|
201
|
+
const relPath = file.replace(srcDir + sep, "").replace(/^\//, "");
|
|
202
|
+
console.log(` - ${key}`);
|
|
203
|
+
console.log(` used in: ${relPath}:${line}`);
|
|
204
|
+
}
|
|
205
|
+
if (missing.length > 10) {
|
|
206
|
+
console.log(` ... and ${missing.length - 10} more`);
|
|
207
|
+
}
|
|
208
|
+
console.log("");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
console.log(`Add the missing keys to all ${locales.length} locale files.\n`);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { readFileSync, existsSync, readdirSync
|
|
2
|
-
import { resolve, dirname, join, extname } from "path";
|
|
1
|
+
import { readFileSync, existsSync, readdirSync } from "fs";
|
|
2
|
+
import { resolve, dirname, join, sep, extname } from "path";
|
|
3
3
|
|
|
4
4
|
interface VerifyOptions {
|
|
5
5
|
srcDir: string;
|
|
@@ -12,6 +12,16 @@ interface ImportInfo {
|
|
|
12
12
|
source: string;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
const NODE_BUILTINS = new Set([
|
|
16
|
+
"assert", "async_hooks", "buffer", "child_process", "cluster", "console",
|
|
17
|
+
"constants", "crypto", "dgram", "diagnostics_channel", "dns", "domain",
|
|
18
|
+
"events", "fs", "http", "http2", "https", "inspector", "module", "net",
|
|
19
|
+
"os", "path", "perf_hooks", "process", "punycode", "querystring", "readline",
|
|
20
|
+
"repl", "stream", "string_decoder", "sys", "timers", "timers/promises",
|
|
21
|
+
"tls", "trace_events", "tty", "url", "util", "v8", "vm", "wasi", "worker_threads",
|
|
22
|
+
"zlib", "node:test",
|
|
23
|
+
]);
|
|
24
|
+
|
|
15
25
|
function findPackageJson(startDir: string): string | null {
|
|
16
26
|
let dir = resolve(startDir);
|
|
17
27
|
while (dir !== dirname(dir)) {
|
|
@@ -36,22 +46,34 @@ function getInstalledPackages(packageJsonPath: string): Set<string> {
|
|
|
36
46
|
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
37
47
|
const deps = Object.keys(pkg.dependencies || {});
|
|
38
48
|
const devDeps = Object.keys(pkg.devDependencies || {});
|
|
39
|
-
|
|
49
|
+
const peerDeps = Object.keys(pkg.peerDependencies || {});
|
|
50
|
+
const optionalDeps = Object.keys(pkg.optionalDependencies || {});
|
|
51
|
+
return new Set([...deps, ...devDeps, ...peerDeps, ...optionalDeps]);
|
|
40
52
|
}
|
|
41
53
|
|
|
42
54
|
function getPathAliases(tsConfigPath: string): string[] {
|
|
43
55
|
try {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
56
|
+
const raw = readFileSync(tsConfigPath, "utf-8");
|
|
57
|
+
// Extract path aliases via regex — tolerant to comments and trailing commas
|
|
58
|
+
const aliases: string[] = [];
|
|
59
|
+
// Match patterns like "@core/*" or "@/components/*" inside "paths": { ... }
|
|
60
|
+
const pathsMatch = raw.match(/"paths"\s*:\s*\{([\s\S]*?)\}/);
|
|
61
|
+
if (pathsMatch) {
|
|
62
|
+
const pathsBlock = pathsMatch[1];
|
|
63
|
+
const keyRegex = /"(@[^"]+)"\s*:/g;
|
|
64
|
+
let m: RegExpExecArray | null;
|
|
65
|
+
while ((m = keyRegex.exec(pathsBlock)) !== null) {
|
|
66
|
+
aliases.push(m[1]);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return aliases.length > 0 ? aliases : ["@/*"];
|
|
47
70
|
} catch {
|
|
48
|
-
return [];
|
|
71
|
+
return ["@/*"];
|
|
49
72
|
}
|
|
50
73
|
}
|
|
51
74
|
|
|
52
75
|
function isPathAlias(source: string, aliases: string[]): boolean {
|
|
53
76
|
for (const alias of aliases) {
|
|
54
|
-
// Convert tsconfig glob to regex: @core/* → /^@core\//
|
|
55
77
|
const prefix = alias.replace(/\/\*$/, "");
|
|
56
78
|
if (alias.endsWith("/*")) {
|
|
57
79
|
if (source === prefix || source.startsWith(prefix + "/")) {
|
|
@@ -72,7 +94,6 @@ function getSourceFiles(dir: string): string[] {
|
|
|
72
94
|
|
|
73
95
|
for (const entry of entries) {
|
|
74
96
|
const fullPath = join(dir, entry.name);
|
|
75
|
-
|
|
76
97
|
if (entry.isDirectory()) {
|
|
77
98
|
if (
|
|
78
99
|
entry.name === "node_modules" ||
|
|
@@ -100,13 +121,31 @@ function extractImports(filePath: string): ImportInfo[] {
|
|
|
100
121
|
const lines = content.split("\n");
|
|
101
122
|
const imports: ImportInfo[] = [];
|
|
102
123
|
|
|
103
|
-
|
|
124
|
+
// Matches single-line imports: import ... from "..."
|
|
125
|
+
const singleLineRegex = /^(?:import\s+.*?from\s+|import\s*\(|require\s*\()["']([^"';]+)["'];?/;
|
|
126
|
+
|
|
127
|
+
// Matches multi-line import end: from "..."
|
|
128
|
+
const multiLineEndRegex = /from\s+["']([^"';]+)["'];?/;
|
|
104
129
|
|
|
105
130
|
for (let i = 0; i < lines.length; i++) {
|
|
106
131
|
const line = lines[i].trim();
|
|
107
|
-
const
|
|
108
|
-
if (
|
|
109
|
-
imports.push({ path: filePath, line: i + 1, source:
|
|
132
|
+
const singleMatch = singleLineRegex.exec(line);
|
|
133
|
+
if (singleMatch) {
|
|
134
|
+
imports.push({ path: filePath, line: i + 1, source: singleMatch[1] });
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
// Multi-line import: starts with "import" but no "from" on same line
|
|
138
|
+
if (line.startsWith("import") && !line.includes("from") && !line.includes("(")) {
|
|
139
|
+
// Look ahead up to 5 lines for "from '...'"
|
|
140
|
+
for (let j = i + 1; j < Math.min(i + 6, lines.length); j++) {
|
|
141
|
+
const nextLine = lines[j].trim();
|
|
142
|
+
const multiMatch = multiLineEndRegex.exec(nextLine);
|
|
143
|
+
if (multiMatch) {
|
|
144
|
+
imports.push({ path: filePath, line: j + 1, source: multiMatch[1] });
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
if (nextLine.endsWith(";")) break; // End of import without from
|
|
148
|
+
}
|
|
110
149
|
}
|
|
111
150
|
}
|
|
112
151
|
|
|
@@ -127,7 +166,12 @@ function resolvePackageName(source: string): string | null {
|
|
|
127
166
|
return idx > 0 ? source.slice(0, idx) : source;
|
|
128
167
|
}
|
|
129
168
|
|
|
130
|
-
function
|
|
169
|
+
function isNodeBuiltin(pkg: string): boolean {
|
|
170
|
+
return NODE_BUILTINS.has(pkg) || NODE_BUILTINS.has(`node:${pkg}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function detectPackageManager(lockFiles: string[]): "npm" | "yarn" | "pnpm" | "bun" | "unknown" {
|
|
174
|
+
if (lockFiles.some((f) => f.endsWith("bun.lockb"))) return "bun";
|
|
131
175
|
if (lockFiles.some((f) => f.endsWith("pnpm-lock.yaml"))) return "pnpm";
|
|
132
176
|
if (lockFiles.some((f) => f.endsWith("yarn.lock"))) return "yarn";
|
|
133
177
|
if (lockFiles.some((f) => f.endsWith("package-lock.json"))) return "npm";
|
|
@@ -149,7 +193,7 @@ export function verifyImportsCommand(options: Partial<VerifyOptions> = {}): void
|
|
|
149
193
|
const tsConfigPath = findTsConfig(srcDir);
|
|
150
194
|
const pathAliases = tsConfigPath ? getPathAliases(tsConfigPath) : [];
|
|
151
195
|
|
|
152
|
-
const lockFiles = ["package-lock.json", "yarn.lock", "pnpm-lock.yaml"].map((f) =>
|
|
196
|
+
const lockFiles = ["package-lock.json", "yarn.lock", "pnpm-lock.yaml", "bun.lockb"].map((f) =>
|
|
153
197
|
join(projectRoot, f)
|
|
154
198
|
);
|
|
155
199
|
const existingLocks = lockFiles.filter((f) => existsSync(f));
|
|
@@ -169,6 +213,7 @@ export function verifyImportsCommand(options: Partial<VerifyOptions> = {}): void
|
|
|
169
213
|
|
|
170
214
|
const pkg = resolvePackageName(imp.source);
|
|
171
215
|
if (!pkg) continue;
|
|
216
|
+
if (isNodeBuiltin(pkg)) continue;
|
|
172
217
|
if (installed.has(pkg)) continue;
|
|
173
218
|
|
|
174
219
|
const list = missing.get(pkg) || [];
|
|
@@ -187,7 +232,7 @@ export function verifyImportsCommand(options: Partial<VerifyOptions> = {}): void
|
|
|
187
232
|
console.log(` 📦 ${pkg}`);
|
|
188
233
|
console.log(` Used in:`);
|
|
189
234
|
for (const imp of imports.slice(0, 3)) {
|
|
190
|
-
const relPath = imp.path.replace(projectRoot + "
|
|
235
|
+
const relPath = imp.path.replace(projectRoot + sep, "").replace(/^\//, "");
|
|
191
236
|
console.log(` - ${relPath}:${imp.line} → import from "${imp.source}"`);
|
|
192
237
|
}
|
|
193
238
|
if (imports.length > 3) {
|
|
@@ -195,11 +240,13 @@ export function verifyImportsCommand(options: Partial<VerifyOptions> = {}): void
|
|
|
195
240
|
}
|
|
196
241
|
|
|
197
242
|
const installCmd =
|
|
198
|
-
pkgManager === "
|
|
199
|
-
? `
|
|
200
|
-
: pkgManager === "
|
|
201
|
-
? `
|
|
202
|
-
:
|
|
243
|
+
pkgManager === "bun"
|
|
244
|
+
? `bun add ${pkg}`
|
|
245
|
+
: pkgManager === "pnpm"
|
|
246
|
+
? `pnpm add ${pkg}`
|
|
247
|
+
: pkgManager === "yarn"
|
|
248
|
+
? `yarn add ${pkg}`
|
|
249
|
+
: `npm install ${pkg}`;
|
|
203
250
|
|
|
204
251
|
console.log(` Install: ${installCmd}\n`);
|
|
205
252
|
}
|
|
@@ -53,4 +53,6 @@ id,area,bad_pattern,why_bad,fix
|
|
|
53
53
|
52,Charts,No empty state for charts with zero data points,Empty chart looks broken or crashes chart library,Show empty state illustration or zero baseline when no data
|
|
54
54
|
53,Charts,Using real-time updates without transition animations,Jarring jumps in chart data poor perceived performance,Use Recharts animation or smooth transitions for live data updates
|
|
55
55
|
54,Charts,Fetching all chart data on every small filter change,Excessive API calls slow performance backend overload,Debounce filter changes use TanStack Query staleTime cache filter state in URL
|
|
56
|
-
|
|
56
|
+
55,Dependencies,Adding new imports without verifying the package is installed,Build fails in CI or for teammates TypeScript errors runtime crashes silent failures,Always check package.json before adding an import If package is missing ask user for confirmation then install with the correct package manager
|
|
57
|
+
57,Dependencies,Adding translation keys in source code without updating all locale files,Users see raw keys or missing text in unsupported languages breaks i18n contract,When adding t(key) always add the key to every messages locale file Run verify-i18n to check
|
|
58
|
+
56,i18n,Adding translation keys in code without updating all locale files,Users see raw keys or missing text in unsupported languages breaks i18n contract,When adding t(key) always add the key to every messages locale file Run verify-i18n to check
|
package/assets/data/charts.csv
CHANGED
|
@@ -11,7 +11,7 @@ id,chart_type,library,use_case,admin_context,best_for_data,avoid_when
|
|
|
11
11
|
10,Heatmap,ApexCharts / MUI X-Charts,Density matrix,User activity by hour/day correlation matrix,Finding patterns in dense data,Simple sparse data
|
|
12
12
|
11,Scatter Plot,Recharts / ApexCharts,Correlation analysis,Price vs sales customer age vs spend,Finding relationships clusters outliers,Single variable data
|
|
13
13
|
12,Bubble Chart,Recharts / ApexCharts,3-variable correlation,Deal size vs probability vs revenue impact,Adding size dimension to scatter,Too many bubbles overlap
|
|
14
|
-
13,Candlestick,ApexCharts
|
|
14
|
+
13,Candlestick,ApexCharts,Financial OHLC,Stock price crypto forex trading data,Open high low close financial data,Non-financial contexts
|
|
15
15
|
14,Sparkline,Recharts / ApexCharts,Mini trend inline,Table row mini chart card header trend,Quick trend in small space,Detailed analysis needed
|
|
16
16
|
15,Gauge Chart,ApexCharts / MUI X-Charts,Single KPI progress,CPU usage disk space quota utilization,0-100% progress toward goal,Multiple metrics
|
|
17
17
|
16,Funnel Chart,Recharts / ApexCharts,Conversion stages,Sales pipeline recruitment funnel drop-off,Sequential stages with drop-off,Non-sequential data
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
id,industry,display_name,pattern,style_priority,color_mood,typography_mood,key_effects,anti_patterns
|
|
2
2
|
1,saas,SaaS / B2B Platform,Feature-Rich Dashboard + Onboarding,bento-grid minimalism soft-ui,Clean blues + white + accent purple,Inter / Public Sans sans-serif,Micro-interactions smooth transitions 200ms subtle shadows,Skeuomorphism bright neon colors cluttered hero slow animations
|
|
3
|
-
2,fintech,Fintech / Banking,Data-Dense Dashboard + Transaction Table,minimalism dark-mode
|
|
3
|
+
2,fintech,Fintech / Banking,Data-Dense Dashboard + Transaction Table,minimalism dark-mode glassmorphism,Deep navy + emerald green + gold accents,Plus Jakarta Sans / DM Sans modern geometric,Real-time data updates subtle pulse animations card hover elevation,Playful colors rounded corners everywhere emoji icons insufficient contrast
|
|
4
4
|
3,healthcare,Healthcare / Medical,Patient List + Appointment Calendar + Charts,minimalism accessible soft-ui,Soft teal + white + warm gray + alert red,Open Sans / Roboto highly readable,Clear visual hierarchy status badges with icons gentle transitions,Small text poor contrast complex medical jargon without tooltips missing loading states
|
|
5
5
|
4,ecommerce,E-commerce Admin,Order Management + Inventory Grid + Sales Charts,bento-grid data-dense,Orange accent + white + dark sidebar + success green,Inter / Source Sans Pro clean functional,Quick actions contextual menus real-time stock indicators,Bright sales-y colors distracting animations missing empty states for out-of-stock
|
|
6
|
-
5,logistics,Logistics / Supply Chain,Map + Shipment Tracker + Fleet Table,real-time
|
|
6
|
+
5,logistics,Logistics / Supply Chain,Map + Shipment Tracker + Fleet Table,real-time glassmorphism,Deep blue + bright cyan + warning amber + map green,Roboto Mono / Inter monospace for tracking IDs,Map integrations timeline views live status indicators,Cluttered maps poor mobile experience missing offline indicators
|
|
7
7
|
6,hr,HR / People Management,Employee Directory + Org Chart + Payroll Table,minimalism soft-ui bento-grid,Warm purple + soft gray + white + status colors,Work Sans / Lato friendly professional,Profile cards org tree visualizations approval workflows,Overly casual fonts missing privacy indicators complex navigation too many clicks
|
|
8
8
|
7,crm,CRM / Sales,Pipeline Board + Contact List + Activity Feed,bento-grid soft-ui,Electric blue + white + warm gray + deal stage colors,Inter / SF Pro clean crisp,Drag-drop pipeline activity timelines win/loss indicators,Missing empty pipeline states poor mobile card layout overwhelming notifications
|
|
9
9
|
8,erp,ERP / Manufacturing,Production Dashboard + Inventory + BOM Table,data-dense executive,Industrial gray + safety orange + machine blue + alert red,IBM Plex Sans / Roboto technical precise,Machine status gauges production line visuals alert banners,Overly decorative fonts cluttered tables missing real-time indicators slow refresh
|
|
10
10
|
9,education,Education / LMS,Course List + Student Progress + Gradebook,minimalism accessible soft-ui,Academic blue + warm white + success green + caution amber,Lora / Open Sans readable elegant,Progress bars achievement badges calendar views,Childish fonts excessive gamification poor accessibility missing progress persistence
|
|
11
|
-
10,government,Government / Public Sector,Case Management + Document List + Reporting,minimalism accessible
|
|
12
|
-
11,cybersecurity,Cybersecurity / SOC,Alert Feed + Threat Map + Incident Table,dark-mode
|
|
11
|
+
10,government,Government / Public Sector,Case Management + Document List + Reporting,minimalism accessible,Official blue + white + gray + priority red,Merriweather / Open Sans formal trustworthy,Clear status workflows document version tracking audit trails,Political colors overly modern flashy design missing WCAG compliance complex language
|
|
12
|
+
11,cybersecurity,Cybersecurity / SOC,Alert Feed + Threat Map + Incident Table,dark-mode ai-native,Deep black + alert red + cyber cyan + warning amber,Fira Code / Inter monospace for logs technical,Dark theme real-time alerts threat level indicators SOC timeline,Light theme by default poor alert visibility cluttered dashboards missing severity colors
|
|
13
13
|
12,real-estate,Real Estate / Property,Property Grid + Map + Lead Pipeline,bento-grid soft-ui,Premium navy + gold accent + white + status green,Playfair Display / Inter luxury clean,Property cards map pins lead scoring visual comparison tools,Excessive imagery poor table performance missing price formatting cluttered filters
|
|
14
|
-
13,energy,Energy / Utilities,Grid Monitor + Meter Readings + Outage Map,real-time
|
|
14
|
+
13,energy,Energy / Utilities,Grid Monitor + Meter Readings + Outage Map,real-time data-dense,Power blue + grid yellow + outage red + eco green,Roboto / Source Sans Pro technical functional,Real-time gauges geographic outage maps consumption charts,Missing time-series data poor mobile map experience slow data refresh
|
|
15
15
|
14,media,Media / Content Management,Asset Library + Editorial Calendar + Analytics,minimalism bento-grid,Dark charcoal + vibrant accent + white + video red,Montserrat / Inter modern dynamic,Media previews drag-drop upload editorial timeline engagement charts,Cluttered asset grids missing metadata poor search missing preview thumbnails
|
|
16
16
|
15,nonprofit,Nonprofit / NGO,Donor CRM + Campaign Tracker + Impact Dashboard,soft-ui accessible minimalism,Hope blue + growth green + warm white + heart red,Merriweather / Lora trustworthy warm,Donor profiles campaign progress impact visualization volunteer management,Overly corporate design missing donation CTAs poor mobile donation flow complex reporting
|
|
@@ -37,3 +37,6 @@ id,page_type,check,severity,why_it_matters
|
|
|
37
37
|
36,dependencies,All new imports resolve to installed packages,High,Missing packages break builds and CI
|
|
38
38
|
37,dependencies,No unused imports or dead dependencies,Medium,Bloats bundle and confuses developers
|
|
39
39
|
38,dependencies,Package manager lockfile is in sync with package.json,High,Prevents inconsistent installs across environments
|
|
40
|
+
39,i18n,All translation keys exist in every locale file,High,Missing keys show raw text to users
|
|
41
|
+
40,i18n,No hardcoded user-facing strings outside t() calls,High,Breaks multi-language support
|
|
42
|
+
41,i18n,New translation keys added to all messages files before commit,High,Prevents incomplete translations
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
id,category,score_1_3,score_4_6,score_7_8,score_9_10
|
|
2
|
-
1,Premium Admin Visual Quality,Layout is broken or unrecognizable as admin UI; no cards; random spacing,Cards present but inconsistent; spacing varies; some hierarchy; looks like generic
|
|
3
|
-
2,
|
|
2
|
+
1,Premium Admin Visual Quality,Layout is broken or unrecognizable as admin UI; no cards; random spacing,Cards present but inconsistent; spacing varies; some hierarchy; looks like generic MUI,Clean card hierarchy; consistent spacing; feels like admin panel; status chips styled,Premium Enterprise admin feel; balanced rhythm; excellent hierarchy; polished status and actions; visually cohesive
|
|
3
|
+
2,MUI Architecture Quality,No typed props; inline everything; no reusable components; raw MUI dumped,Some abstractions; partial types; mixed patterns; inconsistent API surfaces,Good component boundaries; typed props; consistent Form/Table patterns; token usage,Excellent architecture; predictable APIs; disciplined abstractions; token-first; fully typed
|
|
4
4
|
3,Component Reusability,Everything inline; copy-paste patterns; no shared UI primitives,Some shared components but inconsistent props; magic numbers; hardcoded text,Reusable PageLayout Card Table Form primitives; configurable via props,Highly reusable system; composable patterns; design-token driven; minimal duplication
|
|
5
5
|
4,Form Quality,No validation; no loading; no feedback; inline styles; missing labels,Basic validation; some feedback; inconsistent layout; partial loading state,Full typed Form; validation rules; helper text; dirty guard; responsive grid,Enterprise form discipline; accessible labels; clear errors; submit loading; cancel/reset; sectioned layout
|
|
6
6
|
5,Table Quality,No pagination; no sorting; no rowKey; no empty state; inline columns,Basic table with some features; missing loading or error state; hardcoded columns,Typed columns; loading/empty/error; pagination; sorting; row actions; responsive plan,Production table standard; server-side ready; accessible headers; status tags; formatted values; bulk actions
|
package/assets/scripts/search.py
CHANGED
|
@@ -52,8 +52,8 @@ def find_data_dir() -> Path:
|
|
|
52
52
|
return candidates[0]
|
|
53
53
|
|
|
54
54
|
|
|
55
|
-
def bm25_score(row: dict, query_terms: list[str], avgdl: float, k1: float = 1.5, b: float = 0.75) -> float:
|
|
56
|
-
"""
|
|
55
|
+
def bm25_score(row: dict, query_terms: list[str], term_doc_counts: dict[str, int], total_docs: int, avgdl: float, k1: float = 1.5, b: float = 0.75) -> float:
|
|
56
|
+
"""BM25 scoring with proper IDF across all row values."""
|
|
57
57
|
text = " ".join(str(v).lower() for v in row.values() if v is not None)
|
|
58
58
|
doc_len = len(text.split())
|
|
59
59
|
score = 0.0
|
|
@@ -61,9 +61,9 @@ def bm25_score(row: dict, query_terms: list[str], avgdl: float, k1: float = 1.5,
|
|
|
61
61
|
tf = text.count(term.lower())
|
|
62
62
|
if tf == 0:
|
|
63
63
|
continue
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
idf =
|
|
64
|
+
n = term_doc_counts.get(term.lower(), 1)
|
|
65
|
+
idf = max(0.0, (total_docs - n + 0.5) / (n + 0.5))
|
|
66
|
+
idf = max(0.01, idf) # Prevent negative IDF
|
|
67
67
|
tf_component = (tf * (k1 + 1)) / (tf + k1 * (1 - b + b * (doc_len / max(avgdl, 1))))
|
|
68
68
|
score += idf * tf_component
|
|
69
69
|
return score
|
|
@@ -79,11 +79,11 @@ def search_csv(data_dir: Path, domain: str, query: str, max_results: int = 10) -
|
|
|
79
79
|
if not filepath.exists():
|
|
80
80
|
return f"Error: File not found: {filepath}"
|
|
81
81
|
|
|
82
|
-
query_terms = query.split()
|
|
82
|
+
query_terms = [t.lower() for t in query.split()]
|
|
83
83
|
results = []
|
|
84
84
|
total_docs = 0
|
|
85
85
|
|
|
86
|
-
# First pass: count total docs and
|
|
86
|
+
# First pass: count total docs, avgdl, and term document frequencies
|
|
87
87
|
with open(filepath, newline="", encoding="utf-8") as f:
|
|
88
88
|
reader = csv.DictReader(f)
|
|
89
89
|
docs = list(reader)
|
|
@@ -91,9 +91,18 @@ def search_csv(data_dir: Path, domain: str, query: str, max_results: int = 10) -
|
|
|
91
91
|
total_len = sum(len(" ".join(str(v) for v in r.values() if v is not None).split()) for r in docs)
|
|
92
92
|
avgdl = total_len / max(total_docs, 1)
|
|
93
93
|
|
|
94
|
+
term_doc_counts: dict[str, int] = {}
|
|
95
|
+
for term in query_terms:
|
|
96
|
+
count = 0
|
|
97
|
+
for row in docs:
|
|
98
|
+
text = " ".join(str(v).lower() for v in row.values() if v is not None)
|
|
99
|
+
if term in text:
|
|
100
|
+
count += 1
|
|
101
|
+
term_doc_counts[term] = max(count, 1)
|
|
102
|
+
|
|
94
103
|
# Second pass: score
|
|
95
104
|
for row in docs:
|
|
96
|
-
s = bm25_score(row, query_terms, avgdl)
|
|
105
|
+
s = bm25_score(row, query_terms, term_doc_counts, total_docs, avgdl)
|
|
97
106
|
if s > 0:
|
|
98
107
|
results.append((s, row))
|
|
99
108
|
|
|
@@ -227,10 +236,13 @@ def main():
|
|
|
227
236
|
|
|
228
237
|
if args.domain == "all":
|
|
229
238
|
for domain in DOMAINS:
|
|
239
|
+
result = search_csv(data_dir, domain, args.query, args.n)
|
|
240
|
+
if "No results" in result:
|
|
241
|
+
continue
|
|
230
242
|
print(f"\n{'='*60}")
|
|
231
243
|
print(f"Domain: {domain}")
|
|
232
244
|
print(f"{'='*60}")
|
|
233
|
-
print(
|
|
245
|
+
print(result)
|
|
234
246
|
return
|
|
235
247
|
|
|
236
248
|
output = search_csv(data_dir, args.domain, args.query, args.n)
|
package/assets/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { initCommand } from "./commands/init.js";
|
|
3
3
|
import { verifyImportsCommand } from "./commands/verify-imports.js";
|
|
4
|
+
import { verifyI18nCommand } from "./commands/verify-i18n.js";
|
|
4
5
|
|
|
5
6
|
function showHelp(): void {
|
|
6
7
|
console.log(`
|
|
@@ -9,10 +10,12 @@ Enterprise UI Architect CLI
|
|
|
9
10
|
Usage:
|
|
10
11
|
enterprise-ui init [options]
|
|
11
12
|
enterprise-ui verify-imports [options]
|
|
13
|
+
enterprise-ui verify-i18n [options]
|
|
12
14
|
|
|
13
15
|
Commands:
|
|
14
16
|
init Install skill into AI coding assistants
|
|
15
17
|
verify-imports Scan source files and report missing npm packages
|
|
18
|
+
verify-i18n Scan source files and report missing translation keys
|
|
16
19
|
|
|
17
20
|
Options:
|
|
18
21
|
--ai <assistant> Target AI assistant: cursor, claude, windsurf, copilot, codex, all (default: all)
|
|
@@ -26,6 +29,8 @@ Examples:
|
|
|
26
29
|
enterprise-ui init --ai claude --offline
|
|
27
30
|
enterprise-ui verify-imports
|
|
28
31
|
enterprise-ui verify-imports --src ./src
|
|
32
|
+
enterprise-ui verify-i18n
|
|
33
|
+
enterprise-ui verify-i18n --src ./src
|
|
29
34
|
`);
|
|
30
35
|
}
|
|
31
36
|
|
|
@@ -42,7 +47,7 @@ function parseArgs(args: string[]): {
|
|
|
42
47
|
|
|
43
48
|
for (let i = 0; i < args.length; i++) {
|
|
44
49
|
const arg = args[i];
|
|
45
|
-
if (arg === "init" || arg === "verify-imports") {
|
|
50
|
+
if (arg === "init" || arg === "verify-imports" || arg === "verify-i18n") {
|
|
46
51
|
command = arg;
|
|
47
52
|
} else if (arg === "--ai" && i + 1 < args.length) {
|
|
48
53
|
result.ai = args[++i];
|
|
@@ -65,7 +70,7 @@ function main(): number {
|
|
|
65
70
|
const parsed = parseArgs(args);
|
|
66
71
|
|
|
67
72
|
if (parsed.version) {
|
|
68
|
-
console.log("
|
|
73
|
+
console.log("2.0.0");
|
|
69
74
|
return 0;
|
|
70
75
|
}
|
|
71
76
|
|
|
@@ -94,6 +99,16 @@ function main(): number {
|
|
|
94
99
|
}
|
|
95
100
|
}
|
|
96
101
|
|
|
102
|
+
if (parsed.command === "verify-i18n") {
|
|
103
|
+
try {
|
|
104
|
+
verifyI18nCommand({ srcDir: parsed.srcDir });
|
|
105
|
+
return 0;
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.error(`Error: ${(err as Error).message}`);
|
|
108
|
+
return 1;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
97
112
|
console.error(`Unknown command. Run --help for usage.`);
|
|
98
113
|
return 1;
|
|
99
114
|
}
|
|
@@ -93,6 +93,24 @@ python scripts/search.py --query "saas" --design-system --persist --product "MyA
|
|
|
93
93
|
- [ ] Implement retry + error fallback for unstable APIs
|
|
94
94
|
- [ ] Use refetchInterval or WebSocket for real-time dashboards
|
|
95
95
|
|
|
96
|
+
## Package Import Verification
|
|
97
|
+
```bash
|
|
98
|
+
enterprise-ui verify-imports --src ./src
|
|
99
|
+
```
|
|
100
|
+
- [ ] All new imports resolve to installed packages
|
|
101
|
+
- [ ] Check package.json before adding imports
|
|
102
|
+
- [ ] Ask user before installing missing packages
|
|
103
|
+
- [ ] Post-install: run `npx tsc --noEmit`
|
|
104
|
+
|
|
105
|
+
## Translation Verification
|
|
106
|
+
```bash
|
|
107
|
+
enterprise-ui verify-i18n --src ./src
|
|
108
|
+
```
|
|
109
|
+
- [ ] All user-facing strings use `t()` — no hardcoded text
|
|
110
|
+
- [ ] Every `t("key")` exists in all `messages/*.json` files
|
|
111
|
+
- [ ] Use `useTranslations("namespace")` for page-scoped keys
|
|
112
|
+
- [ ] Avoid dynamic template literals for keys
|
|
113
|
+
|
|
96
114
|
## Pre-Delivery Checklist
|
|
97
115
|
- [ ] All interactive elements have hover states and cursor-pointer
|
|
98
116
|
- [ ] Focus states are visible and consistent
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify-i18n.d.ts","sourceRoot":"","sources":["../../src/commands/verify-i18n.ts"],"names":[],"mappings":"AAGA,UAAU,aAAa;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;CACrB;AAoID,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,OAAO,CAAC,aAAa,CAAM,GAAG,IAAI,CA0E5E"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { readFileSync, existsSync, readdirSync } from "fs";
|
|
2
|
+
import { resolve, dirname, join, sep, extname } from "path";
|
|
3
|
+
function findMessagesDir(startDir) {
|
|
4
|
+
let dir = resolve(startDir);
|
|
5
|
+
while (dir !== dirname(dir)) {
|
|
6
|
+
const messagesPath = join(dir, "messages");
|
|
7
|
+
if (existsSync(messagesPath))
|
|
8
|
+
return messagesPath;
|
|
9
|
+
dir = dirname(dir);
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
function getSourceFiles(dir) {
|
|
14
|
+
const results = [];
|
|
15
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
16
|
+
for (const entry of entries) {
|
|
17
|
+
const fullPath = join(dir, entry.name);
|
|
18
|
+
if (entry.isDirectory()) {
|
|
19
|
+
if (entry.name === "node_modules" ||
|
|
20
|
+
entry.name === "dist" ||
|
|
21
|
+
entry.name === "build" ||
|
|
22
|
+
entry.name === ".next" ||
|
|
23
|
+
entry.name.startsWith(".")) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
results.push(...getSourceFiles(fullPath));
|
|
27
|
+
}
|
|
28
|
+
else if (entry.isFile()) {
|
|
29
|
+
const ext = extname(entry.name);
|
|
30
|
+
if (ext === ".ts" || ext === ".tsx" || ext === ".js" || ext === ".jsx") {
|
|
31
|
+
results.push(fullPath);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return results;
|
|
36
|
+
}
|
|
37
|
+
function loadTranslations(messagesDir) {
|
|
38
|
+
const files = readdirSync(messagesDir).filter((f) => f.endsWith(".json"));
|
|
39
|
+
const translations = {};
|
|
40
|
+
for (const file of files) {
|
|
41
|
+
const locale = file.replace(".json", "");
|
|
42
|
+
const content = readFileSync(join(messagesDir, file), "utf-8");
|
|
43
|
+
translations[locale] = JSON.parse(content);
|
|
44
|
+
}
|
|
45
|
+
return translations;
|
|
46
|
+
}
|
|
47
|
+
function getNestedValue(obj, path) {
|
|
48
|
+
const parts = path.split(".");
|
|
49
|
+
let current = obj;
|
|
50
|
+
for (const part of parts) {
|
|
51
|
+
if (current === null || current === undefined)
|
|
52
|
+
return undefined;
|
|
53
|
+
if (typeof current !== "object")
|
|
54
|
+
return undefined;
|
|
55
|
+
current = current[part];
|
|
56
|
+
}
|
|
57
|
+
return current;
|
|
58
|
+
}
|
|
59
|
+
function extractNamespace(content) {
|
|
60
|
+
const match = /useTranslations\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/.exec(content);
|
|
61
|
+
return match ? match[1] : null;
|
|
62
|
+
}
|
|
63
|
+
function isInsideStringLiteral(line, matchIndex) {
|
|
64
|
+
// Check if the character before t( is inside a string literal
|
|
65
|
+
const before = line.slice(0, matchIndex);
|
|
66
|
+
let inSingle = false;
|
|
67
|
+
let inDouble = false;
|
|
68
|
+
let escaped = false;
|
|
69
|
+
for (const ch of before) {
|
|
70
|
+
if (escaped) {
|
|
71
|
+
escaped = false;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (ch === "\\") {
|
|
75
|
+
escaped = true;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (ch === '"' && !inSingle) {
|
|
79
|
+
inDouble = !inDouble;
|
|
80
|
+
}
|
|
81
|
+
else if (ch === "'" && !inDouble) {
|
|
82
|
+
inSingle = !inSingle;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return inSingle || inDouble;
|
|
86
|
+
}
|
|
87
|
+
function extractTranslationKeys(content, namespace) {
|
|
88
|
+
const lines = content.split("\n");
|
|
89
|
+
const keys = [];
|
|
90
|
+
const seen = new Set();
|
|
91
|
+
const regex = /\bt\s*\(\s*["'`]([a-zA-Z0-9_.-]+)["'`]/g;
|
|
92
|
+
for (let i = 0; i < lines.length; i++) {
|
|
93
|
+
const line = lines[i];
|
|
94
|
+
// Skip comment-only lines
|
|
95
|
+
const codePart = line.split("//")[0];
|
|
96
|
+
if (!codePart.includes("t("))
|
|
97
|
+
continue;
|
|
98
|
+
let match;
|
|
99
|
+
while ((match = regex.exec(line)) !== null) {
|
|
100
|
+
if (isInsideStringLiteral(line, match.index))
|
|
101
|
+
continue;
|
|
102
|
+
const rawKey = match[1];
|
|
103
|
+
const key = namespace ? `${namespace}.${rawKey}` : rawKey;
|
|
104
|
+
if (!seen.has(key)) {
|
|
105
|
+
seen.add(key);
|
|
106
|
+
keys.push({ key, file: "", line: i + 1 });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return keys;
|
|
111
|
+
}
|
|
112
|
+
export function verifyI18nCommand(options = {}) {
|
|
113
|
+
const srcDir = options.srcDir || process.cwd();
|
|
114
|
+
const messagesDir = options.messagesDir || findMessagesDir(srcDir);
|
|
115
|
+
if (!messagesDir) {
|
|
116
|
+
console.error("❌ messages/ directory not found. Run this from a project root.");
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
const translations = loadTranslations(messagesDir);
|
|
120
|
+
const locales = Object.keys(translations);
|
|
121
|
+
if (locales.length === 0) {
|
|
122
|
+
console.error("❌ No translation files found in messages/ directory.");
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
const files = getSourceFiles(srcDir);
|
|
126
|
+
const allKeys = new Map();
|
|
127
|
+
for (const file of files) {
|
|
128
|
+
const content = readFileSync(file, "utf-8");
|
|
129
|
+
const namespace = extractNamespace(content);
|
|
130
|
+
const keys = extractTranslationKeys(content, namespace);
|
|
131
|
+
for (const { key, line } of keys) {
|
|
132
|
+
const list = allKeys.get(key) || [];
|
|
133
|
+
list.push({ file, line });
|
|
134
|
+
allKeys.set(key, list);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (allKeys.size === 0) {
|
|
138
|
+
console.log("ℹ️ No translation keys found in source files.");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const missingByLocale = new Map();
|
|
142
|
+
for (const [key, locations] of allKeys) {
|
|
143
|
+
for (const locale of locales) {
|
|
144
|
+
const value = getNestedValue(translations[locale], key);
|
|
145
|
+
if (value === undefined) {
|
|
146
|
+
const list = missingByLocale.get(locale) || [];
|
|
147
|
+
list.push({ key, file: locations[0].file, line: locations[0].line });
|
|
148
|
+
missingByLocale.set(locale, list);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const totalMissing = Array.from(missingByLocale.values()).reduce((sum, list) => sum + list.length, 0);
|
|
153
|
+
if (totalMissing === 0) {
|
|
154
|
+
console.log(`✅ All ${allKeys.size} translation key(s) exist in every locale (${locales.join(", ")}).`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
console.log(`⚠️ Found ${totalMissing} missing translation key(s):\n`);
|
|
158
|
+
for (const [locale, missing] of missingByLocale) {
|
|
159
|
+
if (missing.length === 0)
|
|
160
|
+
continue;
|
|
161
|
+
console.log(` 🌐 ${locale}.json (${missing.length} missing):`);
|
|
162
|
+
for (const { key, file, line } of missing.slice(0, 10)) {
|
|
163
|
+
const relPath = file.replace(srcDir + sep, "").replace(/^\//, "");
|
|
164
|
+
console.log(` - ${key}`);
|
|
165
|
+
console.log(` used in: ${relPath}:${line}`);
|
|
166
|
+
}
|
|
167
|
+
if (missing.length > 10) {
|
|
168
|
+
console.log(` ... and ${missing.length - 10} more`);
|
|
169
|
+
}
|
|
170
|
+
console.log("");
|
|
171
|
+
}
|
|
172
|
+
console.log(`Add the missing keys to all ${locales.length} locale files.\n`);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=verify-i18n.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify-i18n.js","sourceRoot":"","sources":["../../src/commands/verify-i18n.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verify-imports.d.ts","sourceRoot":"","sources":["../../src/commands/verify-imports.ts"],"names":[],"mappings":"AAGA,UAAU,aAAa;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;CACzB;
|
|
1
|
+
{"version":3,"file":"verify-imports.d.ts","sourceRoot":"","sources":["../../src/commands/verify-imports.ts"],"names":[],"mappings":"AAGA,UAAU,aAAa;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;CACzB;AA8KD,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,OAAO,CAAC,aAAa,CAAM,GAAG,IAAI,CA2E/E"}
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { readFileSync, existsSync, readdirSync } from "fs";
|
|
2
|
-
import { resolve, dirname, join, extname } from "path";
|
|
2
|
+
import { resolve, dirname, join, sep, extname } from "path";
|
|
3
|
+
const NODE_BUILTINS = new Set([
|
|
4
|
+
"assert", "async_hooks", "buffer", "child_process", "cluster", "console",
|
|
5
|
+
"constants", "crypto", "dgram", "diagnostics_channel", "dns", "domain",
|
|
6
|
+
"events", "fs", "http", "http2", "https", "inspector", "module", "net",
|
|
7
|
+
"os", "path", "perf_hooks", "process", "punycode", "querystring", "readline",
|
|
8
|
+
"repl", "stream", "string_decoder", "sys", "timers", "timers/promises",
|
|
9
|
+
"tls", "trace_events", "tty", "url", "util", "v8", "vm", "wasi", "worker_threads",
|
|
10
|
+
"zlib", "node:test",
|
|
11
|
+
]);
|
|
3
12
|
function findPackageJson(startDir) {
|
|
4
13
|
let dir = resolve(startDir);
|
|
5
14
|
while (dir !== dirname(dir)) {
|
|
@@ -24,21 +33,33 @@ function getInstalledPackages(packageJsonPath) {
|
|
|
24
33
|
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
25
34
|
const deps = Object.keys(pkg.dependencies || {});
|
|
26
35
|
const devDeps = Object.keys(pkg.devDependencies || {});
|
|
27
|
-
|
|
36
|
+
const peerDeps = Object.keys(pkg.peerDependencies || {});
|
|
37
|
+
const optionalDeps = Object.keys(pkg.optionalDependencies || {});
|
|
38
|
+
return new Set([...deps, ...devDeps, ...peerDeps, ...optionalDeps]);
|
|
28
39
|
}
|
|
29
40
|
function getPathAliases(tsConfigPath) {
|
|
30
41
|
try {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
const raw = readFileSync(tsConfigPath, "utf-8");
|
|
43
|
+
// Extract path aliases via regex — tolerant to comments and trailing commas
|
|
44
|
+
const aliases = [];
|
|
45
|
+
// Match patterns like "@core/*" or "@/components/*" inside "paths": { ... }
|
|
46
|
+
const pathsMatch = raw.match(/"paths"\s*:\s*\{([\s\S]*?)\}/);
|
|
47
|
+
if (pathsMatch) {
|
|
48
|
+
const pathsBlock = pathsMatch[1];
|
|
49
|
+
const keyRegex = /"(@[^"]+)"\s*:/g;
|
|
50
|
+
let m;
|
|
51
|
+
while ((m = keyRegex.exec(pathsBlock)) !== null) {
|
|
52
|
+
aliases.push(m[1]);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return aliases.length > 0 ? aliases : ["@/*"];
|
|
34
56
|
}
|
|
35
57
|
catch {
|
|
36
|
-
return [];
|
|
58
|
+
return ["@/*"];
|
|
37
59
|
}
|
|
38
60
|
}
|
|
39
61
|
function isPathAlias(source, aliases) {
|
|
40
62
|
for (const alias of aliases) {
|
|
41
|
-
// Convert tsconfig glob to regex: @core/* → /^@core\//
|
|
42
63
|
const prefix = alias.replace(/\/\*$/, "");
|
|
43
64
|
if (alias.endsWith("/*")) {
|
|
44
65
|
if (source === prefix || source.startsWith(prefix + "/")) {
|
|
@@ -81,12 +102,30 @@ function extractImports(filePath) {
|
|
|
81
102
|
const content = readFileSync(filePath, "utf-8");
|
|
82
103
|
const lines = content.split("\n");
|
|
83
104
|
const imports = [];
|
|
84
|
-
|
|
105
|
+
// Matches single-line imports: import ... from "..."
|
|
106
|
+
const singleLineRegex = /^(?:import\s+.*?from\s+|import\s*\(|require\s*\()["']([^"';]+)["'];?/;
|
|
107
|
+
// Matches multi-line import end: from "..."
|
|
108
|
+
const multiLineEndRegex = /from\s+["']([^"';]+)["'];?/;
|
|
85
109
|
for (let i = 0; i < lines.length; i++) {
|
|
86
110
|
const line = lines[i].trim();
|
|
87
|
-
const
|
|
88
|
-
if (
|
|
89
|
-
imports.push({ path: filePath, line: i + 1, source:
|
|
111
|
+
const singleMatch = singleLineRegex.exec(line);
|
|
112
|
+
if (singleMatch) {
|
|
113
|
+
imports.push({ path: filePath, line: i + 1, source: singleMatch[1] });
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
// Multi-line import: starts with "import" but no "from" on same line
|
|
117
|
+
if (line.startsWith("import") && !line.includes("from") && !line.includes("(")) {
|
|
118
|
+
// Look ahead up to 5 lines for "from '...'"
|
|
119
|
+
for (let j = i + 1; j < Math.min(i + 6, lines.length); j++) {
|
|
120
|
+
const nextLine = lines[j].trim();
|
|
121
|
+
const multiMatch = multiLineEndRegex.exec(nextLine);
|
|
122
|
+
if (multiMatch) {
|
|
123
|
+
imports.push({ path: filePath, line: j + 1, source: multiMatch[1] });
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
if (nextLine.endsWith(";"))
|
|
127
|
+
break; // End of import without from
|
|
128
|
+
}
|
|
90
129
|
}
|
|
91
130
|
}
|
|
92
131
|
return imports;
|
|
@@ -105,7 +144,12 @@ function resolvePackageName(source) {
|
|
|
105
144
|
const idx = source.indexOf("/");
|
|
106
145
|
return idx > 0 ? source.slice(0, idx) : source;
|
|
107
146
|
}
|
|
147
|
+
function isNodeBuiltin(pkg) {
|
|
148
|
+
return NODE_BUILTINS.has(pkg) || NODE_BUILTINS.has(`node:${pkg}`);
|
|
149
|
+
}
|
|
108
150
|
function detectPackageManager(lockFiles) {
|
|
151
|
+
if (lockFiles.some((f) => f.endsWith("bun.lockb")))
|
|
152
|
+
return "bun";
|
|
109
153
|
if (lockFiles.some((f) => f.endsWith("pnpm-lock.yaml")))
|
|
110
154
|
return "pnpm";
|
|
111
155
|
if (lockFiles.some((f) => f.endsWith("yarn.lock")))
|
|
@@ -125,7 +169,7 @@ export function verifyImportsCommand(options = {}) {
|
|
|
125
169
|
const projectRoot = dirname(packageJsonPath);
|
|
126
170
|
const tsConfigPath = findTsConfig(srcDir);
|
|
127
171
|
const pathAliases = tsConfigPath ? getPathAliases(tsConfigPath) : [];
|
|
128
|
-
const lockFiles = ["package-lock.json", "yarn.lock", "pnpm-lock.yaml"].map((f) => join(projectRoot, f));
|
|
172
|
+
const lockFiles = ["package-lock.json", "yarn.lock", "pnpm-lock.yaml", "bun.lockb"].map((f) => join(projectRoot, f));
|
|
129
173
|
const existingLocks = lockFiles.filter((f) => existsSync(f));
|
|
130
174
|
const pkgManager = detectPackageManager(existingLocks);
|
|
131
175
|
const files = getSourceFiles(srcDir);
|
|
@@ -140,6 +184,8 @@ export function verifyImportsCommand(options = {}) {
|
|
|
140
184
|
const pkg = resolvePackageName(imp.source);
|
|
141
185
|
if (!pkg)
|
|
142
186
|
continue;
|
|
187
|
+
if (isNodeBuiltin(pkg))
|
|
188
|
+
continue;
|
|
143
189
|
if (installed.has(pkg))
|
|
144
190
|
continue;
|
|
145
191
|
const list = missing.get(pkg) || [];
|
|
@@ -155,17 +201,19 @@ export function verifyImportsCommand(options = {}) {
|
|
|
155
201
|
console.log(` 📦 ${pkg}`);
|
|
156
202
|
console.log(` Used in:`);
|
|
157
203
|
for (const imp of imports.slice(0, 3)) {
|
|
158
|
-
const relPath = imp.path.replace(projectRoot + "
|
|
204
|
+
const relPath = imp.path.replace(projectRoot + sep, "").replace(/^\//, "");
|
|
159
205
|
console.log(` - ${relPath}:${imp.line} → import from "${imp.source}"`);
|
|
160
206
|
}
|
|
161
207
|
if (imports.length > 3) {
|
|
162
208
|
console.log(` ... and ${imports.length - 3} more`);
|
|
163
209
|
}
|
|
164
|
-
const installCmd = pkgManager === "
|
|
165
|
-
? `
|
|
166
|
-
: pkgManager === "
|
|
167
|
-
? `
|
|
168
|
-
:
|
|
210
|
+
const installCmd = pkgManager === "bun"
|
|
211
|
+
? `bun add ${pkg}`
|
|
212
|
+
: pkgManager === "pnpm"
|
|
213
|
+
? `pnpm add ${pkg}`
|
|
214
|
+
: pkgManager === "yarn"
|
|
215
|
+
? `yarn add ${pkg}`
|
|
216
|
+
: `npm install ${pkg}`;
|
|
169
217
|
console.log(` Install: ${installCmd}\n`);
|
|
170
218
|
}
|
|
171
219
|
console.log(`Run the install command(s) above, then re-run this check.\n`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verify-imports.js","sourceRoot":"","sources":["../../src/commands/verify-imports.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,WAAW,
|
|
1
|
+
{"version":3,"file":"verify-imports.js","sourceRoot":"","sources":["../../src/commands/verify-imports.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { initCommand } from "./commands/init.js";
|
|
3
3
|
import { verifyImportsCommand } from "./commands/verify-imports.js";
|
|
4
|
+
import { verifyI18nCommand } from "./commands/verify-i18n.js";
|
|
4
5
|
function showHelp() {
|
|
5
6
|
console.log(`
|
|
6
7
|
Enterprise UI Architect CLI
|
|
@@ -8,10 +9,12 @@ Enterprise UI Architect CLI
|
|
|
8
9
|
Usage:
|
|
9
10
|
enterprise-ui init [options]
|
|
10
11
|
enterprise-ui verify-imports [options]
|
|
12
|
+
enterprise-ui verify-i18n [options]
|
|
11
13
|
|
|
12
14
|
Commands:
|
|
13
15
|
init Install skill into AI coding assistants
|
|
14
16
|
verify-imports Scan source files and report missing npm packages
|
|
17
|
+
verify-i18n Scan source files and report missing translation keys
|
|
15
18
|
|
|
16
19
|
Options:
|
|
17
20
|
--ai <assistant> Target AI assistant: cursor, claude, windsurf, copilot, codex, all (default: all)
|
|
@@ -25,6 +28,8 @@ Examples:
|
|
|
25
28
|
enterprise-ui init --ai claude --offline
|
|
26
29
|
enterprise-ui verify-imports
|
|
27
30
|
enterprise-ui verify-imports --src ./src
|
|
31
|
+
enterprise-ui verify-i18n
|
|
32
|
+
enterprise-ui verify-i18n --src ./src
|
|
28
33
|
`);
|
|
29
34
|
}
|
|
30
35
|
function parseArgs(args) {
|
|
@@ -32,7 +37,7 @@ function parseArgs(args) {
|
|
|
32
37
|
let command;
|
|
33
38
|
for (let i = 0; i < args.length; i++) {
|
|
34
39
|
const arg = args[i];
|
|
35
|
-
if (arg === "init" || arg === "verify-imports") {
|
|
40
|
+
if (arg === "init" || arg === "verify-imports" || arg === "verify-i18n") {
|
|
36
41
|
command = arg;
|
|
37
42
|
}
|
|
38
43
|
else if (arg === "--ai" && i + 1 < args.length) {
|
|
@@ -57,7 +62,7 @@ function main() {
|
|
|
57
62
|
const args = process.argv.slice(2);
|
|
58
63
|
const parsed = parseArgs(args);
|
|
59
64
|
if (parsed.version) {
|
|
60
|
-
console.log("
|
|
65
|
+
console.log("2.0.0");
|
|
61
66
|
return 0;
|
|
62
67
|
}
|
|
63
68
|
if (parsed.help || args.length === 0) {
|
|
@@ -84,6 +89,16 @@ function main() {
|
|
|
84
89
|
return 1;
|
|
85
90
|
}
|
|
86
91
|
}
|
|
92
|
+
if (parsed.command === "verify-i18n") {
|
|
93
|
+
try {
|
|
94
|
+
verifyI18nCommand({ srcDir: parsed.srcDir });
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
console.error(`Error: ${err.message}`);
|
|
99
|
+
return 1;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
87
102
|
console.error(`Unknown command. Run --help for usage.`);
|
|
88
103
|
return 1;
|
|
89
104
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|