ai-localize-reporting 2.0.1 → 2.0.4

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 CHANGED
@@ -1,10 +1,15 @@
1
1
  {
2
2
  "name": "ai-localize-reporting",
3
- "version": "2.0.1",
4
- "description": "Localization scan reporting: JSON, HTML, CLI summary",
3
+ "version": "2.0.4",
4
+ "description": "Localization scan reporting: JSON, HTML analytics dashboard and CLI summary",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
7
7
  "types": "./dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "README.md",
11
+ "CHANGELOG.md"
12
+ ],
8
13
  "exports": {
9
14
  ".": {
10
15
  "types": "./dist/index.d.ts",
@@ -12,8 +17,23 @@
12
17
  "require": "./dist/index.js"
13
18
  }
14
19
  },
20
+ "keywords": [
21
+ "i18n",
22
+ "localization",
23
+ "l10n",
24
+ "internationalization",
25
+ "ai-localize",
26
+ "reporting",
27
+ "html-report",
28
+ "cli",
29
+ "dashboard",
30
+ "translation-coverage"
31
+ ],
32
+ "engines": {
33
+ "node": ">=18.0.0"
34
+ },
15
35
  "dependencies": {
16
- "ai-localize-shared": "2.0.1"
36
+ "ai-localize-shared": "2.0.4"
17
37
  },
