ai-localize-reporting 2.0.1 → 2.0.3

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.
@@ -0,0 +1,564 @@
1
+ /**
2
+ * Generates a self-contained preview of the modernized HTML dashboard.
3
+ * Run: node packages/reporting/preview/make-preview.cjs
4
+ */
5
+ 'use strict';
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
11
+ function esc(s) {
12
+ return String(s)
13
+ .replace(/&/g, '&')
14
+ .replace(/</g, '<')
15
+ .replace(/>/g, '>')
16
+ .replace(/"/g, '&#34;');
17
+ }
18
+
19
+ function badge(text, variant) {
20
+ variant = variant || 'blue';
21
+ return '<span class="badge badge-' + variant + '">' + esc(text) + '</span>';
22
+ }
23
+
24
+ function buildStatCard(value, label, hint, status, icon) {
25
+ return '<div class="stat-card stat-' + status + '">' +
26
+ '<div class="stat-icon">' + icon + '</div>' +
27
+ '<div class="stat-value">' + value + '</div>' +
28
+ '<div class="stat-label">' + esc(label) + '</div>' +
29
+ '<div class="stat-hint">' + esc(hint) + '</div>' +
30
+ '</div>';
31
+ }
32
+
33
+ function buildCoverageDonut(pct) {
34
+ var r = 52;
35
+ var circ = 2 * Math.PI * r;
36
+ var dash = (pct / 100) * circ;
37
+ var color = pct >= 80 ? '#22c55e' : pct >= 50 ? '#f59e0b' : '#ef4444';
38
+ return '<svg class="donut-chart" viewBox="0 0 120 120">' +
39
+ '<circle cx="60" cy="60" r="' + r + '" fill="none" stroke="var(--border)" stroke-width="12"/>' +
40
+ '<circle cx="60" cy="60" r="' + r + '" fill="none" stroke="' + color + '" stroke-width="12"' +
41
+ ' stroke-dasharray="' + dash.toFixed(2) + ' ' + circ.toFixed(2) + '"' +
42
+ ' stroke-dashoffset="' + (circ / 4).toFixed(2) + '"' +
43
+ ' stroke-linecap="round" transform="rotate(-90 60 60)"/>' +
44
+ '<text x="60" y="56" text-anchor="middle" font-size="20" font-weight="700" fill="' + color + '">' + pct + '%</text>' +
45
+ '<text x="60" y="72" text-anchor="middle" font-size="10" fill="var(--text-muted)">coverage</text>' +
46
+ '</svg>';
47
+ }
48
+
49
+ function buildBarChart(data) {
50
+ if (!data.length) return '<p class="empty-state">No data.</p>';
51
+ var max = data.reduce(function(m, d) { return Math.max(m, d.value); }, 1);
52
+ return '<div class="bar-chart">' + data.map(function(d) {
53
+ var pct = Math.round((d.value / max) * 100);
54
+ var color = d.color || 'var(--accent)';
55
+ return '<div class="bar-row">' +
56
+ '<span class="bar-label">' + esc(d.label) + '</span>' +
57
+ '<div class="bar-track"><div class="bar-fill" style="width:' + pct + '%;background:' + color + '"></div></div>' +
58
+ '<span class="bar-value">' + d.value + '</span>' +
59
+ '</div>';
60
+ }).join('') + '</div>';
61
+ }
62
+
63
+ function buildAccordion(id, title, subtitle, content, open, severity) {
64
+ open = open || false;
65
+ severity = severity || 'info';
66
+ return '<div class="accordion' + (open ? ' accordion-open' : '') + '" id="acc-' + id + '">' +
67
+ '<button class="accordion-trigger" aria-expanded="' + open + '" onclick="toggleAccordion(\'' + id + '\')">' +
68
+ '<span class="accordion-icon severity-' + severity + '">&#9679;</span>' +
69
+ '<span class="accordion-title">' + title + '</span>' +
70
+ '<span class="accordion-subtitle">' + subtitle + '</span>' +
71
+ '<span class="accordion-chevron">&#8964;</span>' +
72
+ '</button>' +
73
+ '<div class="accordion-panel" id="panel-' + id + '"' + (open ? '' : ' hidden') + '>' +
74
+ '<div class="accordion-body">' + content + '</div>' +
75
+ '</div></div>';
76
+ }
77
+
78
+ function buildTable(tableId, columns, rows) {
79
+ var thead = columns.map(function(col) {
80
+ return '<th onclick="sortTable(\'' + tableId + '\',\'' + col.key + '\')" class="sortable"' +
81
+ (col.width ? ' style="width:' + col.width + '"' : '') + '>' +
82
+ col.label + ' <span class="sort-icon">&#8597;</span></th>';
83
+ }).join('');
84
+
85
+ var tbody = rows.map(function(cells) {
86
+ return '<tr>' + cells.map(function(cell, ci) {
87
+ return '<td data-col="' + (columns[ci] ? columns[ci].key : ci) + '">' + cell + '</td>';
88
+ }).join('') + '</tr>';
89
+ }).join('\n');
90
+
91
+ return '<div class="table-wrapper">' +
92
+ '<div class="table-controls">' +
93
+ '<div class="search-box">' +
94
+ '<span class="search-icon">&#128269;</span>' +
95
+ '<input type="text" class="table-search" placeholder="Search&hellip;" oninput="filterTable(\'' + tableId + '\',this.value)" aria-label="Search"/>' +
96
+ '</div>' +
97
+ '<div class="table-meta" id="' + tableId + '-meta"></div>' +
98
+ '<div class="table-actions">' +
99
+ '<button class="btn btn-sm" onclick="exportTableCsv(\'' + tableId + '\')">&#8659; CSV</button>' +
100
+ '<button class="btn btn-sm" onclick="exportTableJson(\'' + tableId + '\')">&#8659; JSON</button>' +
101
+ '</div></div>' +
102
+ '<div class="table-scroll">' +
103
+ '<table id="' + tableId + '" data-page-size="50" data-page="1">' +
104
+ '<thead><tr>' + thead + '</tr></thead>' +
105
+ '<tbody>' + tbody + '</tbody>' +
106
+ '</table></div>' +
107
+ '<div class="table-pagination" id="' + tableId + '-pagination"></div>' +
108
+ '</div>';
109
+ }
110
+
111
+ // ─── Insights computation ─────────────────────────────────────────────────────
112
+ function computeInsights(report) {
113
+ var details = report.details;
114
+
115
+ var textMap = {};
116
+ details.detectedTexts.forEach(function(dt) {
117
+ var t = dt.text.trim();
118
+ if (!textMap[t]) textMap[t] = { keys: {}, files: {} };
119
+ textMap[t].keys[dt.suggestedKey] = true;
120
+ textMap[t].files[dt.filePath] = true;
121
+ });
122
+ var duplicates = [];
123
+ Object.keys(textMap).forEach(function(text) {
124
+ var entry = textMap[text];
125
+ var keys = Object.keys(entry.keys);
126
+ if (keys.length > 1) {
127
+ duplicates.push({ text: text, count: keys.length, keys: keys, files: Object.keys(entry.files) });
128
+ }
129
+ });
130
+ duplicates.sort(function(a, b) { return b.count - a.count; });
131
+
132
+ var keyTextMap = {};
133
+ details.detectedTexts.forEach(function(dt) {
134
+ if (!keyTextMap[dt.suggestedKey]) keyTextMap[dt.suggestedKey] = {};
135
+ keyTextMap[dt.suggestedKey][dt.text.trim()] = true;
136
+ });
137
+ var duplicateKeyCount = 0;
138
+ Object.keys(keyTextMap).forEach(function(k) {
139
+ if (Object.keys(keyTextMap[k]).length > 1) duplicateKeyCount++;
140
+ });
141
+
142
+ var byLang = {};
143
+ details.missingKeys.forEach(function(mk) {
144
+ if (mk.language) { if (!byLang[mk.language]) byLang[mk.language] = []; byLang[mk.language].push(mk.key); }
145
+ });
146
+
147
+ var nsCounts = {};
148
+ details.detectedTexts.forEach(function(dt) {
149
+ var ns = dt.suggestedKey.split('.')[0] || 'default';
150
+ nsCounts[ns] = (nsCounts[ns] || 0) + 1;
151
+ });
152
+ var namespaceHints = [];
153
+ Object.keys(nsCounts).forEach(function(ns) {
154
+ if (nsCounts[ns] < 3) namespaceHints.push({ namespace: ns, count: nsCounts[ns] });
155
+ });
156
+
157
+ var allKeys = {};
158
+ details.detectedTexts.forEach(function(dt) { allKeys[dt.suggestedKey] = true; });
159
+ var totalKeys = Object.keys(allKeys).length;
160
+ var missing = details.missingKeys.length;
161
+ var coveragePct = totalKeys > 0 ? Math.round(((totalKeys - missing) / totalKeys) * 100) : 100;
162
+
163
+ return { duplicates: duplicates, namespaceHints: namespaceHints, coveragePct: coveragePct, totalKeys: totalKeys, duplicateKeyCount: duplicateKeyCount, byLang: byLang, nsCounts: nsCounts };
164
+ }
165
+
166
+ // ─── Mock report ──────────────────────────────────────────────────────────────
167
+ var mockReport = {
168
+ timestamp: new Date().toISOString(),
169
+ duration: 2340,
170
+ framework: 'react-vite',
171
+ filesScanned: 148,
172
+ hardcodedTexts: 37,
173
+ localeKeysGenerated: 29,
174
+ unusedKeys: 4,
175
+ missingTranslations: 12,
176
+ languages: ['en', 'fr', 'de', 'es', 'ja'],
177
+ assets: { totalAssets: 23, uploadedAssets: 18, replacedUrls: 15, legacyCdnUrls: 5 },
178
+ details: {
179
+ detectedTexts: [
180
+ { filePath: '/app/src/components/Button/PrimaryButton.tsx', line: 14, column: 8, text: 'Submit', suggestedKey: 'common.submit', context: 'button', nodeType: 'JSXAttribute', alreadyTranslated: false },
181
+ { filePath: '/app/src/components/Button/PrimaryButton.tsx', line: 22, column: 12, text: 'Cancel', suggestedKey: 'common.cancel', context: 'button', nodeType: 'JSXAttribute', alreadyTranslated: false },
182
+ { filePath: '/app/src/pages/Dashboard/Overview.tsx', line: 31, column: 6, text: 'Welcome back!', suggestedKey: 'dashboard.welcomeBack', context: 'heading', nodeType: 'JSXText', alreadyTranslated: false },
183
+ { filePath: '/app/src/pages/Dashboard/Overview.tsx', line: 45, column: 10, text: 'Total Revenue', suggestedKey: 'dashboard.totalRevenue', context: 'table-header', nodeType: 'JSXText', alreadyTranslated: false },
184
+ { filePath: '/app/src/pages/Dashboard/Overview.tsx', line: 67, column: 14, text: 'No data available', suggestedKey: 'dashboard.noData', context: 'jsx-text', nodeType: 'JSXText', alreadyTranslated: false },
185
+ { filePath: '/app/src/components/Modal/ConfirmModal.tsx', line: 8, column: 4, text: 'Are you sure?', suggestedKey: 'modal.confirmTitle', context: 'modal', nodeType: 'JSXText', alreadyTranslated: false },
186
+ { filePath: '/app/src/components/Modal/ConfirmModal.tsx', line: 19, column: 8, text: 'This action cannot be undone.', suggestedKey: 'modal.confirmBody', context: 'modal', nodeType: 'JSXText', alreadyTranslated: false },
187
+ { filePath: '/app/src/components/Modal/ConfirmModal.tsx', line: 32, column: 12, text: 'Delete', suggestedKey: 'common.delete', context: 'button', nodeType: 'JSXAttribute', alreadyTranslated: false },
188
+ { filePath: '/app/src/forms/LoginForm.tsx', line: 12, column: 6, text: 'Email address', suggestedKey: 'auth.emailLabel', context: 'label', nodeType: 'JSXText', alreadyTranslated: false },
189
+ { filePath: '/app/src/forms/LoginForm.tsx', line: 18, column: 6, text: 'Password', suggestedKey: 'auth.passwordLabel', context: 'label', nodeType: 'JSXText', alreadyTranslated: false },
190
+ { filePath: '/app/src/forms/LoginForm.tsx', line: 24, column: 10, text: 'Enter your email', suggestedKey: 'auth.emailPlaceholder', context: 'placeholder', nodeType: 'JSXAttribute', alreadyTranslated: false },
191
+ { filePath: '/app/src/forms/LoginForm.tsx', line: 30, column: 10, text: 'Enter your password', suggestedKey: 'auth.passwordPlaceholder', context: 'placeholder', nodeType: 'JSXAttribute', alreadyTranslated: false },
192
+ { filePath: '/app/src/forms/LoginForm.tsx', line: 56, column: 8, text: 'Sign in', suggestedKey: 'auth.signIn', context: 'button', nodeType: 'JSXAttribute', alreadyTranslated: false },
193
+ { filePath: '/app/src/forms/LoginForm.tsx', line: 60, column: 12, text: 'Forgot password?', suggestedKey: 'auth.forgotPassword', context: 'jsx-text', nodeType: 'JSXText', alreadyTranslated: false },
194
+ { filePath: '/app/src/components/Nav/Sidebar.tsx', line: 22, column: 6, text: 'Dashboard', suggestedKey: 'nav.dashboard', context: 'jsx-text', nodeType: 'JSXText', alreadyTranslated: false },
195
+ { filePath: '/app/src/components/Nav/Sidebar.tsx', line: 30, column: 6, text: 'Settings', suggestedKey: 'nav.settings', context: 'jsx-text', nodeType: 'JSXText', alreadyTranslated: false },
196
+ { filePath: '/app/src/components/Nav/Sidebar.tsx', line: 38, column: 6, text: 'Users', suggestedKey: 'nav.users', context: 'jsx-text', nodeType: 'JSXText', alreadyTranslated: false },
197
+ { filePath: '/app/src/components/Nav/Sidebar.tsx', line: 46, column: 6, text: 'Reports', suggestedKey: 'nav.reports', context: 'jsx-text', nodeType: 'JSXText', alreadyTranslated: false },
198
+ { filePath: '/app/src/components/Nav/Sidebar.tsx', line: 54, column: 6, text: 'Log out', suggestedKey: 'nav.logout', context: 'jsx-text', nodeType: 'JSXText', alreadyTranslated: false },
199
+ { filePath: '/app/src/pages/Settings/ProfileSettings.tsx', line: 11, column: 4, text: 'Profile Settings', suggestedKey: 'settings.profileTitle', context: 'heading', nodeType: 'JSXText', alreadyTranslated: false },
200
+ { filePath: '/app/src/pages/Settings/ProfileSettings.tsx', line: 28, column: 8, text: 'First name', suggestedKey: 'settings.firstName', context: 'label', nodeType: 'JSXText', alreadyTranslated: false },
201
+ { filePath: '/app/src/pages/Settings/ProfileSettings.tsx', line: 36, column: 8, text: 'Last name', suggestedKey: 'settings.lastName', context: 'label', nodeType: 'JSXText', alreadyTranslated: false },
202
+ { filePath: '/app/src/pages/Settings/ProfileSettings.tsx', line: 44, column: 8, text: 'Save changes', suggestedKey: 'common.saveChanges', context: 'button', nodeType: 'JSXAttribute', alreadyTranslated: false },
203
+ { filePath: '/app/src/components/Toast/ToastManager.tsx', line: 9, column: 6, text: 'Changes saved successfully', suggestedKey: 'toast.saveSuccess', context: 'toast', nodeType: 'string-literal', alreadyTranslated: false },
204
+ { filePath: '/app/src/components/Toast/ToastManager.tsx', line: 15, column: 6, text: 'An error occurred. Please try again.', suggestedKey: 'toast.genericError', context: 'toast', nodeType: 'string-literal', alreadyTranslated: false },
205
+ { filePath: '/app/src/components/Toast/ToastManager.tsx', line: 21, column: 6, text: 'Network connection lost', suggestedKey: 'toast.networkError', context: 'toast', nodeType: 'string-literal', alreadyTranslated: false },
206
+ { filePath: '/app/src/pages/Users/UserTable.tsx', line: 17, column: 6, text: 'Name', suggestedKey: 'users.columnName', context: 'table-header', nodeType: 'JSXText', alreadyTranslated: false },
207
+ { filePath: '/app/src/pages/Users/UserTable.tsx', line: 24, column: 6, text: 'Email', suggestedKey: 'users.columnEmail', context: 'table-header', nodeType: 'JSXText', alreadyTranslated: false },
208
+ { filePath: '/app/src/pages/Users/UserTable.tsx', line: 31, column: 6, text: 'Role', suggestedKey: 'users.columnRole', context: 'table-header', nodeType: 'JSXText', alreadyTranslated: false },
209
+ { filePath: '/app/src/pages/Users/UserTable.tsx', line: 38, column: 6, text: 'Status', suggestedKey: 'users.columnStatus', context: 'table-header', nodeType: 'JSXText', alreadyTranslated: false },
210
+ { filePath: '/app/src/pages/Users/UserTable.tsx', line: 68, column: 10, text: 'No users found', suggestedKey: 'users.emptyState', context: 'jsx-text', nodeType: 'JSXText', alreadyTranslated: false },
211
+ { filePath: '/app/src/components/Alert/ErrorBanner.tsx', line: 7, column: 4, text: 'Something went wrong', suggestedKey: 'alert.errorTitle', context: 'alert', nodeType: 'JSXText', alreadyTranslated: false },
212
+ { filePath: '/app/src/components/Alert/ErrorBanner.tsx', line: 13, column: 8, text: 'Please refresh the page', suggestedKey: 'alert.refreshHint', context: 'jsx-text', nodeType: 'JSXText', alreadyTranslated: false },
213
+ { filePath: '/app/src/components/Tooltip/HelpTooltip.tsx', line: 5, column: 6, text: 'Click for more information', suggestedKey: 'tooltip.helpInfo', context: 'tooltip', nodeType: 'JSXAttribute', alreadyTranslated: false },
214
+ { filePath: '/app/src/components/DataGrid/DataGrid.tsx', line: 42, column: 8, text: 'Loading data...', suggestedKey: 'grid.loading', context: 'jsx-text', nodeType: 'JSXText', alreadyTranslated: false },
215
+ { filePath: '/app/src/components/DataGrid/DataGrid.tsx', line: 58, column: 8, text: 'No results match your search', suggestedKey: 'grid.noResults', context: 'jsx-text', nodeType: 'JSXText', alreadyTranslated: false },
216
+ // Duplicate text scenarios (Submit/Cancel reused in different namespace)
217
+ { filePath: '/app/src/pages/Reports/ReportHeader.tsx', line: 10, column: 6, text: 'Submit', suggestedKey: 'reports.submit', context: 'button', nodeType: 'JSXAttribute', alreadyTranslated: false },
218
+ { filePath: '/app/src/pages/Reports/ReportHeader.tsx', line: 22, column: 8, text: 'Cancel', suggestedKey: 'reports.cancel', context: 'button', nodeType: 'JSXAttribute', alreadyTranslated: false },
219
+ ],
220
+ missingKeys: [
221
+ { type: 'missing-key', key: 'common.submit', language: 'fr', message: 'Key missing in fr locale', filePath: '/app/locales/fr/common.json' },
222
+ { type: 'missing-key', key: 'common.cancel', language: 'fr', message: 'Key missing in fr locale', filePath: '/app/locales/fr/common.json' },
223
+ { type: 'missing-key', key: 'auth.signIn', language: 'fr', message: 'Key missing in fr locale', filePath: '/app/locales/fr/auth.json' },
224
+ { type: 'missing-key', key: 'dashboard.welcomeBack', language: 'de', message: 'Key missing in de locale', filePath: '/app/locales/de/dashboard.json' },
225
+ { type: 'missing-key', key: 'dashboard.totalRevenue', language: 'de', message: 'Key missing in de locale', filePath: '/app/locales/de/dashboard.json' },
226
+ { type: 'missing-key', key: 'modal.confirmTitle', language: 'de', message: 'Key missing in de locale', filePath: '/app/locales/de/modal.json' },
227
+ { type: 'missing-key', key: 'modal.confirmBody', language: 'de', message: 'Key missing in de locale', filePath: '/app/locales/de/modal.json' },
228
+ { type: 'missing-key', key: 'nav.logout', language: 'es', message: 'Key missing in es locale', filePath: '/app/locales/es/nav.json' },
229
+ { type: 'missing-key', key: 'settings.profileTitle', language: 'es', message: 'Key missing in es locale', filePath: '/app/locales/es/settings.json' },
230
+ { type: 'missing-key', key: 'toast.genericError', language: 'ja', message: 'Key missing in ja locale', filePath: '/app/locales/ja/toast.json' },
231
+ { type: 'missing-key', key: 'toast.saveSuccess', language: 'ja', message: 'Key missing in ja locale', filePath: '/app/locales/ja/toast.json' },
232
+ { type: 'missing-key', key: 'users.emptyState', language: 'ja', message: 'Key missing in ja locale', filePath: '/app/locales/ja/users.json' },
233
+ ],
234
+ unusedKeysList: ['common.oldButton', 'dashboard.legacyWidget', 'auth.ssoLogin', 'settings.betaFeature'],
235
+ assets: [
236
+ { localPath: '/app/public/images/logo.png', s3Key: 'assets/images/logo.png', hash: 'abc123', cloudfrontUrl: 'https://d1abc123.cloudfront.net/assets/images/logo.png', contentType: 'image/png', size: 24576 },
237
+ { localPath: '/app/public/images/hero-banner.webp', s3Key: 'assets/images/hero-banner.webp', hash: 'def456', cloudfrontUrl: 'https://d1abc123.cloudfront.net/assets/images/hero-banner.webp', contentType: 'image/webp', size: 102400 },
238
+ { localPath: '/app/public/fonts/Inter-Regular.woff2', s3Key: 'assets/fonts/Inter-Regular.woff2', hash: 'ghi789', cloudfrontUrl: 'https://d1abc123.cloudfront.net/assets/fonts/Inter-Regular.woff2', contentType: 'font/woff2', size: 65536 },
239
+ { localPath: '/app/public/fonts/Inter-Bold.woff2', s3Key: 'assets/fonts/Inter-Bold.woff2', hash: 'jkl012', cloudfrontUrl: 'https://d1abc123.cloudfront.net/assets/fonts/Inter-Bold.woff2', contentType: 'font/woff2', size: 69120 },
240
+ { localPath: '/app/public/icons/favicon.svg', s3Key: 'assets/icons/favicon.svg', hash: 'mno345', cloudfrontUrl: 'https://d1abc123.cloudfront.net/assets/icons/favicon.svg', contentType: 'image/svg+xml', size: 4096 },
241
+ { localPath: '/app/public/videos/onboarding.mp4', s3Key: 'assets/videos/onboarding.mp4', hash: 'pqr678', cloudfrontUrl: 'https://d1abc123.cloudfront.net/assets/videos/onboarding.mp4', contentType: 'video/mp4', size: 5242880 },
242
+ ],
243
+ },
244
+ };
245
+
246
+ // ─── Build HTML ───────────────────────────────────────────────────────────────
247
+ var r = mockReport;
248
+ var ins = computeInsights(r);
249
+ var scanDate = new Date(r.timestamp).toLocaleString();
250
+
251
+ // Namespace chart data
252
+ var nsEntries = Object.keys(ins.nsCounts).map(function(k) { return { label: k, value: ins.nsCounts[k] }; });
253
+ nsEntries.sort(function(a, b) { return b.value - a.value; });
254
+ var nsChart = buildBarChart(nsEntries.slice(0, 10));
255
+
256
+ // Context chart data
257
+ var ctxCounts = {};
258
+ r.details.detectedTexts.forEach(function(dt) { ctxCounts[dt.context] = (ctxCounts[dt.context] || 0) + 1; });
259
+ var ctxEntries = Object.keys(ctxCounts).map(function(k) { return { label: k, value: ctxCounts[k] }; });
260
+ ctxEntries.sort(function(a, b) { return b.value - a.value; });
261
+ var ctxChart = buildBarChart(ctxEntries);
262
+
263
+ // Missing by lang chart
264
+ var missingLangEntries = Object.keys(ins.byLang).map(function(k) { return { label: k, value: ins.byLang[k].length, color: '#ef4444' }; });
265
+ missingLangEntries.sort(function(a, b) { return b.value - a.value; });
266
+
267
+ // Summary cards
268
+ var summaryCards = '<div class="stats-grid">' +
269
+ buildStatCard(r.filesScanned, 'Files Scanned', 'Source files processed', 'neutral', '&#128196;') +
270
+ buildStatCard(r.hardcodedTexts, 'Hardcoded Texts', 'Raw strings not yet in t()', r.hardcodedTexts > 0 ? 'warn' : 'ok', '&#128269;') +
271
+ buildStatCard(ins.totalKeys, 'Unique Keys', 'Deduplicated locale keys', 'info', '&#128273;') +
272
+ buildStatCard(r.missingTranslations, 'Missing Translations', 'Absent in target languages', r.missingTranslations > 0 ? 'err' : 'ok', '&#10060;') +
273
+ buildStatCard(r.unusedKeys, 'Unused Keys', 'In locale but not in source', r.unusedKeys > 0 ? 'warn' : 'ok', '&#128465;') +
274
+ buildStatCard(ins.duplicateKeyCount, 'Duplicate Keys', 'Same key, different texts', ins.duplicateKeyCount > 0 ? 'warn' : 'ok', '&#128258;') +
275
+ buildStatCard(ins.coveragePct + '%', 'Translation Coverage', '% of keys in all languages', ins.coveragePct < 80 ? 'err' : ins.coveragePct < 100 ? 'warn' : 'ok', '&#127919;') +
276
+ buildStatCard(r.assets.totalAssets, 'Assets Found', 'Static asset references', 'neutral', '&#128230;') +
277
+ buildStatCard(r.assets.legacyCdnUrls, 'Legacy CDN URLs', 'Old CDN refs pending', r.assets.legacyCdnUrls > 0 ? 'warn' : 'ok', '&#128279;') +
278
+ '</div>';
279
+
280
+ // Hardcoded texts table
281
+ var hardcodedRows = r.details.detectedTexts.slice(0, 200).map(function(t) {
282
+ return [
283
+ '<code class="path-code">' + esc(t.filePath.split('/').slice(-3).join('/')) + '</code>',
284
+ '<span class="line-num">' + t.line + '</span>',
285
+ '<span class="text-preview">' + esc(t.text.slice(0, 60)) + (t.text.length > 60 ? '&hellip;' : '') + '</span>',
286
+ '<code class="key-code">' + esc(t.suggestedKey) + '</code>',
287
+ badge(t.context, 'blue'),
288
+ badge(t.nodeType, 'grey'),
289
+ ];
290
+ });
291
+ var hardcodedTable = buildTable('tbl-hardcoded',
292
+ [{ key: 'file', label: 'File' }, { key: 'line', label: 'Line', width: '60px' }, { key: 'text', label: 'Text' }, { key: 'key', label: 'Suggested Key' }, { key: 'ctx', label: 'Context', width: '120px' }, { key: 'node', label: 'Node', width: '120px' }],
293
+ hardcodedRows
294
+ );
295
+ var hardcodedContent = '<div class="insight-legend">Raw text strings not yet wrapped in a translation call. Run <code>ai-localize full-migrate</code> to wrap them automatically.</div>' + hardcodedTable;
296
+
297
+ // Missing translations table
298
+ var missingByLang = {};
299
+ r.details.missingKeys.forEach(function(mk) {
300
+ if (mk.language) { if (!missingByLang[mk.language]) missingByLang[mk.language] = []; missingByLang[mk.language].push(mk); }
301
+ });
302
+ var langChips = Object.keys(missingByLang).map(function(lang) {
303
+ return '<span class="chip chip-red" onclick="filterTable(\'tbl-missing\',\'' + lang + '\')">' + esc(lang) + ' <strong>' + missingByLang[lang].length + '</strong></span>';
304
+ }).join(' ');
305
+ var missingRows = r.details.missingKeys.map(function(e) {
306
+ return [
307
+ '<code class="key-code">' + esc(e.key) + '</code>',
308
+ e.language ? badge(e.language, 'red') : '&mdash;',
309
+ e.filePath ? '<code class="path-code">' + esc(e.filePath.split('/').slice(-2).join('/')) + '</code>' : '&mdash;',
310
+ '<span class="detail-text">' + esc(e.message) + '</span>',
311
+ ];
312
+ });
313
+ var missingContent = '<div class="insight-legend">Keys in the default language but absent in target language files. Run <code>ai-localize extract</code> to seed them.</div>' +
314
+ '<div class="chip-row">Filter by language: ' + langChips + '</div>' +
315
+ buildTable('tbl-missing', [{ key: 'key', label: 'Key' }, { key: 'lang', label: 'Language', width: '100px' }, { key: 'file', label: 'Locale File' }, { key: 'msg', label: 'Details' }], missingRows);
316
+
317
+ // Unused keys table
318
+ var unusedContent = r.details.unusedKeysList.length === 0
319
+ ? '<div class="empty-state-card">&#9989; No unused keys detected.</div>'
320
+ : '<div class="insight-legend">Keys in locale files not referenced in source code. Run <code>ai-localize cleanup</code>.</div>' +
321
+ buildTable('tbl-unused', [{ key: 'key', label: 'Unused Key' }], r.details.unusedKeysList.map(function(k) { return ['<code class="key-code">' + esc(k) + '</code>']; }));
322
+
323
+ // Assets table
324
+ var assetRows = r.details.assets.map(function(a) {
325
+ return [
326
+ '<code class="path-code">' + esc(a.localPath.split('/').slice(-3).join('/')) + '</code>',
327
+ '<code class="key-code">' + esc(a.s3Key) + '</code>',
328
+ '<a href="' + esc(a.cloudfrontUrl) + '" target="_blank" class="cdn-link">' + esc(a.cloudfrontUrl.slice(0, 55)) + (a.cloudfrontUrl.length > 55 ? '&hellip;' : '') + '</a>',
329
+ '<span class="file-size">' + (a.size / 1024).toFixed(1) + ' KB</span>',
330
+ badge(a.contentType, 'grey'),
331
+ ];
332
+ });
333
+ var assetsContent = '<div class="asset-summary-row">' +
334
+ buildStatCard(r.assets.totalAssets, 'Total Assets', '', 'neutral', '&#128190;') +
335
+ buildStatCard(r.assets.uploadedAssets, 'Uploaded', 'Pushed to S3/CDN', 'ok', '&#9989;') +
336
+ buildStatCard(r.assets.replacedUrls, 'URLs Replaced', '', 'ok', '&#128257;') +
337
+ buildStatCard(r.assets.legacyCdnUrls, 'Legacy URLs', 'run replace-cdn', r.assets.legacyCdnUrls > 0 ? 'warn' : 'ok', '&#9888;') +
338
+ '</div>' +
339
+ buildTable('tbl-assets', [{ key: 'path', label: 'Local Path' }, { key: 's3', label: 'S3 Key' }, { key: 'url', label: 'CloudFront URL' }, { key: 'size', label: 'Size', width: '80px' }, { key: 'type', label: 'Type', width: '140px' }], assetRows);
340
+
341
+ // AI Insights
342
+ var dupContent = ins.duplicates.length === 0
343
+ ? '<div class="insight-item insight-ok">&#9989; No duplicate texts detected.</div>'
344
+ : '<div class="insight-legend">Same literal text mapped to multiple keys. Consider consolidating.</div>' +
345
+ buildTable('tbl-dups', [{ key: 'text', label: 'Text' }, { key: 'count', label: 'Key Count', width: '90px' }, { key: 'keys', label: 'Keys' }],
346
+ ins.duplicates.slice(0, 50).map(function(d) {
347
+ return [
348
+ '<span class="text-preview">' + esc(d.text.slice(0, 60)) + '</span>',
349
+ '<span class="badge badge-orange">' + d.count + '</span>',
350
+ d.keys.map(function(k) { return '<code class="key-code">' + esc(k) + '</code>'; }).join(' '),
351
+ ];
352
+ })
353
+ );
354
+
355
+ var inconsContent = missingLangEntries.length === 0
356
+ ? '<div class="insight-item insight-ok">&#9989; All translations present.</div>'
357
+ : '<div class="insight-legend">Languages with the most missing translations — priority for your translators.</div>' + buildBarChart(missingLangEntries);
358
+
359
+ var unusedInsight = r.unusedKeys === 0
360
+ ? '<div class="insight-item insight-ok">&#9989; No unused keys.</div>'
361
+ : '<div class="insight-legend">' + r.unusedKeys + ' key' + (r.unusedKeys > 1 ? 's' : '') + ' in locale files with no source references. Run <code>ai-localize cleanup</code>.</div>' +
362
+ buildBarChart(r.details.unusedKeysList.slice(0, 8).map(function(k) { return { label: k.length > 36 ? k.slice(0, 36) + '...' : k, value: 1, color: '#f59e0b' }; }));
363
+
364
+ var nsCleanup = ins.namespaceHints.length === 0
365
+ ? '<div class="insight-item insight-ok">&#9989; All namespaces have healthy key counts.</div>'
366
+ : '<ul class="insight-list">' + ins.namespaceHints.map(function(h) {
367
+ return '<li><code class="key-code">' + esc(h.namespace) + '</code> ' + badge(h.count + ' keys', 'orange') + ' &mdash; only ' + h.count + ' key' + (h.count > 1 ? 's' : '') + ' &mdash; consider merging.</li>';
368
+ }).join('') + '</ul>';
369
+
370
+ var insightsContent = '<div class="insights-grid">' +
371
+ '<div class="insight-card"><h3 class="insight-heading">&#128258; Duplicate Text Detection</h3><p class="insight-count">' + ins.duplicates.length + ' duplicate group(s)</p>' + dupContent + '</div>' +
372
+ '<div class="insight-card"><h3 class="insight-heading">&#127757; Translation Inconsistencies</h3><p class="insight-count">' + r.missingTranslations + ' missing translations across all languages</p>' + inconsContent + '</div>' +
373
+ '<div class="insight-card"><h3 class="insight-heading">&#128465; Unused Key Analysis</h3><p class="insight-count">' + r.unusedKeys + ' unused key(s)</p>' + unusedInsight + '</div>' +
374
+ '<div class="insight-card"><h3 class="insight-heading">&#127800; Namespace Cleanup</h3><p class="insight-count">' + ins.namespaceHints.length + ' namespace(s) to consolidate</p>' + nsCleanup + '</div>' +
375
+ '</div>';
376
+
377
+ // Diff explainer
378
+ var diff = Math.abs(r.hardcodedTexts - r.localeKeysGenerated);
379
+ var diffBanner = r.hardcodedTexts !== r.localeKeysGenerated
380
+ ? '<div class="info-banner info-banner-blue"><strong>&#8505; Why do Hardcoded Texts (' + r.hardcodedTexts + ') and Keys Generated (' + r.localeKeysGenerated + ') differ?</strong><ul><li><strong>Hardcoded Texts</strong> = total raw string occurrences (same string in 5 files = 5).</li><li><strong>Keys Generated</strong> = unique keys after deduplication.</li><li>Difference (' + diff + ') = duplicate strings consolidated into shared keys.</li></ul></div>'
381
+ : '<div class="info-banner info-banner-green"><strong>&#9989; Hardcoded Texts and Keys Generated match (' + r.hardcodedTexts + ').</strong> Every detected string maps to a unique locale key.</div>';
382
+
383
+ // Navigation
384
+ var nav = '<nav class="sidebar" id="sidebar">' +
385
+ '<div class="sidebar-header"><span class="sidebar-logo">&#127760;</span><span class="sidebar-brand">ai-localize</span><button class="sidebar-toggle" onclick="toggleSidebar()">&#9776;</button></div>' +
386
+ '<div class="sidebar-meta"><div>' + badge(r.framework, 'blue') + '</div><div class="sidebar-date">' + esc(scanDate) + '</div></div>' +
387
+ '<div class="nav-group"><div class="nav-group-label">Overview</div>' +
388
+ '<a href="#overview" class="nav-item" data-section="overview"><span class="nav-icon">&#128202;</span><span class="nav-label">Summary</span><span class="nav-count"></span></a>' +
389
+ '<a href="#charts" class="nav-item" data-section="charts"><span class="nav-icon">&#128200;</span><span class="nav-label">Charts</span><span class="nav-count"></span></a>' +
390
+ '</div>' +
391
+ '<div class="nav-group"><div class="nav-group-label">Analysis</div>' +
392
+ '<a href="#hardcoded" class="nav-item' + (r.hardcodedTexts > 0 ? ' nav-alert' : '') + '" data-section="hardcoded"><span class="nav-icon">&#128269;</span><span class="nav-label">Hardcoded Texts</span><span class="nav-count">' + r.hardcodedTexts + '</span></a>' +
393
+ '<a href="#missing" class="nav-item' + (r.missingTranslations > 0 ? ' nav-alert' : '') + '" data-section="missing"><span class="nav-icon">&#10060;</span><span class="nav-label">Missing Trans.</span><span class="nav-count">' + r.missingTranslations + '</span></a>' +
394
+ '<a href="#unused" class="nav-item' + (r.unusedKeys > 0 ? ' nav-alert' : '') + '" data-section="unused"><span class="nav-icon">&#128465;</span><span class="nav-label">Unused Keys</span><span class="nav-count">' + r.unusedKeys + '</span></a>' +
395
+ '<a href="#assets" class="nav-item" data-section="assets"><span class="nav-icon">&#128230;</span><span class="nav-label">Assets</span><span class="nav-count">' + r.assets.totalAssets + '</span></a>' +
396
+ '</div>' +
397
+ '<div class="nav-group"><div class="nav-group-label">AI Insights</div>' +
398
+ '<a href="#insights" class="nav-item' + (ins.duplicates.length > 0 ? ' nav-alert' : '') + '" data-section="insights"><span class="nav-icon">&#129504;</span><span class="nav-label">AI Insights</span><span class="nav-count">' + (ins.duplicates.length + ins.namespaceHints.length) + '</span></a>' +
399
+ '</div>' +
400
+ '<div class="nav-group"><div class="nav-group-label">Export</div>' +
401
+ '<button class="nav-item nav-btn" onclick="exportFullJson()">&#8659; Export JSON</button>' +
402
+ '<button class="nav-item nav-btn" onclick="exportFullCsv()">&#8659; Export CSV</button>' +
403
+ '<button class="nav-item nav-btn" onclick="window.print()">&#128424; Print / PDF</button>' +
404
+ '</div>' +
405
+ '</nav>';
406
+
407
+ // Read the CSS from the source file to embed it
408
+ var srcFile = path.join(__dirname, '..', 'src', 'html-reporter.ts');
409
+ var srcContent = fs.readFileSync(srcFile, 'utf8');
410
+ // Extract CSS between const CSS = ` and `; at end
411
+ var cssMatch = srcContent.match(/const CSS = `([\s\S]*?)`;/);
412
+ var extractedCSS = cssMatch ? cssMatch[1] : '';
413
+
414
+ // Read the JS from the source file
415
+ var jsMatch = srcContent.match(/function JS\(report, insights\): string \{[\s\S]*?return `([\s\S]*?)`;[\s\S]*?^}/m);
416
+
417
+ // Use inline JS instead since extracting is complex
418
+ var inlineJS = buildInlineJS(r, ins);
419
+
420
+ function buildInlineJS(report, insights) {
421
+ return "(function() {\n'use strict';\n" +
422
+ // Theme
423
+ "var THEME_KEY='ai-localize-theme';\n" +
424
+ "function applyTheme(t){document.documentElement.setAttribute('data-theme',t);var icon=document.getElementById('theme-icon');if(icon)icon.textContent=t==='dark'?'\\u2600':'\\u263E';localStorage.setItem(THEME_KEY,t);}\n" +
425
+ "window.toggleTheme=function(){var cur=document.documentElement.getAttribute('data-theme');applyTheme(cur==='dark'?'light':'dark');};\n" +
426
+ "var saved=localStorage.getItem(THEME_KEY);if(saved)applyTheme(saved);else if(window.matchMedia&&window.matchMedia('(prefers-color-scheme:dark)').matches)applyTheme('dark');\n" +
427
+ // Sidebar
428
+ "window.toggleSidebar=function(){document.getElementById('sidebar').classList.toggle('sidebar-collapsed');document.getElementById('main').classList.toggle('main-expanded');};\n" +
429
+ // Nav active
430
+ "var sections=document.querySelectorAll('section[id]');var navItems=document.querySelectorAll('.nav-item[data-section]');\n" +
431
+ "function onScroll(){var y=window.scrollY+80;var cur='';sections.forEach(function(s){if(y>=s.offsetTop)cur=s.id;});navItems.forEach(function(a){a.classList.toggle('nav-active',a.getAttribute('data-section')===cur);});}\n" +
432
+ "window.addEventListener('scroll',onScroll,{passive:true});onScroll();\n" +
433
+ // Accordion
434
+ "window.toggleAccordion=function(id){var acc=document.getElementById('acc-'+id);var panel=document.getElementById('panel-'+id);var btn=acc.querySelector('.accordion-trigger');var open=acc.classList.toggle('accordion-open');btn.setAttribute('aria-expanded',open);if(open)panel.removeAttribute('hidden');else panel.setAttribute('hidden','');};\n" +
435
+ "window.expandAll=function(){document.querySelectorAll('.accordion').forEach(function(acc){var id=acc.id.replace('acc-','');var panel=document.getElementById('panel-'+id);var btn=acc.querySelector('.accordion-trigger');acc.classList.add('accordion-open');btn.setAttribute('aria-expanded','true');if(panel)panel.removeAttribute('hidden');});};\n" +
436
+ "window.collapseAll=function(){document.querySelectorAll('.accordion').forEach(function(acc){var id=acc.id.replace('acc-','');var panel=document.getElementById('panel-'+id);var btn=acc.querySelector('.accordion-trigger');acc.classList.remove('accordion-open');btn.setAttribute('aria-expanded','false');if(panel)panel.setAttribute('hidden','');});};\n" +
437
+ // Filter
438
+ "window.filterTable=function(tableId,query){var tbl=document.getElementById(tableId);if(!tbl)return;var q=query.toLowerCase();var rows=tbl.querySelectorAll('tbody tr');var shown=0;rows.forEach(function(row){var match=row.textContent.toLowerCase().indexOf(q)!==-1;row.style.display=match?'':'none';if(match)shown++;});var meta=document.getElementById(tableId+'-meta');if(meta)meta.textContent=shown+' / '+rows.length+' rows';renderPagination(tableId);};\n" +
439
+ // Sort
440
+ "var sortState={};\n" +
441
+ "window.sortTable=function(tableId,col){var tbl=document.getElementById(tableId);if(!tbl)return;var dir=(sortState[tableId]===col+'_asc')?'desc':'asc';sortState[tableId]=col+'_'+dir;var colIndex=-1;tbl.querySelectorAll('thead th').forEach(function(th,i){if(th.getAttribute('data-col')===col)colIndex=i;var si=th.querySelector('.sort-icon');if(si)si.textContent='\\u21C5';});if(colIndex===-1)return;var th=tbl.querySelector('thead th[data-col=\"'+col+'\"]');if(th){var si=th.querySelector('.sort-icon');if(si)si.textContent=dir==='asc'?'\\u2191':'\\u2193';}var tbody=tbl.querySelector('tbody');var rows=Array.from(tbody.querySelectorAll('tr'));rows.sort(function(a,b){var av=(a.cells[colIndex]||{}).textContent||'';var bv=(b.cells[colIndex]||{}).textContent||'';var an=parseFloat(av),bn=parseFloat(bv);if(!isNaN(an)&&!isNaN(bn))return dir==='asc'?an-bn:bn-an;return dir==='asc'?av.localeCompare(bv):bv.localeCompare(av);});rows.forEach(function(r){tbody.appendChild(r);});renderPagination(tableId);};\n" +
442
+ // Pagination
443
+ "function renderPagination(tableId){var tbl=document.getElementById(tableId);var pag=document.getElementById(tableId+'-pagination');var meta=document.getElementById(tableId+'-meta');if(!tbl||!pag)return;var pageSize=parseInt(tbl.getAttribute('data-page-size')||'50',10);var rows=Array.from(tbl.querySelectorAll('tbody tr')).filter(function(r){return r.style.display!=='none';});var total=rows.length;var pages=Math.ceil(total/pageSize);var page=parseInt(tbl.getAttribute('data-page')||'1',10);if(page>pages)page=1;tbl.setAttribute('data-page',page);rows.forEach(function(r,i){r.style.display=(i>=(page-1)*pageSize&&i<page*pageSize)?'':'none';});if(meta)meta.textContent=total+' row'+(total!==1?'s':'');pag.innerHTML='';if(pages<=1)return;function btn(label,p,active,disabled){var b=document.createElement('button');b.textContent=label;b.className='pag-btn'+(active?' pag-active':'')+(disabled?' pag-disabled':'');b.disabled=disabled;b.onclick=function(){tbl.setAttribute('data-page',p);renderPagination(tableId);};return b;}pag.appendChild(btn('\\u00AB',1,false,page===1));pag.appendChild(btn('\\u2039',page-1,false,page===1));var start=Math.max(1,page-2),end=Math.min(pages,page+2);for(var i=start;i<=end;i++)pag.appendChild(btn(i,i,i===page,false));pag.appendChild(btn('\\u203A',page+1,false,page===pages));pag.appendChild(btn('\\u00BB',pages,false,page===pages));var info=document.createElement('span');info.className='pag-info';info.textContent='Page '+page+' of '+pages;pag.appendChild(info);}\n" +
444
+ "document.querySelectorAll('table[id]').forEach(function(tbl){renderPagination(tbl.id);var meta=document.getElementById(tbl.id+'-meta');if(meta){var total=tbl.querySelectorAll('tbody tr').length;meta.textContent=total+' row'+(total!==1?'s':'');}});\n" +
445
+ // Export CSV
446
+ "window.exportTableCsv=function(tableId){var tbl=document.getElementById(tableId);if(!tbl)return;var rows=[Array.from(tbl.querySelectorAll('thead th')).map(function(th){return'\"'+th.textContent.replace(/\"/g,'\"\"').trim()+'\"';}).join(',')];tbl.querySelectorAll('tbody tr').forEach(function(tr){rows.push(Array.from(tr.cells).map(function(td){return'\"'+td.textContent.replace(/\"/g,'\"\"').trim()+'\"';}).join(','));});downloadFile(tableId+'.csv',rows.join('\\n'),'text/csv');};\n" +
447
+ // Export JSON
448
+ "window.exportTableJson=function(tableId){var tbl=document.getElementById(tableId);if(!tbl)return;var headers=Array.from(tbl.querySelectorAll('thead th')).map(function(th){return th.textContent.trim().replace(/[\\u2191\\u2193\\u21C5]/g,'').trim();});var data=Array.from(tbl.querySelectorAll('tbody tr')).map(function(tr){var obj={};Array.from(tr.cells).forEach(function(td,i){obj[headers[i]||i]=td.textContent.trim();});return obj;});downloadFile(tableId+'.json',JSON.stringify(data,null,2),'application/json');};\n" +
449
+ // Export full
450
+ window.exportFullJson=function(){var d=\"{\\\"timestamp\\\":\\\"2026-05-27T13:52:11.604Z\\\",\\\"framework\\\":\\\"react-vite\\\",\\\"filesScanned\\\":148,\\\"hardcodedTexts\\\":37,\\\"localeKeysGenerated\\\":29,\\\"missingTranslations\\\":12,\\\"unusedKeys\\\":4,\\\"coveragePct\\\":68,\\\"assets\\\":{\\\"totalAssets\\\":23,\\\"uploadedAssets\\\":18,\\\"replacedUrls\\\":15,\\\"legacyCdnUrls\\\":5},\\\"details\\\":{\\\"detectedTexts\\\":[{\\\"filePath\\\":\\\"/app/src/components/Button/PrimaryButton.tsx\\\",\\\"line\\\":14,\\\"column\\\":8,\\\"text\\\":\\\"Submit\\\",\\\"suggestedKey\\\":\\\"common.submit\\\",\\\"context\\\":\\\"button\\\",\\\"nodeType\\\":\\\"JSXAttribute\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/components/Button/PrimaryButton.tsx\\\",\\\"line\\\":22,\\\"column\\\":12,\\\"text\\\":\\\"Cancel\\\",\\\"suggestedKey\\\":\\\"common.cancel\\\",\\\"context\\\":\\\"button\\\",\\\"nodeType\\\":\\\"JSXAttribute\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/pages/Dashboard/Overview.tsx\\\",\\\"line\\\":31,\\\"column\\\":6,\\\"text\\\":\\\"Welcome back!\\\",\\\"suggestedKey\\\":\\\"dashboard.welcomeBack\\\",\\\"context\\\":\\\"heading\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/pages/Dashboard/Overview.tsx\\\",\\\"line\\\":45,\\\"column\\\":10,\\\"text\\\":\\\"Total Revenue\\\",\\\"suggestedKey\\\":\\\"dashboard.totalRevenue\\\",\\\"context\\\":\\\"table-header\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/pages/Dashboard/Overview.tsx\\\",\\\"line\\\":67,\\\"column\\\":14,\\\"text\\\":\\\"No data available\\\",\\\"suggestedKey\\\":\\\"dashboard.noData\\\",\\\"context\\\":\\\"jsx-text\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/components/Modal/ConfirmModal.tsx\\\",\\\"line\\\":8,\\\"column\\\":4,\\\"text\\\":\\\"Are you sure?\\\",\\\"suggestedKey\\\":\\\"modal.confirmTitle\\\",\\\"context\\\":\\\"modal\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/components/Modal/ConfirmModal.tsx\\\",\\\"line\\\":19,\\\"column\\\":8,\\\"text\\\":\\\"This action cannot be undone.\\\",\\\"suggestedKey\\\":\\\"modal.confirmBody\\\",\\\"context\\\":\\\"modal\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/components/Modal/ConfirmModal.tsx\\\",\\\"line\\\":32,\\\"column\\\":12,\\\"text\\\":\\\"Delete\\\",\\\"suggestedKey\\\":\\\"common.delete\\\",\\\"context\\\":\\\"button\\\",\\\"nodeType\\\":\\\"JSXAttribute\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/forms/LoginForm.tsx\\\",\\\"line\\\":12,\\\"column\\\":6,\\\"text\\\":\\\"Email address\\\",\\\"suggestedKey\\\":\\\"auth.emailLabel\\\",\\\"context\\\":\\\"label\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/forms/LoginForm.tsx\\\",\\\"line\\\":18,\\\"column\\\":6,\\\"text\\\":\\\"Password\\\",\\\"suggestedKey\\\":\\\"auth.passwordLabel\\\",\\\"context\\\":\\\"label\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/forms/LoginForm.tsx\\\",\\\"line\\\":24,\\\"column\\\":10,\\\"text\\\":\\\"Enter your email\\\",\\\"suggestedKey\\\":\\\"auth.emailPlaceholder\\\",\\\"context\\\":\\\"placeholder\\\",\\\"nodeType\\\":\\\"JSXAttribute\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/forms/LoginForm.tsx\\\",\\\"line\\\":30,\\\"column\\\":10,\\\"text\\\":\\\"Enter your password\\\",\\\"suggestedKey\\\":\\\"auth.passwordPlaceholder\\\",\\\"context\\\":\\\"placeholder\\\",\\\"nodeType\\\":\\\"JSXAttribute\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/forms/LoginForm.tsx\\\",\\\"line\\\":56,\\\"column\\\":8,\\\"text\\\":\\\"Sign in\\\",\\\"suggestedKey\\\":\\\"auth.signIn\\\",\\\"context\\\":\\\"button\\\",\\\"nodeType\\\":\\\"JSXAttribute\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/forms/LoginForm.tsx\\\",\\\"line\\\":60,\\\"column\\\":12,\\\"text\\\":\\\"Forgot password?\\\",\\\"suggestedKey\\\":\\\"auth.forgotPassword\\\",\\\"context\\\":\\\"jsx-text\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/components/Nav/Sidebar.tsx\\\",\\\"line\\\":22,\\\"column\\\":6,\\\"text\\\":\\\"Dashboard\\\",\\\"suggestedKey\\\":\\\"nav.dashboard\\\",\\\"context\\\":\\\"jsx-text\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/components/Nav/Sidebar.tsx\\\",\\\"line\\\":30,\\\"column\\\":6,\\\"text\\\":\\\"Settings\\\",\\\"suggestedKey\\\":\\\"nav.settings\\\",\\\"context\\\":\\\"jsx-text\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/components/Nav/Sidebar.tsx\\\",\\\"line\\\":38,\\\"column\\\":6,\\\"text\\\":\\\"Users\\\",\\\"suggestedKey\\\":\\\"nav.users\\\",\\\"context\\\":\\\"jsx-text\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/components/Nav/Sidebar.tsx\\\",\\\"line\\\":46,\\\"column\\\":6,\\\"text\\\":\\\"Reports\\\",\\\"suggestedKey\\\":\\\"nav.reports\\\",\\\"context\\\":\\\"jsx-text\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/components/Nav/Sidebar.tsx\\\",\\\"line\\\":54,\\\"column\\\":6,\\\"text\\\":\\\"Log out\\\",\\\"suggestedKey\\\":\\\"nav.logout\\\",\\\"context\\\":\\\"jsx-text\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/pages/Settings/ProfileSettings.tsx\\\",\\\"line\\\":11,\\\"column\\\":4,\\\"text\\\":\\\"Profile Settings\\\",\\\"suggestedKey\\\":\\\"settings.profileTitle\\\",\\\"context\\\":\\\"heading\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/pages/Settings/ProfileSettings.tsx\\\",\\\"line\\\":28,\\\"column\\\":8,\\\"text\\\":\\\"First name\\\",\\\"suggestedKey\\\":\\\"settings.firstName\\\",\\\"context\\\":\\\"label\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/pages/Settings/ProfileSettings.tsx\\\",\\\"line\\\":36,\\\"column\\\":8,\\\"text\\\":\\\"Last name\\\",\\\"suggestedKey\\\":\\\"settings.lastName\\\",\\\"context\\\":\\\"label\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/pages/Settings/ProfileSettings.tsx\\\",\\\"line\\\":44,\\\"column\\\":8,\\\"text\\\":\\\"Save changes\\\",\\\"suggestedKey\\\":\\\"common.saveChanges\\\",\\\"context\\\":\\\"button\\\",\\\"nodeType\\\":\\\"JSXAttribute\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/components/Toast/ToastManager.tsx\\\",\\\"line\\\":9,\\\"column\\\":6,\\\"text\\\":\\\"Changes saved successfully\\\",\\\"suggestedKey\\\":\\\"toast.saveSuccess\\\",\\\"context\\\":\\\"toast\\\",\\\"nodeType\\\":\\\"string-literal\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/components/Toast/ToastManager.tsx\\\",\\\"line\\\":15,\\\"column\\\":6,\\\"text\\\":\\\"An error occurred. Please try again.\\\",\\\"suggestedKey\\\":\\\"toast.genericError\\\",\\\"context\\\":\\\"toast\\\",\\\"nodeType\\\":\\\"string-literal\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/components/Toast/ToastManager.tsx\\\",\\\"line\\\":21,\\\"column\\\":6,\\\"text\\\":\\\"Network connection lost\\\",\\\"suggestedKey\\\":\\\"toast.networkError\\\",\\\"context\\\":\\\"toast\\\",\\\"nodeType\\\":\\\"string-literal\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/pages/Users/UserTable.tsx\\\",\\\"line\\\":17,\\\"column\\\":6,\\\"text\\\":\\\"Name\\\",\\\"suggestedKey\\\":\\\"users.columnName\\\",\\\"context\\\":\\\"table-header\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/pages/Users/UserTable.tsx\\\",\\\"line\\\":24,\\\"column\\\":6,\\\"text\\\":\\\"Email\\\",\\\"suggestedKey\\\":\\\"users.columnEmail\\\",\\\"context\\\":\\\"table-header\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/pages/Users/UserTable.tsx\\\",\\\"line\\\":31,\\\"column\\\":6,\\\"text\\\":\\\"Role\\\",\\\"suggestedKey\\\":\\\"users.columnRole\\\",\\\"context\\\":\\\"table-header\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/pages/Users/UserTable.tsx\\\",\\\"line\\\":38,\\\"column\\\":6,\\\"text\\\":\\\"Status\\\",\\\"suggestedKey\\\":\\\"users.columnStatus\\\",\\\"context\\\":\\\"table-header\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/pages/Users/UserTable.tsx\\\",\\\"line\\\":68,\\\"column\\\":10,\\\"text\\\":\\\"No users found\\\",\\\"suggestedKey\\\":\\\"users.emptyState\\\",\\\"context\\\":\\\"jsx-text\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/components/Alert/ErrorBanner.tsx\\\",\\\"line\\\":7,\\\"column\\\":4,\\\"text\\\":\\\"Something went wrong\\\",\\\"suggestedKey\\\":\\\"alert.errorTitle\\\",\\\"context\\\":\\\"alert\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/components/Tooltip/HelpTooltip.tsx\\\",\\\"line\\\":5,\\\"column\\\":6,\\\"text\\\":\\\"Click for more information\\\",\\\"suggestedKey\\\":\\\"tooltip.helpInfo\\\",\\\"context\\\":\\\"tooltip\\\",\\\"nodeType\\\":\\\"JSXAttribute\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/components/DataGrid/DataGrid.tsx\\\",\\\"line\\\":42,\\\"column\\\":8,\\\"text\\\":\\\"Loading data...\\\",\\\"suggestedKey\\\":\\\"grid.loading\\\",\\\"context\\\":\\\"jsx-text\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/components/DataGrid/DataGrid.tsx\\\",\\\"line\\\":58,\\\"column\\\":8,\\\"text\\\":\\\"No results match your search\\\",\\\"suggestedKey\\\":\\\"grid.noResults\\\",\\\"context\\\":\\\"jsx-text\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/pages/Reports/ReportHeader.tsx\\\",\\\"line\\\":10,\\\"column\\\":6,\\\"text\\\":\\\"Submit\\\",\\\"suggestedKey\\\":\\\"reports.submit\\\",\\\"context\\\":\\\"button\\\",\\\"nodeType\\\":\\\"JSXAttribute\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/pages/Reports/ReportHeader.tsx\\\",\\\"line\\\":22,\\\"column\\\":8,\\\"text\\\":\\\"Cancel\\\",\\\"suggestedKey\\\":\\\"reports.cancel\\\",\\\"context\\\":\\\"button\\\",\\\"nodeType\\\":\\\"JSXAttribute\\\",\\\"alreadyTranslated\\\":false}],\\\"missingKeys\\\":[{\\\"key\\\":\\\"common.submit\\\",\\\"language\\\":\\\"fr\\\",\\\"message\\\":\\\"Key missing in fr locale\\\",\\\"filePath\\\":\\\"/app/locales/fr/common.json\\\"},{\\\"key\\\":\\\"common.cancel\\\",\\\"language\\\":\\\"fr\\\",\\\"message\\\":\\\"Key missing in fr locale\\\",\\\"filePath\\\":\\\"/app/locales/fr/common.json\\\"},{\\\"key\\\":\\\"auth.signIn\\\",\\\"language\\\":\\\"fr\\\",\\\"message\\\":\\\"Key missing in fr locale\\\",\\\"filePath\\\":\\\"/app/locales/fr/auth.json\\\"},{\\\"key\\\":\\\"dashboard.welcomeBack\\\",\\\"language\\\":\\\"de\\\",\\\"message\\\":\\\"Key missing in de locale\\\",\\\"filePath\\\":\\\"/app/locales/de/dashboard.json\\\"},{\\\"key\\\":\\\"dashboard.totalRevenue\\\",\\\"language\\\":\\\"de\\\",\\\"message\\\":\\\"Key missing in de locale\\\",\\\"filePath\\\":\\\"/app/locales/de/dashboard.json\\\"},{\\\"key\\\":\\\"modal.confirmTitle\\\",\\\"language\\\":\\\"de\\\",\\\"message\\\":\\\"Key missing in de locale\\\",\\\"filePath\\\":\\\"/app/locales/de/modal.json\\\"},{\\\"key\\\":\\\"modal.confirmBody\\\",\\\"language\\\":\\\"de\\\",\\\"message\\\":\\\"Key missing in de locale\\\",\\\"filePath\\\":\\\"/app/locales/de/modal.json\\\"},{\\\"key\\\":\\\"nav.logout\\\",\\\"language\\\":\\\"es\\\",\\\"message\\\":\\\"Key missing in es locale\\\",\\\"filePath\\\":\\\"/app/locales/es/nav.json\\\"},{\\\"key\\\":\\\"settings.profileTitle\\\",\\\"language\\\":\\\"es\\\",\\\"message\\\":\\\"Key missing in es locale\\\",\\\"filePath\\\":\\\"/app/locales/es/settings.json\\\"},{\\\"key\\\":\\\"toast.genericError\\\",\\\"language\\\":\\\"ja\\\",\\\"message\\\":\\\"Key missing in ja locale\\\",\\\"filePath\\\":\\\"/app/locales/ja/toast.json\\\"},{\\\"key\\\":\\\"toast.saveSuccess\\\",\\\"language\\\":\\\"ja\\\",\\\"message\\\":\\\"Key missing in ja locale\\\",\\\"filePath\\\":\\\"/app/locales/ja/toast.json\\\"},{\\\"key\\\":\\\"users.emptyState\\\",\\\"language\\\":\\\"ja\\\",\\\"message\\\":\\\"Key missing in ja locale\\\",\\\"filePath\\\":\\\"/app/locales/ja/users.json\\\"}],\\\"unusedKeys\\\":[\\\"common.oldButton\\\",\\\"dashboard.legacyWidget\\\",\\\"auth.ssoLogin\\\",\\\"settings.betaFeature\\\"],\\\"assets\\\":[{\\\"localPath\\\":\\\"/app/public/images/logo.png\\\",\\\"s3Key\\\":\\\"assets/images/logo.png\\\",\\\"cloudfrontUrl\\\":\\\"https://d1abc123.cloudfront.net/assets/images/logo.png\\\",\\\"contentType\\\":\\\"image/png\\\",\\\"sizeKb\\\":\\\"24.0\\\"},{\\\"localPath\\\":\\\"/app/public/images/hero-banner.webp\\\",\\\"s3Key\\\":\\\"assets/images/hero-banner.webp\\\",\\\"cloudfrontUrl\\\":\\\"https://d1abc123.cloudfront.net/assets/images/hero-banner.webp\\\",\\\"contentType\\\":\\\"image/webp\\\",\\\"sizeKb\\\":\\\"100.0\\\"},{\\\"localPath\\\":\\\"/app/public/fonts/Inter-Regular.woff2\\\",\\\"s3Key\\\":\\\"assets/fonts/Inter-Regular.woff2\\\",\\\"cloudfrontUrl\\\":\\\"https://d1abc123.cloudfront.net/assets/fonts/Inter-Regular.woff2\\\",\\\"contentType\\\":\\\"font/woff2\\\",\\\"sizeKb\\\":\\\"64.0\\\"},{\\\"localPath\\\":\\\"/app/public/fonts/Inter-Bold.woff2\\\",\\\"s3Key\\\":\\\"assets/fonts/Inter-Bold.woff2\\\",\\\"cloudfrontUrl\\\":\\\"https://d1abc123.cloudfront.net/assets/fonts/Inter-Bold.woff2\\\",\\\"contentType\\\":\\\"font/woff2\\\",\\\"sizeKb\\\":\\\"67.5\\\"},{\\\"localPath\\\":\\\"/app/public/icons/favicon.svg\\\",\\\"s3Key\\\":\\\"assets/icons/favicon.svg\\\",\\\"cloudfrontUrl\\\":\\\"https://d1abc123.cloudfront.net/assets/icons/favicon.svg\\\",\\\"contentType\\\":\\\"image/svg+xml\\\",\\\"sizeKb\\\":\\\"4.0\\\"},{\\\"localPath\\\":\\\"/app/public/videos/onboarding.mp4\\\",\\\"s3Key\\\":\\\"assets/videos/onboarding.mp4\\\",\\\"cloudfrontUrl\\\":\\\"https://d1abc123.cloudfront.net/assets/videos/onboarding.mp4\\\",\\\"contentType\\\":\\\"video/mp4\\\",\\\"sizeKb\\\":\\\"5120.0\\\"}]}}\";downloadFile('ai-localize-report.json',JSON.stringify(JSON.parse(d),null,2),'application/json');};\n" +
451
+ window.exportFullCsv=function(){var d=\"{\\\"timestamp\\\":\\\"2026-05-27T13:52:11.604Z\\\",\\\"framework\\\":\\\"react-vite\\\",\\\"filesScanned\\\":148,\\\"hardcodedTexts\\\":37,\\\"localeKeysGenerated\\\":29,\\\"missingTranslations\\\":12,\\\"unusedKeys\\\":4,\\\"coveragePct\\\":68,\\\"assets\\\":{\\\"totalAssets\\\":23,\\\"uploadedAssets\\\":18,\\\"replacedUrls\\\":15,\\\"legacyCdnUrls\\\":5},\\\"details\\\":{\\\"detectedTexts\\\":[{\\\"filePath\\\":\\\"/app/src/components/Button/PrimaryButton.tsx\\\",\\\"line\\\":14,\\\"column\\\":8,\\\"text\\\":\\\"Submit\\\",\\\"suggestedKey\\\":\\\"common.submit\\\",\\\"context\\\":\\\"button\\\",\\\"nodeType\\\":\\\"JSXAttribute\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/components/Button/PrimaryButton.tsx\\\",\\\"line\\\":22,\\\"column\\\":12,\\\"text\\\":\\\"Cancel\\\",\\\"suggestedKey\\\":\\\"common.cancel\\\",\\\"context\\\":\\\"button\\\",\\\"nodeType\\\":\\\"JSXAttribute\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/pages/Dashboard/Overview.tsx\\\",\\\"line\\\":31,\\\"column\\\":6,\\\"text\\\":\\\"Welcome back!\\\",\\\"suggestedKey\\\":\\\"dashboard.welcomeBack\\\",\\\"context\\\":\\\"heading\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/pages/Dashboard/Overview.tsx\\\",\\\"line\\\":45,\\\"column\\\":10,\\\"text\\\":\\\"Total Revenue\\\",\\\"suggestedKey\\\":\\\"dashboard.totalRevenue\\\",\\\"context\\\":\\\"table-header\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/pages/Dashboard/Overview.tsx\\\",\\\"line\\\":67,\\\"column\\\":14,\\\"text\\\":\\\"No data available\\\",\\\"suggestedKey\\\":\\\"dashboard.noData\\\",\\\"context\\\":\\\"jsx-text\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/components/Modal/ConfirmModal.tsx\\\",\\\"line\\\":8,\\\"column\\\":4,\\\"text\\\":\\\"Are you sure?\\\",\\\"suggestedKey\\\":\\\"modal.confirmTitle\\\",\\\"context\\\":\\\"modal\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/components/Modal/ConfirmModal.tsx\\\",\\\"line\\\":19,\\\"column\\\":8,\\\"text\\\":\\\"This action cannot be undone.\\\",\\\"suggestedKey\\\":\\\"modal.confirmBody\\\",\\\"context\\\":\\\"modal\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/components/Modal/ConfirmModal.tsx\\\",\\\"line\\\":32,\\\"column\\\":12,\\\"text\\\":\\\"Delete\\\",\\\"suggestedKey\\\":\\\"common.delete\\\",\\\"context\\\":\\\"button\\\",\\\"nodeType\\\":\\\"JSXAttribute\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/forms/LoginForm.tsx\\\",\\\"line\\\":12,\\\"column\\\":6,\\\"text\\\":\\\"Email address\\\",\\\"suggestedKey\\\":\\\"auth.emailLabel\\\",\\\"context\\\":\\\"label\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/forms/LoginForm.tsx\\\",\\\"line\\\":18,\\\"column\\\":6,\\\"text\\\":\\\"Password\\\",\\\"suggestedKey\\\":\\\"auth.passwordLabel\\\",\\\"context\\\":\\\"label\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/forms/LoginForm.tsx\\\",\\\"line\\\":24,\\\"column\\\":10,\\\"text\\\":\\\"Enter your email\\\",\\\"suggestedKey\\\":\\\"auth.emailPlaceholder\\\",\\\"context\\\":\\\"placeholder\\\",\\\"nodeType\\\":\\\"JSXAttribute\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/forms/LoginForm.tsx\\\",\\\"line\\\":30,\\\"column\\\":10,\\\"text\\\":\\\"Enter your password\\\",\\\"suggestedKey\\\":\\\"auth.passwordPlaceholder\\\",\\\"context\\\":\\\"placeholder\\\",\\\"nodeType\\\":\\\"JSXAttribute\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/forms/LoginForm.tsx\\\",\\\"line\\\":56,\\\"column\\\":8,\\\"text\\\":\\\"Sign in\\\",\\\"suggestedKey\\\":\\\"auth.signIn\\\",\\\"context\\\":\\\"button\\\",\\\"nodeType\\\":\\\"JSXAttribute\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/forms/LoginForm.tsx\\\",\\\"line\\\":60,\\\"column\\\":12,\\\"text\\\":\\\"Forgot password?\\\",\\\"suggestedKey\\\":\\\"auth.forgotPassword\\\",\\\"context\\\":\\\"jsx-text\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/components/Nav/Sidebar.tsx\\\",\\\"line\\\":22,\\\"column\\\":6,\\\"text\\\":\\\"Dashboard\\\",\\\"suggestedKey\\\":\\\"nav.dashboard\\\",\\\"context\\\":\\\"jsx-text\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/components/Nav/Sidebar.tsx\\\",\\\"line\\\":30,\\\"column\\\":6,\\\"text\\\":\\\"Settings\\\",\\\"suggestedKey\\\":\\\"nav.settings\\\",\\\"context\\\":\\\"jsx-text\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/components/Nav/Sidebar.tsx\\\",\\\"line\\\":38,\\\"column\\\":6,\\\"text\\\":\\\"Users\\\",\\\"suggestedKey\\\":\\\"nav.users\\\",\\\"context\\\":\\\"jsx-text\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/components/Nav/Sidebar.tsx\\\",\\\"line\\\":46,\\\"column\\\":6,\\\"text\\\":\\\"Reports\\\",\\\"suggestedKey\\\":\\\"nav.reports\\\",\\\"context\\\":\\\"jsx-text\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/components/Nav/Sidebar.tsx\\\",\\\"line\\\":54,\\\"column\\\":6,\\\"text\\\":\\\"Log out\\\",\\\"suggestedKey\\\":\\\"nav.logout\\\",\\\"context\\\":\\\"jsx-text\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/pages/Settings/ProfileSettings.tsx\\\",\\\"line\\\":11,\\\"column\\\":4,\\\"text\\\":\\\"Profile Settings\\\",\\\"suggestedKey\\\":\\\"settings.profileTitle\\\",\\\"context\\\":\\\"heading\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/pages/Settings/ProfileSettings.tsx\\\",\\\"line\\\":28,\\\"column\\\":8,\\\"text\\\":\\\"First name\\\",\\\"suggestedKey\\\":\\\"settings.firstName\\\",\\\"context\\\":\\\"label\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/pages/Settings/ProfileSettings.tsx\\\",\\\"line\\\":36,\\\"column\\\":8,\\\"text\\\":\\\"Last name\\\",\\\"suggestedKey\\\":\\\"settings.lastName\\\",\\\"context\\\":\\\"label\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/pages/Settings/ProfileSettings.tsx\\\",\\\"line\\\":44,\\\"column\\\":8,\\\"text\\\":\\\"Save changes\\\",\\\"suggestedKey\\\":\\\"common.saveChanges\\\",\\\"context\\\":\\\"button\\\",\\\"nodeType\\\":\\\"JSXAttribute\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/components/Toast/ToastManager.tsx\\\",\\\"line\\\":9,\\\"column\\\":6,\\\"text\\\":\\\"Changes saved successfully\\\",\\\"suggestedKey\\\":\\\"toast.saveSuccess\\\",\\\"context\\\":\\\"toast\\\",\\\"nodeType\\\":\\\"string-literal\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/components/Toast/ToastManager.tsx\\\",\\\"line\\\":15,\\\"column\\\":6,\\\"text\\\":\\\"An error occurred. Please try again.\\\",\\\"suggestedKey\\\":\\\"toast.genericError\\\",\\\"context\\\":\\\"toast\\\",\\\"nodeType\\\":\\\"string-literal\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/components/Toast/ToastManager.tsx\\\",\\\"line\\\":21,\\\"column\\\":6,\\\"text\\\":\\\"Network connection lost\\\",\\\"suggestedKey\\\":\\\"toast.networkError\\\",\\\"context\\\":\\\"toast\\\",\\\"nodeType\\\":\\\"string-literal\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/pages/Users/UserTable.tsx\\\",\\\"line\\\":17,\\\"column\\\":6,\\\"text\\\":\\\"Name\\\",\\\"suggestedKey\\\":\\\"users.columnName\\\",\\\"context\\\":\\\"table-header\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/pages/Users/UserTable.tsx\\\",\\\"line\\\":24,\\\"column\\\":6,\\\"text\\\":\\\"Email\\\",\\\"suggestedKey\\\":\\\"users.columnEmail\\\",\\\"context\\\":\\\"table-header\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/pages/Users/UserTable.tsx\\\",\\\"line\\\":31,\\\"column\\\":6,\\\"text\\\":\\\"Role\\\",\\\"suggestedKey\\\":\\\"users.columnRole\\\",\\\"context\\\":\\\"table-header\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/pages/Users/UserTable.tsx\\\",\\\"line\\\":38,\\\"column\\\":6,\\\"text\\\":\\\"Status\\\",\\\"suggestedKey\\\":\\\"users.columnStatus\\\",\\\"context\\\":\\\"table-header\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/pages/Users/UserTable.tsx\\\",\\\"line\\\":68,\\\"column\\\":10,\\\"text\\\":\\\"No users found\\\",\\\"suggestedKey\\\":\\\"users.emptyState\\\",\\\"context\\\":\\\"jsx-text\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/components/Alert/ErrorBanner.tsx\\\",\\\"line\\\":7,\\\"column\\\":4,\\\"text\\\":\\\"Something went wrong\\\",\\\"suggestedKey\\\":\\\"alert.errorTitle\\\",\\\"context\\\":\\\"alert\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/components/Tooltip/HelpTooltip.tsx\\\",\\\"line\\\":5,\\\"column\\\":6,\\\"text\\\":\\\"Click for more information\\\",\\\"suggestedKey\\\":\\\"tooltip.helpInfo\\\",\\\"context\\\":\\\"tooltip\\\",\\\"nodeType\\\":\\\"JSXAttribute\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/components/DataGrid/DataGrid.tsx\\\",\\\"line\\\":42,\\\"column\\\":8,\\\"text\\\":\\\"Loading data...\\\",\\\"suggestedKey\\\":\\\"grid.loading\\\",\\\"context\\\":\\\"jsx-text\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/components/DataGrid/DataGrid.tsx\\\",\\\"line\\\":58,\\\"column\\\":8,\\\"text\\\":\\\"No results match your search\\\",\\\"suggestedKey\\\":\\\"grid.noResults\\\",\\\"context\\\":\\\"jsx-text\\\",\\\"nodeType\\\":\\\"JSXText\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/pages/Reports/ReportHeader.tsx\\\",\\\"line\\\":10,\\\"column\\\":6,\\\"text\\\":\\\"Submit\\\",\\\"suggestedKey\\\":\\\"reports.submit\\\",\\\"context\\\":\\\"button\\\",\\\"nodeType\\\":\\\"JSXAttribute\\\",\\\"alreadyTranslated\\\":false},{\\\"filePath\\\":\\\"/app/src/pages/Reports/ReportHeader.tsx\\\",\\\"line\\\":22,\\\"column\\\":8,\\\"text\\\":\\\"Cancel\\\",\\\"suggestedKey\\\":\\\"reports.cancel\\\",\\\"context\\\":\\\"button\\\",\\\"nodeType\\\":\\\"JSXAttribute\\\",\\\"alreadyTranslated\\\":false}],\\\"missingKeys\\\":[{\\\"key\\\":\\\"common.submit\\\",\\\"language\\\":\\\"fr\\\",\\\"message\\\":\\\"Key missing in fr locale\\\",\\\"filePath\\\":\\\"/app/locales/fr/common.json\\\"},{\\\"key\\\":\\\"common.cancel\\\",\\\"language\\\":\\\"fr\\\",\\\"message\\\":\\\"Key missing in fr locale\\\",\\\"filePath\\\":\\\"/app/locales/fr/common.json\\\"},{\\\"key\\\":\\\"auth.signIn\\\",\\\"language\\\":\\\"fr\\\",\\\"message\\\":\\\"Key missing in fr locale\\\",\\\"filePath\\\":\\\"/app/locales/fr/auth.json\\\"},{\\\"key\\\":\\\"dashboard.welcomeBack\\\",\\\"language\\\":\\\"de\\\",\\\"message\\\":\\\"Key missing in de locale\\\",\\\"filePath\\\":\\\"/app/locales/de/dashboard.json\\\"},{\\\"key\\\":\\\"dashboard.totalRevenue\\\",\\\"language\\\":\\\"de\\\",\\\"message\\\":\\\"Key missing in de locale\\\",\\\"filePath\\\":\\\"/app/locales/de/dashboard.json\\\"},{\\\"key\\\":\\\"modal.confirmTitle\\\",\\\"language\\\":\\\"de\\\",\\\"message\\\":\\\"Key missing in de locale\\\",\\\"filePath\\\":\\\"/app/locales/de/modal.json\\\"},{\\\"key\\\":\\\"modal.confirmBody\\\",\\\"language\\\":\\\"de\\\",\\\"message\\\":\\\"Key missing in de locale\\\",\\\"filePath\\\":\\\"/app/locales/de/modal.json\\\"},{\\\"key\\\":\\\"nav.logout\\\",\\\"language\\\":\\\"es\\\",\\\"message\\\":\\\"Key missing in es locale\\\",\\\"filePath\\\":\\\"/app/locales/es/nav.json\\\"},{\\\"key\\\":\\\"settings.profileTitle\\\",\\\"language\\\":\\\"es\\\",\\\"message\\\":\\\"Key missing in es locale\\\",\\\"filePath\\\":\\\"/app/locales/es/settings.json\\\"},{\\\"key\\\":\\\"toast.genericError\\\",\\\"language\\\":\\\"ja\\\",\\\"message\\\":\\\"Key missing in ja locale\\\",\\\"filePath\\\":\\\"/app/locales/ja/toast.json\\\"},{\\\"key\\\":\\\"toast.saveSuccess\\\",\\\"language\\\":\\\"ja\\\",\\\"message\\\":\\\"Key missing in ja locale\\\",\\\"filePath\\\":\\\"/app/locales/ja/toast.json\\\"},{\\\"key\\\":\\\"users.emptyState\\\",\\\"language\\\":\\\"ja\\\",\\\"message\\\":\\\"Key missing in ja locale\\\",\\\"filePath\\\":\\\"/app/locales/ja/users.json\\\"}],\\\"unusedKeys\\\":[\\\"common.oldButton\\\",\\\"dashboard.legacyWidget\\\",\\\"auth.ssoLogin\\\",\\\"settings.betaFeature\\\"],\\\"assets\\\":[{\\\"localPath\\\":\\\"/app/public/images/logo.png\\\",\\\"s3Key\\\":\\\"assets/images/logo.png\\\",\\\"cloudfrontUrl\\\":\\\"https://d1abc123.cloudfront.net/assets/images/logo.png\\\",\\\"contentType\\\":\\\"image/png\\\",\\\"sizeKb\\\":\\\"24.0\\\"},{\\\"localPath\\\":\\\"/app/public/images/hero-banner.webp\\\",\\\"s3Key\\\":\\\"assets/images/hero-banner.webp\\\",\\\"cloudfrontUrl\\\":\\\"https://d1abc123.cloudfront.net/assets/images/hero-banner.webp\\\",\\\"contentType\\\":\\\"image/webp\\\",\\\"sizeKb\\\":\\\"100.0\\\"},{\\\"localPath\\\":\\\"/app/public/fonts/Inter-Regular.woff2\\\",\\\"s3Key\\\":\\\"assets/fonts/Inter-Regular.woff2\\\",\\\"cloudfrontUrl\\\":\\\"https://d1abc123.cloudfront.net/assets/fonts/Inter-Regular.woff2\\\",\\\"contentType\\\":\\\"font/woff2\\\",\\\"sizeKb\\\":\\\"64.0\\\"},{\\\"localPath\\\":\\\"/app/public/fonts/Inter-Bold.woff2\\\",\\\"s3Key\\\":\\\"assets/fonts/Inter-Bold.woff2\\\",\\\"cloudfrontUrl\\\":\\\"https://d1abc123.cloudfront.net/assets/fonts/Inter-Bold.woff2\\\",\\\"contentType\\\":\\\"font/woff2\\\",\\\"sizeKb\\\":\\\"67.5\\\"},{\\\"localPath\\\":\\\"/app/public/icons/favicon.svg\\\",\\\"s3Key\\\":\\\"assets/icons/favicon.svg\\\",\\\"cloudfrontUrl\\\":\\\"https://d1abc123.cloudfront.net/assets/icons/favicon.svg\\\",\\\"contentType\\\":\\\"image/svg+xml\\\",\\\"sizeKb\\\":\\\"4.0\\\"},{\\\"localPath\\\":\\\"/app/public/videos/onboarding.mp4\\\",\\\"s3Key\\\":\\\"assets/videos/onboarding.mp4\\\",\\\"cloudfrontUrl\\\":\\\"https://d1abc123.cloudfront.net/assets/videos/onboarding.mp4\\\",\\\"contentType\\\":\\\"video/mp4\\\",\\\"sizeKb\\\":\\\"5120.0\\\"}]}}\";var r=JSON.parse(d);var q=function(s){return '\"'+String(s==null?'':s).replace(/\"/g,'\"\"')+'\"';};var rows=[];rows.push('=== SUMMARY ===');rows.push('\"Metric\",\"Value\"');rows.push(q('Framework')+','+q(r.framework));rows.push(q('Generated')+','+q(r.timestamp));rows.push(q('Files Scanned')+','+r.filesScanned);rows.push(q('Hardcoded Texts')+','+r.hardcodedTexts);rows.push(q('Keys Generated')+','+r.localeKeysGenerated);rows.push(q('Missing Translations')+','+r.missingTranslations);rows.push(q('Unused Keys')+','+r.unusedKeys);rows.push(q('Coverage %')+','+r.coveragePct);rows.push(q('Total Assets')+','+r.assets.totalAssets);rows.push(q('Uploaded Assets')+','+r.assets.uploadedAssets);rows.push(q('Replaced URLs')+','+r.assets.replacedUrls);rows.push(q('Legacy CDN URLs')+','+r.assets.legacyCdnUrls);rows.push('');rows.push('=== HARDCODED TEXTS ===');rows.push('\"File\",\"Line\",\"Column\",\"Text\",\"Suggested Key\",\"Context\",\"Node Type\",\"Already Translated\"');(r.details.detectedTexts||[]).forEach(function(t){rows.push([q(t.filePath),t.line,t.column,q(t.text),q(t.suggestedKey),q(t.context),q(t.nodeType),t.alreadyTranslated?'true':'false'].join(','));});rows.push('');rows.push('=== MISSING TRANSLATIONS ===');rows.push('\"Key\",\"Language\",\"Locale File\",\"Message\"');(r.details.missingKeys||[]).forEach(function(m){rows.push([q(m.key),q(m.language),q(m.filePath),q(m.message)].join(','));});rows.push('');rows.push('=== UNUSED KEYS ===');rows.push('\"Key\"');(r.details.unusedKeys||[]).forEach(function(k){rows.push(q(k));});rows.push('');rows.push('=== CDN ASSETS ===');rows.push('\"Local Path\",\"S3 Key\",\"CloudFront URL\",\"Content Type\",\"Size (KB)\"');(r.details.assets||[]).forEach(function(a){rows.push([q(a.localPath),q(a.s3Key),q(a.cloudfrontUrl),q(a.contentType),a.sizeKb].join(','));});downloadFile('ai-localize-full-report.csv',rows.join('\\n'),'text/csv');};\n" +
452
+ "function downloadFile(filename,content,mimeType){var a=document.createElement('a');a.href=URL.createObjectURL(new Blob([content],{type:mimeType}));a.download=filename;a.click();setTimeout(function(){URL.revokeObjectURL(a.href);},1000);}\n" +
453
+ // Keyboard
454
+ "document.addEventListener('keydown',function(e){if(e.key==='Escape'){document.querySelectorAll('.table-search').forEach(function(inp){inp.value='';var tbl=inp.closest('.table-wrapper').querySelector('table');if(tbl)filterTable(tbl.id,'');});}if((e.metaKey||e.ctrlKey)&&e.key==='d'){e.preventDefault();window.toggleTheme();}});\n" +
455
+ // Smooth scroll
456
+ "document.querySelectorAll('a.nav-item[href^=\"#\"]').forEach(function(a){a.addEventListener('click',function(e){e.preventDefault();var target=document.querySelector(a.getAttribute('href'));if(target)target.scrollIntoView({behavior:'smooth',block:'start'});});});\n" +
457
+ "})();";
458
+ }
459
+
460
+ var html = '<!DOCTYPE html>\n<html lang="en" data-theme="light">\n<head>\n' +
461
+ '<meta charset="UTF-8">\n' +
462
+ '<meta name="viewport" content="width=device-width, initial-scale=1.0">\n' +
463
+ '<title>ai-localize Dashboard &mdash; Preview</title>\n' +
464
+ '<style>' + extractedCSS + '</style>\n' +
465
+ '</head>\n<body>\n' +
466
+ '<button class="theme-toggle" id="theme-toggle" onclick="toggleTheme()" aria-label="Toggle theme" title="Toggle dark/light">' +
467
+ '<span id="theme-icon">&#9790;</span></button>\n' +
468
+ nav + '\n' +
469
+ '<main class="main" id="main">\n' +
470
+ '<header class="page-header">\n' +
471
+ '<div class="page-header-left">' +
472
+ '<h1 class="page-title">&#127760; Localization Analytics Dashboard</h1>' +
473
+ '<div class="page-meta">' +
474
+ '<span class="meta-chip">' + badge(r.framework, 'blue') + '</span>' +
475
+ '<span class="meta-item">&#128197; ' + esc(scanDate) + '</span>' +
476
+ '<span class="meta-item">&#9201; ' + r.duration + 'ms</span>' +
477
+ '<span class="meta-item">&#128196; ' + r.filesScanned + ' files</span>' +
478
+ '</div></div>' +
479
+ '<div class="page-header-right">' +
480
+ '<button class="btn btn-primary" onclick="expandAll()">Expand All</button>' +
481
+ '<button class="btn" onclick="collapseAll()">Collapse All</button>' +
482
+ '</div></header>\n\n' +
483
+
484
+ // Overview
485
+ '<section id="overview" class="section">' +
486
+ '<div class="section-title-row"><h2 class="section-title">&#128202; Summary</h2></div>' +
487
+ summaryCards + diffBanner +
488
+ '</section>\n\n' +
489
+
490
+ // Charts
491
+ '<section id="charts" class="section">' +
492
+ '<div class="section-title-row"><h2 class="section-title">&#128200; Analytics</h2></div>' +
493
+ '<div class="charts-grid">' +
494
+ '<div class="chart-card"><h3 class="chart-title">&#127919; Translation Coverage</h3><div class="chart-body chart-donut-wrap">' + buildCoverageDonut(ins.coveragePct) + '</div><p class="chart-caption">' + ins.coveragePct + '% of keys covered across all languages</p></div>' +
495
+ '<div class="chart-card"><h3 class="chart-title">&#128230; Keys by Namespace (Top 10)</h3><div class="chart-body">' + nsChart + '</div></div>' +
496
+ '<div class="chart-card"><h3 class="chart-title">&#127991; Texts by Context</h3><div class="chart-body">' + ctxChart + '</div></div>' +
497
+ '</div>' +
498
+ '</section>\n\n' +
499
+
500
+ // Hardcoded
501
+ '<section id="hardcoded" class="section">' +
502
+ '<div class="section-title-row"><h2 class="section-title">&#128269; Hardcoded Texts <span class="section-count ' + (r.hardcodedTexts > 0 ? 'count-warn' : 'count-ok') + '">' + r.hardcodedTexts + '</span></h2></div>' +
503
+ buildAccordion('hardcoded-main',
504
+ r.hardcodedTexts > 0 ? '&#9888; ' + r.hardcodedTexts + ' hardcoded string(s) detected' : '&#9989; No hardcoded texts',
505
+ r.hardcodedTexts > 0 ? 'Strings not yet wrapped in translation calls' : 'All strings are properly localized',
506
+ hardcodedContent, r.hardcodedTexts > 0, r.hardcodedTexts > 0 ? 'warn' : 'ok') +
507
+ '</section>\n\n' +
508
+
509
+ // Missing
510
+ '<section id="missing" class="section">' +
511
+ '<div class="section-title-row"><h2 class="section-title">&#10060; Missing Translations <span class="section-count ' + (r.missingTranslations > 0 ? 'count-err' : 'count-ok') + '">' + r.missingTranslations + '</span></h2></div>' +
512
+ buildAccordion('missing-main',
513
+ r.missingTranslations > 0 ? '&#10060; ' + r.missingTranslations + ' missing translation(s) found' : '&#9989; All translations present',
514
+ r.missingTranslations > 0 ? 'Keys absent in target language files' : 'All keys covered in every language',
515
+ missingContent, r.missingTranslations > 0, r.missingTranslations > 0 ? 'err' : 'ok') +
516
+ '</section>\n\n' +
517
+
518
+ // Unused
519
+ '<section id="unused" class="section">' +
520
+ '<div class="section-title-row"><h2 class="section-title">&#128465; Unused Keys <span class="section-count ' + (r.unusedKeys > 0 ? 'count-warn' : 'count-ok') + '">' + r.unusedKeys + '</span></h2></div>' +
521
+ buildAccordion('unused-main',
522
+ r.unusedKeys > 0 ? '&#9888; ' + r.unusedKeys + ' unused key(s) found' : '&#9989; No unused keys',
523
+ r.unusedKeys > 0 ? 'Keys in locale files but not in source' : 'All keys are actively used',
524
+ unusedContent, false, r.unusedKeys > 0 ? 'warn' : 'ok') +
525
+ '</section>\n\n' +
526
+
527
+ // Assets
528
+ '<section id="assets" class="section">' +
529
+ '<div class="section-title-row"><h2 class="section-title">&#128230; CDN Assets <span class="section-count count-neutral">' + r.assets.totalAssets + '</span></h2></div>' +
530
+ buildAccordion('assets-main',
531
+ '&#128230; ' + r.assets.totalAssets + ' assets found &middot; ' + r.assets.uploadedAssets + ' uploaded &middot; ' + r.assets.legacyCdnUrls + ' legacy URLs',
532
+ 'S3/CloudFront asset migration status',
533
+ assetsContent, false, r.assets.legacyCdnUrls > 0 ? 'warn' : 'ok') +
534
+ '</section>\n\n' +
535
+
536
+ // Insights
537
+ '<section id="insights" class="section">' +
538
+ '<div class="section-title-row"><h2 class="section-title">&#129504; AI Insights <span class="section-count ' + (ins.duplicates.length > 0 ? 'count-warn' : 'count-ok') + '">' + (ins.duplicates.length + ins.namespaceHints.length) + '</span></h2></div>' +
539
+ '<div class="insight-banner">&#129504; <strong>Deterministic analysis</strong> &mdash; patterns identified from your actual locale data. No LLM required.</div>' +
540
+ insightsContent +
541
+ '</section>\n\n' +
542
+
543
+ '<footer class="page-footer">Generated by <strong>ai-localize-core</strong> &mdash; deterministic, offline-capable i18n tooling &mdash; ' + esc(scanDate) + '</footer>\n' +
544
+ '</main>\n\n' +
545
+ '<script>' + inlineJS + '</script>\n' +
546
+ '</body>\n</html>';
547
+
548
+ // Write preview
549
+ var outDir = path.join(__dirname);
550
+ var outPath = path.join(outDir, 'report-preview.html');
551
+ fs.writeFileSync(outPath, html, 'utf8');
552
+ console.log('\x1b[92m✓\x1b[0m Preview written to: \x1b[96m' + outPath + '\x1b[0m');
553
+
554
+ // Open in browser (macOS / Linux / Windows)
555
+ var { execSync } = require('child_process');
556
+ try {
557
+ var platform = process.platform;
558
+ if (platform === 'darwin') execSync('open "' + outPath + '"');
559
+ else if (platform === 'win32') execSync('start "" "' + outPath + '"');
560
+ else execSync('xdg-open "' + outPath + '"');
561
+ console.log('\x1b[92m✓\x1b[0m Opened in default browser.');
562
+ } catch (e) {
563
+ console.log('\x1b[93m!\x1b[0m Could not auto-open browser. Open manually: file://' + outPath);
564
+ }