ai-localize-reporting 2.0.3 → 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 +23 -3
- package/preview/fix-exports.js +0 -147
- package/preview/fix-preview-exports.js +0 -166
- package/preview/generate-preview.ts +0 -105
- package/preview/make-preview.cjs +0 -564
- package/preview/make-preview.mjs +0 -398
- package/preview/report-preview.html +0 -831
- package/src/cli-reporter.ts +0 -601
- package/src/html-reporter.ts +0 -1690
- package/src/index.ts +0 -3
- package/src/report-builder.ts +0 -35
- package/tsconfig.json +0 -11
package/preview/make-preview.cjs
DELETED
|
@@ -1,564 +0,0 @@
|
|
|
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, '"');
|
|
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 + '">●</span>' +
|
|
69
|
-
'<span class="accordion-title">' + title + '</span>' +
|
|
70
|
-
'<span class="accordion-subtitle">' + subtitle + '</span>' +
|
|
71
|
-
'<span class="accordion-chevron">⌄</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">↕</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">🔍</span>' +
|
|
95
|
-
'<input type="text" class="table-search" placeholder="Search…" 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 + '\')">⇓ CSV</button>' +
|
|
100
|
-
'<button class="btn btn-sm" onclick="exportTableJson(\'' + tableId + '\')">⇓ 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', '📄') +
|
|
270
|
-
buildStatCard(r.hardcodedTexts, 'Hardcoded Texts', 'Raw strings not yet in t()', r.hardcodedTexts > 0 ? 'warn' : 'ok', '🔍') +
|
|
271
|
-
buildStatCard(ins.totalKeys, 'Unique Keys', 'Deduplicated locale keys', 'info', '🔑') +
|
|
272
|
-
buildStatCard(r.missingTranslations, 'Missing Translations', 'Absent in target languages', r.missingTranslations > 0 ? 'err' : 'ok', '❌') +
|
|
273
|
-
buildStatCard(r.unusedKeys, 'Unused Keys', 'In locale but not in source', r.unusedKeys > 0 ? 'warn' : 'ok', '🗑') +
|
|
274
|
-
buildStatCard(ins.duplicateKeyCount, 'Duplicate Keys', 'Same key, different texts', ins.duplicateKeyCount > 0 ? 'warn' : 'ok', '🔂') +
|
|
275
|
-
buildStatCard(ins.coveragePct + '%', 'Translation Coverage', '% of keys in all languages', ins.coveragePct < 80 ? 'err' : ins.coveragePct < 100 ? 'warn' : 'ok', '🎯') +
|
|
276
|
-
buildStatCard(r.assets.totalAssets, 'Assets Found', 'Static asset references', 'neutral', '📦') +
|
|
277
|
-
buildStatCard(r.assets.legacyCdnUrls, 'Legacy CDN URLs', 'Old CDN refs pending', r.assets.legacyCdnUrls > 0 ? 'warn' : 'ok', '🔗') +
|
|
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 ? '…' : '') + '</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') : '—',
|
|
309
|
-
e.filePath ? '<code class="path-code">' + esc(e.filePath.split('/').slice(-2).join('/')) + '</code>' : '—',
|
|
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">✅ 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 ? '…' : '') + '</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', '💾') +
|
|
335
|
-
buildStatCard(r.assets.uploadedAssets, 'Uploaded', 'Pushed to S3/CDN', 'ok', '✅') +
|
|
336
|
-
buildStatCard(r.assets.replacedUrls, 'URLs Replaced', '', 'ok', '🔁') +
|
|
337
|
-
buildStatCard(r.assets.legacyCdnUrls, 'Legacy URLs', 'run replace-cdn', r.assets.legacyCdnUrls > 0 ? 'warn' : 'ok', '⚠') +
|
|
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">✅ 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">✅ 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">✅ 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">✅ 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') + ' — only ' + h.count + ' key' + (h.count > 1 ? 's' : '') + ' — consider merging.</li>';
|
|
368
|
-
}).join('') + '</ul>';
|
|
369
|
-
|
|
370
|
-
var insightsContent = '<div class="insights-grid">' +
|
|
371
|
-
'<div class="insight-card"><h3 class="insight-heading">🔂 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">🌍 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">🗑 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">🌸 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>ℹ 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>✅ 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">🌐</span><span class="sidebar-brand">ai-localize</span><button class="sidebar-toggle" onclick="toggleSidebar()">☰</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">📊</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">📈</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">🔍</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">❌</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">🗑</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">📦</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">🧠</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()">⇓ Export JSON</button>' +
|
|
402
|
-
'<button class="nav-item nav-btn" onclick="exportFullCsv()">⇓ Export CSV</button>' +
|
|
403
|
-
'<button class="nav-item nav-btn" onclick="window.print()">🖨 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 — 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">☾</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">🌐 Localization Analytics Dashboard</h1>' +
|
|
473
|
-
'<div class="page-meta">' +
|
|
474
|
-
'<span class="meta-chip">' + badge(r.framework, 'blue') + '</span>' +
|
|
475
|
-
'<span class="meta-item">📅 ' + esc(scanDate) + '</span>' +
|
|
476
|
-
'<span class="meta-item">⏱ ' + r.duration + 'ms</span>' +
|
|
477
|
-
'<span class="meta-item">📄 ' + 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">📊 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">📈 Analytics</h2></div>' +
|
|
493
|
-
'<div class="charts-grid">' +
|
|
494
|
-
'<div class="chart-card"><h3 class="chart-title">🎯 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">📦 Keys by Namespace (Top 10)</h3><div class="chart-body">' + nsChart + '</div></div>' +
|
|
496
|
-
'<div class="chart-card"><h3 class="chart-title">🏷 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">🔍 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 ? '⚠ ' + r.hardcodedTexts + ' hardcoded string(s) detected' : '✅ 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">❌ 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 ? '❌ ' + r.missingTranslations + ' missing translation(s) found' : '✅ 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">🗑 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 ? '⚠ ' + r.unusedKeys + ' unused key(s) found' : '✅ 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">📦 CDN Assets <span class="section-count count-neutral">' + r.assets.totalAssets + '</span></h2></div>' +
|
|
530
|
-
buildAccordion('assets-main',
|
|
531
|
-
'📦 ' + r.assets.totalAssets + ' assets found · ' + r.assets.uploadedAssets + ' uploaded · ' + 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">🧠 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">🧠 <strong>Deterministic analysis</strong> — 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> — deterministic, offline-capable i18n tooling — ' + 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
|
-
}
|