18
38
  "devDependencies": {
19
39
  "tsup": "^8.0.1",
@@ -1,29 +0,0 @@
1
- import type { Report } from "ai-localize-shared";
2
-
3
- export function printCliSummary(report: Report): void {
4
- const lines = [
5
- "",
6
- "╔════════════════════════════════════════════════════╗",
7
- "║ ai-localize-core Report Summary ║",
8
- "╚════════════════════════════════════════════════════╝",
9
- "",
10
- ` Framework : ${report.framework}`,
11
- ` Timestamp : ${report.timestamp}`,
12
- ` Duration : ${report.duration}ms`,
13
- "",
14
- " ─── Localization ────────────────────────────────",
15
- ` Files Scanned : ${report.filesScanned}`,
16
- ` Hardcoded Texts : ${report.hardcodedTexts}`,
17
- ` Keys Generated : ${report.localeKeysGenerated}`,
18
- ` Missing Trans. : ${report.missingTranslations}`,
19
- ` Unused Keys : ${report.unusedKeys}`,
20
- "",
21
- " ─── Assets / CDN ────────────────────────────────",
22
- ` Total Assets : ${report.assets.totalAssets}`,
23
- ` Uploaded Assets : ${report.assets.uploadedAssets}`,
24
- ` Replaced URLs : ${report.assets.replacedUrls}`,
25
- `Legacy CDN URLs : ${report.assets.legacyCdnUrls}`,
26
- "",
27
- ];
28
- console.log(lines.join('\n'));
29
- }
@@ -1,336 +0,0 @@
1
- import type { Report } from "ai-localize-shared";
2
- import * as fs from "fs";
3
- import * as path from "path";
4
- import { ensureDir } from "ai-localize-shared";
5
-
6
- /**
7
- * Generates a comprehensive, self-contained HTML report file.
8
- *
9
- * @param report - The report data object built by `buildReport()`
10
- * @param outputPath - Full path to the output HTML file
11
- * e.g. `.ai-localize-reports/report.html`
12
- */
13
- export function generateHtmlReport(report: Report, outputPath: string): void {
14
- ensureDir(path.dirname(outputPath));
15
- const html = buildHtml(report);
16
- fs.writeFileSync(outputPath, html, "utf-8");
17
- }
18
-
19
- // ─── Helpers ─────────────────────────────────────────────────────────────────
20
-
21
- /** Escapes a string for safe insertion into HTML. */
22
- function esc(s: string): string {
23
- return s
24
- .replace(/&/g, "&")
25
- .replace(/</g, "<")
26
- .replace(/>/g, ">")
27
- .replace(/"/g, "&#34;");
28
- }
29
-
30
- type BadgeColor = "blue" | "red" | "green" | "orange" | "grey";
31
-
32
- function badge(text: string, color: BadgeColor): string {
33
- return '<span class="badge badge-' + color + '">' + esc(text) + "</span>";
34
- }
35
-
36
- function sectionHeader(title: string, legend: string): string {
37
- return (
38
- '\n<div class="section-header">\n' +
39
- " <h2>" + title + "</h2>\n" +
40
- ' <p class="legend">' + legend + "</p>\n" +
41
- "</div>"
42
- );
43
- }
44
-
45
- // ─── HTML builder ─────────────────────────────────────────────────────────────
46
-
47
- function buildHtml(report: Report): string {
48
- const {
49
- timestamp,
50
- framework,
51
- duration,
52
- filesScanned,
53
- hardcodedTexts,
54
- localeKeysGenerated,
55
- unusedKeys,
56
- missingTranslations,
57
- assets,
58
- details,
59
- } = report;
60
-
61
- const scanDate = new Date(timestamp).toLocaleString();
62
-
63
- // ── Hardcoded Texts ───────────────────────────────────────────────────────
64
- const hardcodedRows = details.detectedTexts
65
- .slice(0, 200)
66
- .map(
67
- (t) =>
68
- "<tr>" +
69
- "<td><code class=\"path\">" + esc(t.filePath.split("/").slice(-3).join("/")) + "</code></td>" +
70
- '<td class="center">' + t.line + "</td>" +
71
- '<td class="text-cell">' + esc(t.text.slice(0, 80)) + (t.text.length > 80 ? "&#8230;" : "") + "</td>" +
72
- '<td><code class="key">' + esc(t.suggestedKey) + "</code></td>" +
73
- "<td>" + badge(t.context, "blue") + "</td>" +
74
- "</tr>"
75
- )
76
- .join("\n");
77
-
78
- const hardcodedOverflow =
79
- details.detectedTexts.length > 200
80
- ? '<tr><td colspan="5" class="overflow-row">&#8230;and ' +
81
- (details.detectedTexts.length - 200) +
82
- " more — run <code>ai-localize scan --output results.json</code> for the full list</td></tr>"
83
- : "";
84
-
85
- const hardcodedSection =
86
- details.detectedTexts.length > 0
87
- ? sectionHeader(
88
- "&#128269; Hardcoded Texts Found (" + details.detectedTexts.length + ")",
89
- "These are raw text strings discovered in your source files that are <strong>not yet wrapped</strong> in a translation call. " +
90
- "Each row shows the file, line number, the text itself, the suggested locale key that would be generated, and the AST context. " +
91
- "Run <code>ai-localize extract</code> to generate locale files and " +
92
- "<code>ai-localize full-migrate</code> to wrap them automatically."
93
- ) +
94
- "\n<table>\n" +
95
- "<thead><tr><th>File (last 3 segments)</th><th>Line</th><th>Text</th><th>Suggested Key</th><th>Context</th></tr></thead>\n" +
96
- "<tbody>" + hardcodedRows + hardcodedOverflow + "</tbody>\n</table>"
97
- : sectionHeader(
98
- "&#9989; Hardcoded Texts Found (0)",
99
- "No hardcoded text was detected. All user-visible strings appear to be wrapped in translation calls already."
100
- );
101
-
102
- // ── Missing Translations ───────────────────────────────────────────────────
103
- const missingRows = details.missingKeys
104
- .map(
105
- (e) =>
106
- "<tr>" +
107
- '<td><code class="key">' + esc(e.key) + "</code></td>" +
108
- "<td>" + (e.language ? badge(e.language, "red") : "—") + "</td>" +
109
- "<td>" + (e.filePath ? '<code class="path">' + esc(e.filePath) + "</code>" : "—") + "</td>" +
110
- "<td>" + esc(e.message) + "</td>" +
111
- "</tr>"
112
- )
113
- .join("\n");
114
-
115
- const missingSection =
116
- details.missingKeys.length > 0
117
- ? sectionHeader(
118
- "&#10060; Missing Translations (" + details.missingKeys.length + ")",
119
- "These locale keys exist in the <strong>default language</strong> file but are <strong>absent</strong> in one or more target language files. " +
120
- "A missing translation means your app will fall back to the default language (or show a blank / raw key) for users of that language. " +
121
- "Ask your translators to fill in these entries. " +
122
- "Running <code>ai-localize extract</code> now seeds all target language files with the source value so nothing is blank."
123
- ) +
124
- "\n<table>\n" +
125
- "<thead><tr><th>Key</th><th>Missing In Language</th><th>Locale File</th><th>Details</th></tr></thead>\n" +
126
- "<tbody>" + missingRows + "</tbody>\n</table>"
127
- : sectionHeader(
128
- "&#9989; Missing Translations (0)",
129
- "All keys present in the default language are also present in every target language file."
130
- );
131
-
132
- // ── Unused Keys ────────────────────────────────────────────────────────────
133
- const unusedRows = details.unusedKeysList
134
- .map((key) => "<tr><td><code class=\"key\">" + esc(key) + "</code></td></tr>")
135
- .join("\n");
136
-
137
- const unusedSection =
138
- details.unusedKeysList.length > 0
139
- ? sectionHeader(
140
- "&#128465;&#65039; Unused Keys (" + details.unusedKeysList.length + ")",
141
- "These translation keys exist in your locale JSON files but are <strong>not referenced anywhere</strong> in the scanned source code. " +
142
- "They are likely left over from removed features or renamed components. " +
143
- "Unused keys bloat your locale bundles and can confuse translators. " +
144
- "Run <code>ai-localize cleanup</code> to remove them (review the list carefully first)."
145
- ) +
146
- "\n<table>\n" +
147
- "<thead><tr><th>Unused Key</th></tr></thead>\n" +
148
- "<tbody>" + unusedRows + "</tbody>\n</table>"
149
- : sectionHeader(
150
- "&#9989; Unused Keys (0)",
151
- "No unused translation keys detected. Your locale files are clean."
152
- );
153
-
154
- // ── Assets ─────────────────────────────────────────────────────────────────
155
- const assetRows = details.assets
156
- .map(
157
- (a) =>
158
- "<tr>" +
159
- "<td><code class=\"path\">" + esc(a.localPath.split("/").slice(-3).join("/")) + "</code></td>" +
160
- '<td><code class="key">' + esc(a.s3Key) + "</code></td>" +
161
- "<td><code>" + esc(a.cloudfrontUrl) + "</code></td>" +
162
- '<td class="center">' + (a.size / 1024).toFixed(1) + " KB</td>" +
163
- "<td>" + badge(a.contentType, "grey") + "</td>" +
164
- "</tr>"
165
- )
166
- .join("\n");
167
-
168
- const assetsSectionTitle =
169
- "&#128230; Assets (" +
170
- assets.totalAssets + " found &#183; " +
171
- assets.uploadedAssets + " uploaded &#183; " +
172
- assets.replacedUrls + " URLs replaced &#183; " +
173
- assets.legacyCdnUrls + " legacy CDN URLs)";
174
-
175
- const assetsLegend =
176
- "<strong>Total assets</strong> = all static asset references found in source (images, fonts, CSS, JS). " +
177
- "<strong>Uploaded</strong> = files pushed to S3/CloudFront in this run. " +
178
- "<strong>Replaced URLs</strong> = legacy CDN references rewritten to the new CloudFront URL in source files. " +
179
- "<strong>Legacy CDN URLs</strong> = old CDN references still present in source that have not yet been replaced — " +
180
- "run <code>ai-localize replace-cdn</code> to fix them.";
181
-
182
- const assetsSection =
183
- sectionHeader(assetsSectionTitle, assetsLegend) +
184
- (details.assets.length > 0
185
- ? "\n<table>\n" +
186
- "<thead><tr><th>Local Path</th><th>S3 Key</th><th>CloudFront URL</th><th>Size</th><th>Content Type</th></tr></thead>\n" +
187
- "<tbody>" + assetRows + "</tbody>\n</table>"
188
- : '\n<p class="empty-state">No assets were uploaded in this run. ' +
189
- "Use <code>ai-localize upload-assets</code> to push static assets to S3/CloudFront.</p>");
190
-
191
- // ── Hardcoded vs Keys Generated explainer ─────────────────────────────────
192
- const diff = Math.abs(hardcodedTexts - localeKeysGenerated);
193
- const diffExplainer =
194
- hardcodedTexts !== localeKeysGenerated
195
- ? '<div class="info-box">' +
196
- "<strong>&#8505;&#65039; Why do Hardcoded Texts (" + hardcodedTexts + ") and Keys Generated (" + localeKeysGenerated + ") differ?</strong><br>" +
197
- "<ul>" +
198
- "<li><strong>Hardcoded Texts</strong> = total raw string occurrences across all files (same string in 5 files = 5).</li>" +
199
- "<li><strong>Keys Generated</strong> = number of <em>unique</em> locale keys after deduplication. Identical strings share one key.</li>" +
200
- "<li>The difference (" + diff + ") = duplicate strings consolidated into shared keys.</li>" +
201
- "</ul>" +
202
- "</div>"
203
- : '<div class="info-box success-box">' +
204
- "<strong>&#9989; Hardcoded Texts and Keys Generated match (" + hardcodedTexts + ").</strong> " +
205
- "Every detected string maps to a unique locale key." +
206
- "</div>";
207
-
208
- // ── Summary cards ──────────────────────────────────────────────────────────
209
- const statCard = (
210
- num: number | string,
211
- label: string,
212
- hint: string,
213
- colorClass: string
214
- ): string =>
215
- '<div class="stat-card">' +
216
- '<div class="num ' + colorClass + '">' + num + "</div>" +
217
- '<div class="lbl">' + label + "</div>" +
218
- '<div class="hint">' + hint + "</div>" +
219
- "</div>";
220
-
221
- const summaryCards =
222
- '<div class="stats">' +
223
- statCard(filesScanned, "Files Scanned", "Source files inspected by AST scanner", "num-neutral") +
224
- statCard(hardcodedTexts, "Hardcoded Texts", "Raw strings not yet wrapped in t()", hardcodedTexts > 0 ? "num-warn" : "num-ok") +
225
- statCard(localeKeysGenerated, "Keys Generated", "Unique locale keys (deduplicated)", "num-ok") +
226
- statCard(missingTranslations, "Missing Translations", "Keys absent in target languages", missingTranslations > 0 ? "num-err" : "num-ok") +
227
- statCard(unusedKeys, "Unused Keys", "Keys in locale files not used in code", unusedKeys > 0 ? "num-warn" : "num-ok") +
228
- statCard(assets.totalAssets, "Assets Found", "Static asset references in source", "num-neutral") +
229
- statCard(assets.uploadedAssets, "Assets Uploaded", "Pushed to S3/CloudFront", "num-ok") +
230
- statCard(assets.legacyCdnUrls, "Legacy CDN URLs", "Old CDN refs not yet replaced", assets.legacyCdnUrls > 0 ? "num-warn" : "num-ok") +
231
- "</div>";
232
-
233
- // ── Full document ──────────────────────────────────────────────────────────
234
- return "<!DOCTYPE html>\n" +
235
- '<html lang="en">\n' +
236
- "<head>\n" +
237
- '<meta charset="UTF-8">\n' +
238
- '<meta name="viewport" content="width=device-width, initial-scale=1.0">\n' +
239
- "<title>ai-localize Report &#8212; " + scanDate + "</title>\n" +
240
- "<style>\n" + CSS + "\n</style>\n" +
241
- "</head>\n" +
242
- "<body>\n" +
243
- "<h1>&#127760; ai-localize Report <small>&#8212; " + esc(framework) + "</small></h1>\n" +
244
- '<p class="meta">' +
245
- "<strong>Generated:</strong> " + scanDate +
246
- " &nbsp;|&nbsp; <strong>Duration:</strong> " + duration + "ms" +
247
- " &nbsp;|&nbsp; <strong>Files Scanned:</strong> " + filesScanned +
248
- "</p>\n" +
249
- summaryCards + "\n" +
250
- diffExplainer + "\n" +
251
- hardcodedSection + "\n" +
252
- missingSection + "\n" +
253
- unusedSection + "\n" +
254
- assetsSection + "\n" +
255
- '<footer>Generated by <strong>ai-localize-core</strong> &#8212; deterministic, offline-capable i18n tooling</footer>\n' +
256
- "</body>\n</html>";
257
- }
258
-
259
- // ─── Stylesheet ───────────────────────────────────────────────────────────────
260
-
261
- const CSS = `
262
- *, *::before, *::after { box-sizing: border-box; }
263
- body {
264
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
265
- max-width: 1280px; margin: 0 auto; padding: 32px 24px;
266
- background: #f0f2f5; color: #1a1a2e; line-height: 1.6;
267
- }
268
- h1 { font-size: 2rem; color: #0f3460; margin-bottom: 4px; }
269
- h1 small { font-size: 1rem; font-weight: 400; color: #666; }
270
- h2 { font-size: 1.2rem; color: #16213e; margin: 0 0 6px; }
271
- .meta { color: #666; font-size: 0.9rem; margin-bottom: 32px; }
272
- .meta strong { color: #333; }
273
- .stats {
274
- display: grid;
275
- grid-template-columns: repeat(auto-fit, minmax(155px, 1fr));
276
- gap: 14px; margin-bottom: 32px;
277
- }
278
- .stat-card {
279
- background: white; border-radius: 10px; padding: 18px 14px;
280
- box-shadow: 0 2px 8px rgba(0,0,0,0.08); text-align: center;
281
- }
282
- .stat-card .num { font-size: 2rem; font-weight: 700; line-height: 1; }
283
- .stat-card .lbl { color: #444; font-size: 0.82rem; margin-top: 6px; font-weight: 600; }
284
- .stat-card .hint { color: #999; font-size: 0.75rem; margin-top: 3px; font-style: italic; }
285
- .num-neutral { color: #0f3460; }
286
- .num-ok { color: #38a169; }
287
- .num-warn { color: #d69e2e; }
288
- .num-err { color: #e53e3e; }
289
- .section-header { margin: 40px 0 12px; }
290
- .legend {
291
- font-size: 0.875rem; color: #555;
292
- background: #f8f9ff; border-left: 4px solid #0f3460;
293
- padding: 10px 14px; border-radius: 0 6px 6px 0; margin: 8px 0 14px;
294
- }
295
- .legend code { background: #e8ebff; padding: 1px 5px; border-radius: 3px; font-size: 0.85em; }
296
- table {
297
- width: 100%; border-collapse: collapse; background: white;
298
- border-radius: 10px; overflow: hidden;
299
- box-shadow: 0 2px 8px rgba(0,0,0,0.08); margin-bottom: 8px;
300
- }
301
- th {
302
- background: #0f3460; color: white; padding: 11px 14px;
303
- text-align: left; font-size: 0.84rem; font-weight: 600;
304
- }
305
- td { padding: 9px 14px; border-bottom: 1px solid #f0f0f0; font-size: 0.84rem; vertical-align: top; }
306
- tr:last-child td { border-bottom: none; }
307
- tr:hover td { background: #f8f9ff; }
308
- .center { text-align: center; }
309
- .text-cell { max-width: 260px; word-break: break-word; }
310
- code.path { color: #555; font-size: 0.81rem; }
311
- code.key { color: #0f3460; font-size: 0.81rem; }
312
- .overflow-row { text-align: center; color: #888; font-style: italic; padding: 12px; }
313
- .badge {
314
- display: inline-block; padding: 2px 8px; border-radius: 10px;
315
- font-size: 0.75rem; font-weight: 600; white-space: nowrap;
316
- }
317
- .badge-blue { background: #ebf4ff; color: #2b6cb0; }
318
- .badge-red { background: #fff5f5; color: #c53030; }
319
- .badge-green { background: #f0fff4; color: #276749; }
320
- .badge-orange { background: #fffaf0; color: #c05621; }
321
- .badge-grey { background: #f7f7f7; color: #555; }
322
- .info-box {
323
- background: #ebf8ff; border-left: 4px solid #3182ce;
324
- padding: 14px 18px; border-radius: 0 8px 8px 0;
325
- margin: 0 0 28px; font-size: 0.9rem;
326
- }
327
- .info-box ul { margin: 8px 0 0 18px; padding: 0; }
328
- .info-box li { margin-bottom: 4px; }
329
- .success-box { background: #f0fff4; border-left-color: #38a169; }
330
- .empty-state { color: #888; font-style: italic; font-size: 0.88rem; padding: 8px 0; }
331
- footer {
332
- margin-top: 48px; padding-top: 16px;
333
- border-top: 1px solid #ddd; color: #aaa;
334
- font-size: 0.8rem; text-align: center;
335
- }
336
- `;
package/src/index.ts DELETED
@@ -1,3 +0,0 @@
1
- export * from "./report-builder.js";
2
- export * from "./html-reporter.js";
3
- export * from "./cli-reporter.js";
@@ -1,35 +0,0 @@
1
- import type { ScanResult, ValidationResult, Report, CloudFrontAsset } from "ai-localize-shared";
2
-
3
- export interface ReportInput {
4
- scanResult: ScanResult;
5
- validationResult?: ValidationResult;
6
- uploadedAssets?: CloudFrontAsset[];
7
- replacedUrls?: number;
8
- }
9
-
10
- export function buildReport(input: ReportInput): Report {
11
- const { scanResult, validationResult, uploadedAssets = [], replacedUrls = 0 } = input;
12
- return {
13
- timestamp: new Date().toISOString(),
14
- duration: scanResult.duration,
15
- framework: scanResult.framework,
16
- filesScanned: scanResult.scannedFiles,
17
- hardcodedTexts: scanResult.detectedTexts.length,
18
- localeKeysGenerated: new Set(scanResult.detectedTexts.map((d) => d.suggestedKey)).size,
19
- unusedKeys: validationResult?.warnings.filter((w) => w.type === "unused-key").length || 0,
20
- missingTranslations: validationResult?.errors.filter((e) => e.type === "missing-key").length || 0,
21
- languages: [],
22
- assets: {
23
- totalAssets: scanResult.assets.length,
24
- uploadedAssets: uploadedAssets.length,
25
- replacedUrls,
26
- legacyCdnUrls: scanResult.legacyCdnUrls.length,
27
- },
28
- details: {
29
- detectedTexts: scanResult.detectedTexts,
30
- missingKeys: validationResult?.errors.filter((e) => e.type === "missing-key") || [],
31
- unusedKeysList: validationResult?.warnings.filter((w) => w.type === "unused-key").map((w) => w.key) || [],
32
- assets: uploadedAssets,
33
- },
34
- };
35
- }
package/tsconfig.json DELETED
@@ -1,6 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": { "outDir": "./dist", "rootDir": "./src" },
4
- "include": ["src/**/*"],
5
- "exclude": ["node_modules", "dist"]
6
- }