ai-localize-reporting 2.0.5 → 2.0.7
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/CHANGELOG.md +32 -0
- package/LICENSE +21 -0
- package/README.md +77 -0
- package/dist/index.js +178 -72
- package/dist/index.mjs +178 -72
- package/package.json +14 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# ai-localize-reporting
|
|
2
2
|
|
|
3
|
+
## 2.0.7
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- **HTML report UI refinements** (9 fixes):
|
|
8
|
+
- Accordion chevron now rotates correctly on expand/collapse (CSS `transform` fix).
|
|
9
|
+
- Translation coverage % in the stat card now shows the correct rounded integer.
|
|
10
|
+
- Filter input and pagination controls are now visible and functional on all tables.
|
|
11
|
+
- Language filter chips on the Missing Translations table now toggle correctly.
|
|
12
|
+
- Keyboard shortcut hint (`Cmd+D` / `Ctrl+D`) for dark-mode toggle is now displayed.
|
|
13
|
+
- Mobile sidebar collapses automatically after a nav link is clicked.
|
|
14
|
+
- Print/PDF CSS hides interactive controls and expands all accordions.
|
|
15
|
+
- Stat card hover-lift animation no longer causes layout shift on Safari.
|
|
16
|
+
- CSV/JSON export buttons now correctly serialise filtered (not full) table data.
|
|
17
|
+
- **91 new tests** — 58 `html-reporter` tests + 33 `cli-reporter` tests covering all
|
|
18
|
+
major rendering paths, edge cases, and the new UI fixes.
|
|
19
|
+
- **CLI reporter circular next-steps fix** — the "Recommended next steps" list no longer
|
|
20
|
+
repeats the same step in a loop when all checks pass.
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- Added `repository`, `homepage`, `bugs`, `author` fields to `package.json` for npm registry display.
|
|
25
|
+
- Added `sideEffects: false` to enable tree-shaking in bundlers.
|
|
26
|
+
- Added `prepublishOnly` script to ensure the package is built before publishing.
|
|
27
|
+
|
|
28
|
+
## 2.0.6
|
|
29
|
+
|
|
30
|
+
### Patch Changes
|
|
31
|
+
|
|
32
|
+
- Add per-package README.md files so each package displays documentation on npmjs.com
|
|
33
|
+
- Update README version badge to 2.0.6
|
|
34
|
+
|
|
3
35
|
## 2.0.5
|
|
4
36
|
|
|
5
37
|
### Patch Changes
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026 ai-localize-core contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# ai-localize-reporting
|
|
2
|
+
|
|
3
|
+
> HTML analytics dashboard + rich CLI terminal reporter for the [ai-localize-core](https://github.com/ai-localize/ai-localize-core) platform.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/ai-localize-reporting)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## What it does
|
|
11
|
+
|
|
12
|
+
- **`buildReport()`** — assembles a `Report` object from scan + validation results
|
|
13
|
+
- **`generateHtmlReport()`** — writes a self-contained, interactive HTML analytics dashboard
|
|
14
|
+
- **`printCliSummary()`** — prints a rich structured terminal summary with ANSI colour, bar charts, and tables
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install ai-localize-reporting
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { buildReport, generateHtmlReport, printCliSummary } from 'ai-localize-reporting';
|
|
26
|
+
|
|
27
|
+
const report = buildReport({
|
|
28
|
+
scanResult,
|
|
29
|
+
validationResult,
|
|
30
|
+
uploadedAssets, // optional
|
|
31
|
+
replacedUrls, // optional
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Write HTML dashboard
|
|
35
|
+
generateHtmlReport(report, './.reports/report.html');
|
|
36
|
+
|
|
37
|
+
// Print terminal summary
|
|
38
|
+
printCliSummary(report);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## HTML report features
|
|
42
|
+
|
|
43
|
+
- **Sticky sidebar navigation** with active section highlighting and alert indicators
|
|
44
|
+
- **Light / dark theme** — system preference, `localStorage`, `Cmd+D` / `Ctrl+D` shortcut
|
|
45
|
+
- **9 stat cards** — top-border colour coding (ok / warn / error / info)
|
|
46
|
+
- **SVG donut chart** — translation coverage % (no external dependencies)
|
|
47
|
+
- **Bar charts** — keys by namespace, texts by AST context
|
|
48
|
+
- **Interactive tables** — search, sort, CSV/JSON export, pagination (50 rows/page)
|
|
49
|
+
- **AI Insights** (100% deterministic) — duplicate text detection, translation inconsistency, unused keys, namespace cleanup hints
|
|
50
|
+
- **Export panel** — full report JSON, summary CSV, print/PDF
|
|
51
|
+
- **Responsive + accessible** — ARIA roles, keyboard navigation, mobile sidebar
|
|
52
|
+
|
|
53
|
+
## CLI terminal summary features
|
|
54
|
+
|
|
55
|
+
- **Zero new dependencies** — all ANSI codes inlined; `NO_COLOR` env var respected
|
|
56
|
+
- **Full-width banner** with centred title
|
|
57
|
+
- **9 stat lines** with status dots (green/yellow/red) and contextual hints
|
|
58
|
+
- **Coverage progress bar** (24-block, colour-coded)
|
|
59
|
+
- **Top-files bar chart**, missing-translations ranking, namespace distribution, AST context distribution
|
|
60
|
+
- **AI Insights block** and recommended next steps
|
|
61
|
+
- **Responsive width** — reads `process.stdout.columns`, caps at 120 chars
|
|
62
|
+
|
|
63
|
+
## Coverage calculation (v2.0.5+)
|
|
64
|
+
|
|
65
|
+
Coverage % is computed using **distinct missing key names** (not raw per-language count), so it correctly reflects the fraction of unique keys that have been translated:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
coverage = (totalUniqueKeys - distinctMissingKeys) / totalUniqueKeys × 100
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Part of ai-localize-core
|
|
74
|
+
|
|
75
|
+
Install the CLI for the complete toolset: `npm install -g ai-localize-cli`
|
|
76
|
+
|
|
77
|
+
MIT © ai-localize-core contributors
|
package/dist/index.js
CHANGED
|
@@ -135,15 +135,16 @@ function computeInsights(report) {
|
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
const totalKeys = new Set(details.detectedTexts.map((d) => d.suggestedKey)).size;
|
|
138
|
-
const
|
|
139
|
-
const
|
|
138
|
+
const distinctMissingKeys = new Set(details.missingKeys.map((mk) => mk.key)).size;
|
|
139
|
+
const coveredKeys = Math.max(0, totalKeys - distinctMissingKeys);
|
|
140
|
+
const coveragePct = totalKeys > 0 ? Math.round(coveredKeys / totalKeys * 100) : 100;
|
|
140
141
|
return { duplicates, inconsistencies, namespaceHints, coveragePct, totalKeys, duplicateKeyCount };
|
|
141
142
|
}
|
|
142
143
|
function buildNavItem(id, icon, label, count, alert = false) {
|
|
143
144
|
return `<a href="#${id}" class="nav-item${alert ? " nav-alert" : ""}" data-section="${id}">
|
|
144
145
|
<span class="nav-icon">${icon}</span>
|
|
145
146
|
<span class="nav-label">${esc(label)}</span>
|
|
146
|
-
|
|
147
|
+
<span class="nav-count">${count}</span>
|
|
147
148
|
</a>`;
|
|
148
149
|
}
|
|
149
150
|
function buildStatCard(value, label, hint, status, icon) {
|
|
@@ -156,15 +157,15 @@ function buildStatCard(value, label, hint, status, icon) {
|
|
|
156
157
|
}
|
|
157
158
|
function buildAccordion(id, title, subtitle, content, open = false, severity = "info") {
|
|
158
159
|
return `<div class="accordion${open ? " accordion-open" : ""}" id="acc-${id}">
|
|
159
|
-
<button class="accordion-trigger" aria-expanded="${open}" aria-controls="panel-${id}" onclick="toggleAccordion('${id}')">
|
|
160
|
-
<span class="accordion-icon severity-${severity}">●</span>
|
|
160
|
+
<button class="accordion-trigger" aria-expanded="${open ? "true" : "false"}" aria-controls="panel-${id}" onclick="toggleAccordion('${escJs(id)}')">
|
|
161
|
+
<span class="accordion-icon severity-${severity}" aria-hidden="true">●</span>
|
|
161
162
|
<span class="accordion-title">${title}</span>
|
|
162
|
-
<span class="accordion-subtitle">${subtitle}</span>
|
|
163
|
-
<span class="accordion-chevron">&#
|
|
163
|
+
<span class="accordion-subtitle">${subtitle}</span>
|
|
164
|
+
<span class="accordion-chevron" aria-hidden="true">▼</span>
|
|
164
165
|
</button>
|
|
165
|
-
<div class="accordion-panel" id="panel-${id}" role="region" ${open ? "" : "hidden"}>
|
|
166
|
+
<div class="accordion-panel" id="panel-${id}" role="region" aria-labelledby="acc-${id}"${open ? "" : " hidden"}>
|
|
166
167
|
<div class="accordion-body">${content}</div>
|
|
167
|
-
|
|
168
|
+
</div>
|
|
168
169
|
</div>`;
|
|
169
170
|
}
|
|
170
171
|
function buildCoverageDonut(pct) {
|
|
@@ -304,13 +305,17 @@ function buildHtml(report) {
|
|
|
304
305
|
e.filePath ? `<code class="path-code">${esc(e.filePath)}</code>` : "\u2014",
|
|
305
306
|
`<span class="detail-text">${esc(e.message)}</span>`
|
|
306
307
|
]);
|
|
307
|
-
const langChips = [...missingByLang.entries()].map(([lang, keys]) => `<
|
|
308
|
+
const langChips = [...missingByLang.entries()].map(([lang, keys]) => `<button class="chip chip-red" onclick="applyLangFilter('${escJs(lang)}')" aria-label="Filter by ${esc(lang)}">${esc(lang)} <strong>${keys.length}</strong></button>`).join(" ");
|
|
308
309
|
missingContent = `
|
|
309
310
|
<div class="insight-legend">
|
|
310
311
|
These locale keys exist in the <strong>default language</strong> but are <strong>absent</strong> in one or more target language files.
|
|
311
312
|
Ask your translators to fill in these entries. Running <code>ai-localize extract</code> seeds all target files with the source value.
|
|
312
313
|
</div>
|
|
313
|
-
<div class="chip-row">
|
|
314
|
+
<div class="chip-row">
|
|
315
|
+
<span class="chip-label">Filter by language:</span>
|
|
316
|
+
${langChips}
|
|
317
|
+
<button class="chip chip-clear" onclick="applyLangFilter('')" aria-label="Clear language filter">✕ Clear</button>
|
|
318
|
+
</div>
|
|
314
319
|
${buildSearchableTable("tbl-missing", [
|
|
315
320
|
{ key: "key", label: "Key" },
|
|
316
321
|
{ key: "language", label: "Language", width: "100px" },
|
|
@@ -477,7 +482,7 @@ ${nav}
|
|
|
477
482
|
<div class="page-header-left">
|
|
478
483
|
<h1 class="page-title">🌐 Localization Analytics Dashboard</h1>
|
|
479
484
|
<div class="page-meta">
|
|
480
|
-
<span class="meta-chip">${badge(framework, "blue")}</span>
|
|
485
|
+
<span class="meta-chip">${badge(framework, "blue")}</span>
|
|
481
486
|
<span class="meta-item">📅 ${scanDate}</span>
|
|
482
487
|
<span class="meta-item">⏱ ${duration}ms</span>
|
|
483
488
|
<span class="meta-item">📄 ${filesScanned} files</span>
|
|
@@ -492,11 +497,11 @@ ${nav}
|
|
|
492
497
|
<!-- Summary -->
|
|
493
498
|
<section id="overview" class="section">
|
|
494
499
|
<div class="section-title-row">
|
|
495
|
-
|
|
500
|
+
<h2 class="section-title">📊 Summary</h2>
|
|
496
501
|
</div>
|
|
497
502
|
${summaryCards}
|
|
498
503
|
${diffExplainer}
|
|
499
|
-
</section>
|
|
504
|
+
</section>
|
|
500
505
|
|
|
501
506
|
<!-- Charts -->
|
|
502
507
|
<section id="charts" class="section">
|
|
@@ -553,7 +558,7 @@ ${nav}
|
|
|
553
558
|
|
|
554
559
|
<!-- Assets -->
|
|
555
560
|
<section id="assets" class="section">
|
|
556
|
-
|
|
561
|
+
<div class="section-title-row">
|
|
557
562
|
<h2 class="section-title">📦 CDN Assets <span class="section-count count-neutral">${assets.totalAssets}</span></h2>
|
|
558
563
|
</div>
|
|
559
564
|
${buildAccordion(
|
|
@@ -564,7 +569,7 @@ ${nav}
|
|
|
564
569
|
false,
|
|
565
570
|
assets.legacyCdnUrls > 0 ? "warn" : "ok"
|
|
566
571
|
)}
|
|
567
|
-
</section>
|
|
572
|
+
</section>
|
|
568
573
|
|
|
569
574
|
<!-- AI Insights -->
|
|
570
575
|
<section id="insights" class="section">
|
|
@@ -579,6 +584,9 @@ ${nav}
|
|
|
579
584
|
|
|
580
585
|
<footer class="page-footer">
|
|
581
586
|
Generated by <strong>ai-localize-core</strong> — deterministic, offline-capable i18n tooling — ${scanDate}
|
|
587
|
+
<span class="footer-shortcuts" aria-label="Keyboard shortcuts">
|
|
588
|
+
<kbd>Esc</kbd> clear search • <kbd>Cmd+D</kbd> toggle theme • <kbd>Tab</kbd> navigate
|
|
589
|
+
</span>
|
|
582
590
|
</footer>
|
|
583
591
|
</main>
|
|
584
592
|
|
|
@@ -653,10 +661,14 @@ function JS(report, insights) {
|
|
|
653
661
|
// \u2500\u2500 Active nav \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
654
662
|
var sections = document.querySelectorAll('section[id]');
|
|
655
663
|
var navItems = document.querySelectorAll('.nav-item[data-section]');
|
|
664
|
+
// Use 120px offset to account for sticky table headers and give visual lead-time
|
|
665
|
+
var NAV_OFFSET = 120;
|
|
656
666
|
function onScroll() {
|
|
657
|
-
var scrollY = window.scrollY +
|
|
667
|
+
var scrollY = window.scrollY + NAV_OFFSET;
|
|
658
668
|
var current = '';
|
|
659
|
-
sections.forEach(function(s) {
|
|
669
|
+
sections.forEach(function(s) {
|
|
670
|
+
if (scrollY >= s.offsetTop) current = s.id;
|
|
671
|
+
});
|
|
660
672
|
navItems.forEach(function(a) {
|
|
661
673
|
a.classList.toggle('nav-active', a.getAttribute('data-section') === current);
|
|
662
674
|
});
|
|
@@ -668,10 +680,18 @@ var scrollY = window.scrollY + 80;
|
|
|
668
680
|
window.toggleAccordion = function(id) {
|
|
669
681
|
var acc = document.getElementById('acc-' + id);
|
|
670
682
|
var panel = document.getElementById('panel-' + id);
|
|
683
|
+
if (!acc || !panel) return;
|
|
671
684
|
var btn = acc.querySelector('.accordion-trigger');
|
|
672
|
-
var
|
|
673
|
-
|
|
674
|
-
|
|
685
|
+
var isOpen = acc.classList.contains('accordion-open');
|
|
686
|
+
if (isOpen) {
|
|
687
|
+
acc.classList.remove('accordion-open');
|
|
688
|
+
btn.setAttribute('aria-expanded', 'false');
|
|
689
|
+
panel.setAttribute('hidden', '');
|
|
690
|
+
} else {
|
|
691
|
+
acc.classList.add('accordion-open');
|
|
692
|
+
btn.setAttribute('aria-expanded', 'true');
|
|
693
|
+
panel.removeAttribute('hidden');
|
|
694
|
+
}
|
|
675
695
|
};
|
|
676
696
|
window.expandAll = function() {
|
|
677
697
|
document.querySelectorAll('.accordion').forEach(function(acc) {
|
|
@@ -679,7 +699,7 @@ var scrollY = window.scrollY + 80;
|
|
|
679
699
|
var panel = document.getElementById('panel-' + id);
|
|
680
700
|
var btn = acc.querySelector('.accordion-trigger');
|
|
681
701
|
acc.classList.add('accordion-open');
|
|
682
|
-
btn.setAttribute('aria-expanded', 'true');
|
|
702
|
+
if (btn) btn.setAttribute('aria-expanded', 'true');
|
|
683
703
|
if (panel) panel.removeAttribute('hidden');
|
|
684
704
|
});
|
|
685
705
|
};
|
|
@@ -687,30 +707,40 @@ var scrollY = window.scrollY + 80;
|
|
|
687
707
|
document.querySelectorAll('.accordion').forEach(function(acc) {
|
|
688
708
|
var id = acc.id.replace('acc-', '');
|
|
689
709
|
var panel = document.getElementById('panel-' + id);
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
btn.setAttribute('aria-expanded', 'false');
|
|
710
|
+
var btn = acc.querySelector('.accordion-trigger');
|
|
711
|
+
acc.classList.remove('accordion-open');
|
|
712
|
+
if (btn) btn.setAttribute('aria-expanded', 'false');
|
|
693
713
|
if (panel) panel.setAttribute('hidden', '');
|
|
694
714
|
});
|
|
695
715
|
};
|
|
696
716
|
|
|
697
|
-
// \u2500\u2500 Table: filter \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
717
|
+
// \u2500\u2500 Table: filter \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
718
|
+
// Mark rows with data-filtered attribute instead of display:none so that
|
|
719
|
+
// renderPagination can independently control visibility for pagination.
|
|
698
720
|
window.filterTable = function(tableId, query) {
|
|
699
721
|
var tbl = document.getElementById(tableId);
|
|
700
722
|
if (!tbl) return;
|
|
701
|
-
var q = query.toLowerCase();
|
|
723
|
+
var q = (query || '').toLowerCase().trim();
|
|
702
724
|
var rows = tbl.querySelectorAll('tbody tr');
|
|
703
|
-
var shown = 0;
|
|
704
725
|
rows.forEach(function(row) {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
726
|
+
if (q === '' || row.textContent.toLowerCase().indexOf(q) !== -1) {
|
|
727
|
+
row.removeAttribute('data-filtered');
|
|
728
|
+
} else {
|
|
729
|
+
row.setAttribute('data-filtered', '1');
|
|
730
|
+
}
|
|
708
731
|
});
|
|
709
|
-
|
|
710
|
-
|
|
732
|
+
// Reset to page 1 whenever filter changes
|
|
733
|
+
tbl.setAttribute('data-page', '1');
|
|
711
734
|
renderPagination(tableId);
|
|
712
735
|
};
|
|
713
736
|
|
|
737
|
+
// \u2500\u2500 Language chip filter (syncs search-box input + resets page) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
738
|
+
window.applyLangFilter = function(lang) {
|
|
739
|
+
var inp = document.querySelector('#tbl-missing-wrapper .table-search');
|
|
740
|
+
if (inp) inp.value = lang;
|
|
741
|
+
window.filterTable('tbl-missing', lang);
|
|
742
|
+
};
|
|
743
|
+
|
|
714
744
|
// \u2500\u2500 Table: sort \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
715
745
|
var sortState = {};
|
|
716
746
|
window.sortTable = function(tableId, col) {
|
|
@@ -721,39 +751,52 @@ var scrollY = window.scrollY + 80;
|
|
|
721
751
|
var colIndex = -1;
|
|
722
752
|
tbl.querySelectorAll('thead th').forEach(function(th, i) {
|
|
723
753
|
if (th.getAttribute('data-col') === col) colIndex = i;
|
|
724
|
-
|
|
754
|
+
var icon = th.querySelector('.sort-icon');
|
|
755
|
+
if (icon) icon.textContent = '\\u21C5';
|
|
725
756
|
});
|
|
726
|
-
|
|
757
|
+
if (colIndex === -1) return;
|
|
727
758
|
var th = tbl.querySelector('thead th[data-col="' + col + '"]');
|
|
728
|
-
if (th)
|
|
759
|
+
if (th) {
|
|
760
|
+
var icon = th.querySelector('.sort-icon');
|
|
761
|
+
if (icon) icon.textContent = dir === 'asc' ? '\\u2191' : '\\u2193';
|
|
762
|
+
}
|
|
729
763
|
var tbody = tbl.querySelector('tbody');
|
|
730
764
|
var rows = Array.from(tbody.querySelectorAll('tr'));
|
|
731
765
|
rows.sort(function(a, b) {
|
|
732
766
|
var av = (a.cells[colIndex] || {}).textContent || '';
|
|
733
|
-
|
|
767
|
+
var bv = (b.cells[colIndex] || {}).textContent || '';
|
|
734
768
|
var an = parseFloat(av), bn = parseFloat(bv);
|
|
735
769
|
if (!isNaN(an) && !isNaN(bn)) return dir === 'asc' ? an - bn : bn - an;
|
|
736
770
|
return dir === 'asc' ? av.localeCompare(bv) : bv.localeCompare(av);
|
|
737
771
|
});
|
|
738
772
|
rows.forEach(function(r) { tbody.appendChild(r); });
|
|
739
|
-
renderPagination(tableId);
|
|
773
|
+
renderPagination(tableId);
|
|
740
774
|
};
|
|
741
775
|
|
|
742
776
|
// \u2500\u2500 Table: pagination \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
777
|
+
// Reads data-filtered attribute set by filterTable; does not conflict with
|
|
778
|
+
// filter visibility since rows are never hidden by this function until
|
|
779
|
+
// it has determined which rows belong to the current page.
|
|
743
780
|
function renderPagination(tableId) {
|
|
744
781
|
var tbl = document.getElementById(tableId);
|
|
745
782
|
var pag = document.getElementById(tableId + '-pagination');
|
|
746
783
|
var meta = document.getElementById(tableId + '-meta');
|
|
747
784
|
if (!tbl || !pag) return;
|
|
748
785
|
var pageSize = parseInt(tbl.getAttribute('data-page-size') || '50', 10);
|
|
749
|
-
var
|
|
750
|
-
|
|
751
|
-
var
|
|
786
|
+
var allRows = Array.from(tbl.querySelectorAll('tbody tr'));
|
|
787
|
+
// Rows that passed the current filter
|
|
788
|
+
var visibleRows = allRows.filter(function(r) { return !r.hasAttribute('data-filtered'); });
|
|
789
|
+
var total = visibleRows.length;
|
|
790
|
+
var pages = Math.max(1, Math.ceil(total / pageSize));
|
|
752
791
|
var page = parseInt(tbl.getAttribute('data-page') || '1', 10);
|
|
753
792
|
if (page > pages) page = 1;
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
793
|
+
if (page < 1) page = 1;
|
|
794
|
+
tbl.setAttribute('data-page', String(page));
|
|
795
|
+
// First hide all rows, then show only the current page slice of visible rows
|
|
796
|
+
allRows.forEach(function(r) { r.style.display = 'none'; });
|
|
797
|
+
var startIdx = (page - 1) * pageSize;
|
|
798
|
+
visibleRows.forEach(function(r, i) {
|
|
799
|
+
if (i >= startIdx && i < startIdx + pageSize) r.style.display = '';
|
|
757
800
|
});
|
|
758
801
|
if (meta) meta.textContent = total + ' row' + (total !== 1 ? 's' : '');
|
|
759
802
|
pag.innerHTML = '';
|
|
@@ -763,13 +806,13 @@ renderPagination(tableId);
|
|
|
763
806
|
b.textContent = label;
|
|
764
807
|
b.className = 'pag-btn' + (active ? ' pag-active' : '') + (disabled ? ' pag-disabled' : '');
|
|
765
808
|
b.disabled = disabled;
|
|
766
|
-
b.onclick = function() { tbl.setAttribute('data-page', p); renderPagination(tableId); };
|
|
809
|
+
b.onclick = function() { tbl.setAttribute('data-page', String(p)); renderPagination(tableId); };
|
|
767
810
|
return b;
|
|
768
811
|
}
|
|
769
|
-
|
|
812
|
+
pag.appendChild(btn('\\u00AB', 1, false, page === 1));
|
|
770
813
|
pag.appendChild(btn('\\u2039', page - 1, false, page === 1));
|
|
771
|
-
var
|
|
772
|
-
for (var i =
|
|
814
|
+
var winStart = Math.max(1, page - 2), winEnd = Math.min(pages, page + 2);
|
|
815
|
+
for (var i = winStart; i <= winEnd; i++) pag.appendChild(btn(i, i, i === page, false));
|
|
773
816
|
pag.appendChild(btn('\\u203A', page + 1, false, page === pages));
|
|
774
817
|
pag.appendChild(btn('\\u00BB', pages, false, page === pages));
|
|
775
818
|
var info = document.createElement('span');
|
|
@@ -877,21 +920,33 @@ renderPagination(tableId);
|
|
|
877
920
|
|
|
878
921
|
function downloadFile(filename, content, mimeType) {
|
|
879
922
|
var a = document.createElement('a');
|
|
880
|
-
|
|
923
|
+
a.href = URL.createObjectURL(new Blob([content], { type: mimeType }));
|
|
881
924
|
a.download = filename;
|
|
882
925
|
a.click();
|
|
883
926
|
setTimeout(function() { URL.revokeObjectURL(a.href); }, 1000);
|
|
884
927
|
}
|
|
885
928
|
|
|
886
929
|
// \u2500\u2500 Keyboard navigation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
930
|
+
// Escape: clear all table search inputs and reset filters
|
|
931
|
+
// Cmd/Ctrl+D: toggle dark/light theme
|
|
887
932
|
document.addEventListener('keydown', function(e) {
|
|
888
933
|
if (e.key === 'Escape') {
|
|
889
|
-
document.querySelectorAll('.table-search').forEach(function(inp) {
|
|
934
|
+
document.querySelectorAll('.table-search').forEach(function(inp) {
|
|
935
|
+
var wrapper = inp.closest('.table-wrapper');
|
|
936
|
+
if (!wrapper) return;
|
|
937
|
+
var tbl = wrapper.querySelector('table');
|
|
938
|
+
if (!tbl) return;
|
|
939
|
+
inp.value = '';
|
|
940
|
+
window.filterTable(tbl.id, '');
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'd') {
|
|
944
|
+
e.preventDefault();
|
|
945
|
+
window.toggleTheme();
|
|
890
946
|
}
|
|
891
|
-
if ((e.metaKey || e.ctrlKey) && e.key === 'd') { e.preventDefault(); window.toggleTheme(); }
|
|
892
947
|
});
|
|
893
948
|
|
|
894
|
-
// \u2500\u2500 Smooth scroll for nav \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
949
|
+
// \u2500\u2500 Smooth scroll for nav \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
895
950
|
document.querySelectorAll('a.nav-item[href^="#"]').forEach(function(a) {
|
|
896
951
|
a.addEventListener('click', function(e) {
|
|
897
952
|
e.preventDefault();
|
|
@@ -1238,10 +1293,12 @@ padding: 6px 16px 3px;
|
|
|
1238
1293
|
}
|
|
1239
1294
|
.accordion-subtitle { font-size: 12px; color: var(--text-muted); margin-right: 12px; }
|
|
1240
1295
|
.accordion-chevron {
|
|
1241
|
-
font-size:
|
|
1296
|
+
font-size: 14px;
|
|
1242
1297
|
color: var(--text-muted);
|
|
1243
1298
|
transition: transform var(--transition);
|
|
1299
|
+
display: inline-block;
|
|
1244
1300
|
}
|
|
1301
|
+
/* \u25BE (▼) points down by default; rotates to \u25B4 when open */
|
|
1245
1302
|
.accordion-open .accordion-chevron { transform: rotate(180deg); }
|
|
1246
1303
|
.accordion-panel { border-top: 1px solid var(--border); }
|
|
1247
1304
|
.accordion-body { padding: 18px; }
|
|
@@ -1311,15 +1368,20 @@ border-radius: var(--radius);
|
|
|
1311
1368
|
|
|
1312
1369
|
/* \u2500\u2500 Chip filter row \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
1313
1370
|
.chip-row { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; align-items: center; font-size: 12px; color: var(--text-muted); }
|
|
1371
|
+
.chip-label { font-size: 12px; color: var(--text-muted); white-space: nowrap; }
|
|
1314
1372
|
.chip {
|
|
1315
1373
|
display: inline-flex; align-items: center; gap: 4px;
|
|
1316
1374
|
padding: 3px 10px; border-radius: 14px;
|
|
1317
|
-
font-size: 12px; font-weight: 600;
|
|
1375
|
+
font-size: 12px; font-weight: 600;
|
|
1318
1376
|
cursor: pointer; border: 1px solid transparent;
|
|
1319
|
-
transition: box-shadow var(--transition);
|
|
1377
|
+
transition: box-shadow var(--transition), background var(--transition);
|
|
1378
|
+
font-family: var(--font);
|
|
1320
1379
|
}
|
|
1321
1380
|
.chip:hover { box-shadow: 0 0 0 2px var(--accent); }
|
|
1381
|
+
.chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
1322
1382
|
.chip-red { background: #fee2e2; color: #991b1b; }
|
|
1383
|
+
.chip-clear { background: var(--bg-elevated); color: var(--text-muted); border: 1px solid var(--border); }
|
|
1384
|
+
.chip-clear:hover { background: var(--border); box-shadow: none; }
|
|
1323
1385
|
[data-theme="dark"] .chip-red { background: #450a0a; color: #fca5a5; }
|
|
1324
1386
|
|
|
1325
1387
|
/* \u2500\u2500 Tables \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
@@ -1586,25 +1648,57 @@ font-size: 12px;
|
|
|
1586
1648
|
font-size: 12px;
|
|
1587
1649
|
color: var(--text-subtle);
|
|
1588
1650
|
text-align: center;
|
|
1651
|
+
display: flex;
|
|
1652
|
+
flex-direction: column;
|
|
1653
|
+
align-items: center;
|
|
1654
|
+
gap: 8px;
|
|
1655
|
+
}
|
|
1656
|
+
.footer-shortcuts { font-size: 11px; color: var(--text-subtle); }
|
|
1657
|
+
kbd {
|
|
1658
|
+
display: inline-block;
|
|
1659
|
+
padding: 1px 6px;
|
|
1660
|
+
font-size: 10px;
|
|
1661
|
+
font-family: var(--font-mono);
|
|
1662
|
+
background: var(--bg-elevated);
|
|
1663
|
+
border: 1px solid var(--border);
|
|
1664
|
+
border-bottom-width: 2px;
|
|
1665
|
+
border-radius: 4px;
|
|
1666
|
+
color: var(--text-muted);
|
|
1667
|
+
white-space: nowrap;
|
|
1589
1668
|
}
|
|
1590
1669
|
|
|
1591
1670
|
/* \u2500\u2500 Print \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
1592
1671
|
@media print {
|
|
1593
1672
|
.sidebar, .theme-toggle, .table-controls, .table-pagination, .page-header-right { display: none !important; }
|
|
1594
1673
|
.main { margin-left: 0 !important; padding: 16px; }
|
|
1595
|
-
|
|
1596
|
-
.accordion-panel
|
|
1674
|
+
/* Force accordion panels visible \u2014 the hidden attribute needs !important override */
|
|
1675
|
+
.accordion-panel { display: block !important; visibility: visible !important; }
|
|
1676
|
+
.accordion-panel[hidden] { display: block !important; visibility: visible !important; }
|
|
1597
1677
|
.stat-card, .chart-card, .accordion, .table-wrapper { break-inside: avoid; }
|
|
1598
1678
|
body { background: white; color: black; }
|
|
1679
|
+
a { color: inherit; text-decoration: none; }
|
|
1599
1680
|
}
|
|
1600
1681
|
|
|
1601
|
-
/* \u2500\u2500 Responsive \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1682
|
+
/* \u2500\u2500 Responsive / Mobile \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
1602
1683
|
@media (max-width: 768px) {
|
|
1603
1684
|
body { flex-direction: column; }
|
|
1604
|
-
|
|
1685
|
+
/* On mobile the sidebar becomes an in-flow top-bar, not fixed/absolute */
|
|
1686
|
+
.sidebar {
|
|
1687
|
+
position: relative;
|
|
1688
|
+
width: 100%;
|
|
1689
|
+
height: auto;
|
|
1690
|
+
overflow: visible;
|
|
1691
|
+
border-right: none;
|
|
1692
|
+
border-bottom: 1px solid var(--border);
|
|
1693
|
+
flex-direction: column;
|
|
1694
|
+
}
|
|
1695
|
+
.nav-group { display: flex; flex-wrap: wrap; padding: 4px 8px; border-bottom: none; }
|
|
1696
|
+
.nav-group-label { width: 100%; padding: 4px 8px 2px; }
|
|
1697
|
+
.nav-item { padding: 6px 10px; border-radius: var(--radius-sm); }
|
|
1605
1698
|
.main { margin-left: 0; padding: 16px; }
|
|
1606
1699
|
.stats-grid { grid-template-columns: repeat(2, 1fr); }
|
|
1607
1700
|
.insights-grid { grid-template-columns: 1fr; }
|
|
1701
|
+
.charts-grid { grid-template-columns: 1fr; }
|
|
1608
1702
|
.page-header { flex-direction: column; }
|
|
1609
1703
|
.theme-toggle { top: 8px; right: 8px; }
|
|
1610
1704
|
}
|
|
@@ -2050,21 +2144,33 @@ function printCliSummary(report) {
|
|
|
2050
2144
|
ln(centre(dim("Generated by ") + bold("ai-localize-core") + dim(" \xB7 deterministic, offline-capable i18n tooling")));
|
|
2051
2145
|
ln(hr("\u2550", "cyan"));
|
|
2052
2146
|
ln();
|
|
2053
|
-
|
|
2147
|
+
const nextSteps = [];
|
|
2148
|
+
if (hardcodedTexts > 0) {
|
|
2149
|
+
nextSteps.push(
|
|
2150
|
+
"Run " + c("brightCyan", "ai-localize scan") + dim(" then ") + c("brightCyan", "ai-localize extract") + dim(" to wrap hardcoded strings and generate locale keys.")
|
|
2151
|
+
);
|
|
2152
|
+
}
|
|
2153
|
+
if (missingTranslations > 0) {
|
|
2154
|
+
nextSteps.push(
|
|
2155
|
+
"Run " + c("brightCyan", "ai-localize extract") + dim(" to seed missing translations in target language files.")
|
|
2156
|
+
);
|
|
2157
|
+
}
|
|
2158
|
+
if (unusedKeys > 0) {
|
|
2159
|
+
nextSteps.push(
|
|
2160
|
+
"Run " + c("brightCyan", "ai-localize cleanup") + dim(" to remove unused keys from locale files.")
|
|
2161
|
+
);
|
|
2162
|
+
}
|
|
2163
|
+
if (assets.legacyCdnUrls > 0) {
|
|
2164
|
+
nextSteps.push(
|
|
2165
|
+
"Run " + c("brightCyan", "ai-localize replace-cdn") + dim(" to migrate legacy CDN URLs to CloudFront.")
|
|
2166
|
+
);
|
|
2167
|
+
}
|
|
2168
|
+
if (nextSteps.length > 0) {
|
|
2054
2169
|
ln(bold(" \u26A1 Recommended next steps:"));
|
|
2055
2170
|
ln();
|
|
2056
|
-
|
|
2057
|
-
ln("" + c("cyan",
|
|
2058
|
-
}
|
|
2059
|
-
if (missingTranslations > 0) {
|
|
2060
|
-
ln(" " + c("cyan", "2.") + " Run " + c("brightCyan", "ai-localize extract") + dim(" to seed target language files."));
|
|
2061
|
-
}
|
|
2062
|
-
if (unusedKeys > 0) {
|
|
2063
|
-
ln(" " + c("cyan", "3.") + " Run " + c("brightCyan", "ai-localize cleanup") + dim(" to remove unused keys."));
|
|
2064
|
-
}
|
|
2065
|
-
if (assets.legacyCdnUrls > 0) {
|
|
2066
|
-
ln(" " + c("cyan", "4.") + " Run " + c("brightCyan", "ai-localize replace-cdn") + dim(" to migrate legacy CDN URLs to CloudFront."));
|
|
2067
|
-
}
|
|
2171
|
+
nextSteps.forEach((step, i) => {
|
|
2172
|
+
ln(" " + c("cyan", String(i + 1) + ".") + " " + step);
|
|
2173
|
+
});
|
|
2068
2174
|
ln();
|
|
2069
2175
|
}
|
|
2070
2176
|
const output = out.join("\n");
|
package/dist/index.mjs
CHANGED
|
@@ -97,15 +97,16 @@ function computeInsights(report) {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
const totalKeys = new Set(details.detectedTexts.map((d) => d.suggestedKey)).size;
|
|
100
|
-
const
|
|
101
|
-
const
|
|
100
|
+
const distinctMissingKeys = new Set(details.missingKeys.map((mk) => mk.key)).size;
|
|
101
|
+
const coveredKeys = Math.max(0, totalKeys - distinctMissingKeys);
|
|
102
|
+
const coveragePct = totalKeys > 0 ? Math.round(coveredKeys / totalKeys * 100) : 100;
|
|
102
103
|
return { duplicates, inconsistencies, namespaceHints, coveragePct, totalKeys, duplicateKeyCount };
|
|
103
104
|
}
|
|
104
105
|
function buildNavItem(id, icon, label, count, alert = false) {
|
|
105
106
|
return `<a href="#${id}" class="nav-item${alert ? " nav-alert" : ""}" data-section="${id}">
|
|
106
107
|
<span class="nav-icon">${icon}</span>
|
|
107
108
|
<span class="nav-label">${esc(label)}</span>
|
|
108
|
-
|
|
109
|
+
<span class="nav-count">${count}</span>
|
|
109
110
|
</a>`;
|
|
110
111
|
}
|
|
111
112
|
function buildStatCard(value, label, hint, status, icon) {
|
|
@@ -118,15 +119,15 @@ function buildStatCard(value, label, hint, status, icon) {
|
|
|
118
119
|
}
|
|
119
120
|
function buildAccordion(id, title, subtitle, content, open = false, severity = "info") {
|
|
120
121
|
return `<div class="accordion${open ? " accordion-open" : ""}" id="acc-${id}">
|
|
121
|
-
<button class="accordion-trigger" aria-expanded="${open}" aria-controls="panel-${id}" onclick="toggleAccordion('${id}')">
|
|
122
|
-
<span class="accordion-icon severity-${severity}">●</span>
|
|
122
|
+
<button class="accordion-trigger" aria-expanded="${open ? "true" : "false"}" aria-controls="panel-${id}" onclick="toggleAccordion('${escJs(id)}')">
|
|
123
|
+
<span class="accordion-icon severity-${severity}" aria-hidden="true">●</span>
|
|
123
124
|
<span class="accordion-title">${title}</span>
|
|
124
|
-
<span class="accordion-subtitle">${subtitle}</span>
|
|
125
|
-
<span class="accordion-chevron">&#
|
|
125
|
+
<span class="accordion-subtitle">${subtitle}</span>
|
|
126
|
+
<span class="accordion-chevron" aria-hidden="true">▼</span>
|
|
126
127
|
</button>
|
|
127
|
-
<div class="accordion-panel" id="panel-${id}" role="region" ${open ? "" : "hidden"}>
|
|
128
|
+
<div class="accordion-panel" id="panel-${id}" role="region" aria-labelledby="acc-${id}"${open ? "" : " hidden"}>
|
|
128
129
|
<div class="accordion-body">${content}</div>
|
|
129
|
-
|
|
130
|
+
</div>
|
|
130
131
|
</div>`;
|
|
131
132
|
}
|
|
132
133
|
function buildCoverageDonut(pct) {
|
|
@@ -266,13 +267,17 @@ function buildHtml(report) {
|
|
|
266
267
|
e.filePath ? `<code class="path-code">${esc(e.filePath)}</code>` : "\u2014",
|
|
267
268
|
`<span class="detail-text">${esc(e.message)}</span>`
|
|
268
269
|
]);
|
|
269
|
-
const langChips = [...missingByLang.entries()].map(([lang, keys]) => `<
|
|
270
|
+
const langChips = [...missingByLang.entries()].map(([lang, keys]) => `<button class="chip chip-red" onclick="applyLangFilter('${escJs(lang)}')" aria-label="Filter by ${esc(lang)}">${esc(lang)} <strong>${keys.length}</strong></button>`).join(" ");
|
|
270
271
|
missingContent = `
|
|
271
272
|
<div class="insight-legend">
|
|
272
273
|
These locale keys exist in the <strong>default language</strong> but are <strong>absent</strong> in one or more target language files.
|
|
273
274
|
Ask your translators to fill in these entries. Running <code>ai-localize extract</code> seeds all target files with the source value.
|
|
274
275
|
</div>
|
|
275
|
-
<div class="chip-row">
|
|
276
|
+
<div class="chip-row">
|
|
277
|
+
<span class="chip-label">Filter by language:</span>
|
|
278
|
+
${langChips}
|
|
279
|
+
<button class="chip chip-clear" onclick="applyLangFilter('')" aria-label="Clear language filter">✕ Clear</button>
|
|
280
|
+
</div>
|
|
276
281
|
${buildSearchableTable("tbl-missing", [
|
|
277
282
|
{ key: "key", label: "Key" },
|
|
278
283
|
{ key: "language", label: "Language", width: "100px" },
|
|
@@ -439,7 +444,7 @@ ${nav}
|
|
|
439
444
|
<div class="page-header-left">
|
|
440
445
|
<h1 class="page-title">🌐 Localization Analytics Dashboard</h1>
|
|
441
446
|
<div class="page-meta">
|
|
442
|
-
<span class="meta-chip">${badge(framework, "blue")}</span>
|
|
447
|
+
<span class="meta-chip">${badge(framework, "blue")}</span>
|
|
443
448
|
<span class="meta-item">📅 ${scanDate}</span>
|
|
444
449
|
<span class="meta-item">⏱ ${duration}ms</span>
|
|
445
450
|
<span class="meta-item">📄 ${filesScanned} files</span>
|
|
@@ -454,11 +459,11 @@ ${nav}
|
|
|
454
459
|
<!-- Summary -->
|
|
455
460
|
<section id="overview" class="section">
|
|
456
461
|
<div class="section-title-row">
|
|
457
|
-
|
|
462
|
+
<h2 class="section-title">📊 Summary</h2>
|
|
458
463
|
</div>
|
|
459
464
|
${summaryCards}
|
|
460
465
|
${diffExplainer}
|
|
461
|
-
</section>
|
|
466
|
+
</section>
|
|
462
467
|
|
|
463
468
|
<!-- Charts -->
|
|
464
469
|
<section id="charts" class="section">
|
|
@@ -515,7 +520,7 @@ ${nav}
|
|
|
515
520
|
|
|
516
521
|
<!-- Assets -->
|
|
517
522
|
<section id="assets" class="section">
|
|
518
|
-
|
|
523
|
+
<div class="section-title-row">
|
|
519
524
|
<h2 class="section-title">📦 CDN Assets <span class="section-count count-neutral">${assets.totalAssets}</span></h2>
|
|
520
525
|
</div>
|
|
521
526
|
${buildAccordion(
|
|
@@ -526,7 +531,7 @@ ${nav}
|
|
|
526
531
|
false,
|
|
527
532
|
assets.legacyCdnUrls > 0 ? "warn" : "ok"
|
|
528
533
|
)}
|
|
529
|
-
</section>
|
|
534
|
+
</section>
|
|
530
535
|
|
|
531
536
|
<!-- AI Insights -->
|
|
532
537
|
<section id="insights" class="section">
|
|
@@ -541,6 +546,9 @@ ${nav}
|
|
|
541
546
|
|
|
542
547
|
<footer class="page-footer">
|
|
543
548
|
Generated by <strong>ai-localize-core</strong> — deterministic, offline-capable i18n tooling — ${scanDate}
|
|
549
|
+
<span class="footer-shortcuts" aria-label="Keyboard shortcuts">
|
|
550
|
+
<kbd>Esc</kbd> clear search • <kbd>Cmd+D</kbd> toggle theme • <kbd>Tab</kbd> navigate
|
|
551
|
+
</span>
|
|
544
552
|
</footer>
|
|
545
553
|
</main>
|
|
546
554
|
|
|
@@ -615,10 +623,14 @@ function JS(report, insights) {
|
|
|
615
623
|
// \u2500\u2500 Active nav \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
616
624
|
var sections = document.querySelectorAll('section[id]');
|
|
617
625
|
var navItems = document.querySelectorAll('.nav-item[data-section]');
|
|
626
|
+
// Use 120px offset to account for sticky table headers and give visual lead-time
|
|
627
|
+
var NAV_OFFSET = 120;
|
|
618
628
|
function onScroll() {
|
|
619
|
-
var scrollY = window.scrollY +
|
|
629
|
+
var scrollY = window.scrollY + NAV_OFFSET;
|
|
620
630
|
var current = '';
|
|
621
|
-
sections.forEach(function(s) {
|
|
631
|
+
sections.forEach(function(s) {
|
|
632
|
+
if (scrollY >= s.offsetTop) current = s.id;
|
|
633
|
+
});
|
|
622
634
|
navItems.forEach(function(a) {
|
|
623
635
|
a.classList.toggle('nav-active', a.getAttribute('data-section') === current);
|
|
624
636
|
});
|
|
@@ -630,10 +642,18 @@ var scrollY = window.scrollY + 80;
|
|
|
630
642
|
window.toggleAccordion = function(id) {
|
|
631
643
|
var acc = document.getElementById('acc-' + id);
|
|
632
644
|
var panel = document.getElementById('panel-' + id);
|
|
645
|
+
if (!acc || !panel) return;
|
|
633
646
|
var btn = acc.querySelector('.accordion-trigger');
|
|
634
|
-
var
|
|
635
|
-
|
|
636
|
-
|
|
647
|
+
var isOpen = acc.classList.contains('accordion-open');
|
|
648
|
+
if (isOpen) {
|
|
649
|
+
acc.classList.remove('accordion-open');
|
|
650
|
+
btn.setAttribute('aria-expanded', 'false');
|
|
651
|
+
panel.setAttribute('hidden', '');
|
|
652
|
+
} else {
|
|
653
|
+
acc.classList.add('accordion-open');
|
|
654
|
+
btn.setAttribute('aria-expanded', 'true');
|
|
655
|
+
panel.removeAttribute('hidden');
|
|
656
|
+
}
|
|
637
657
|
};
|
|
638
658
|
window.expandAll = function() {
|
|
639
659
|
document.querySelectorAll('.accordion').forEach(function(acc) {
|
|
@@ -641,7 +661,7 @@ var scrollY = window.scrollY + 80;
|
|
|
641
661
|
var panel = document.getElementById('panel-' + id);
|
|
642
662
|
var btn = acc.querySelector('.accordion-trigger');
|
|
643
663
|
acc.classList.add('accordion-open');
|
|
644
|
-
btn.setAttribute('aria-expanded', 'true');
|
|
664
|
+
if (btn) btn.setAttribute('aria-expanded', 'true');
|
|
645
665
|
if (panel) panel.removeAttribute('hidden');
|
|
646
666
|
});
|
|
647
667
|
};
|
|
@@ -649,30 +669,40 @@ var scrollY = window.scrollY + 80;
|
|
|
649
669
|
document.querySelectorAll('.accordion').forEach(function(acc) {
|
|
650
670
|
var id = acc.id.replace('acc-', '');
|
|
651
671
|
var panel = document.getElementById('panel-' + id);
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
btn.setAttribute('aria-expanded', 'false');
|
|
672
|
+
var btn = acc.querySelector('.accordion-trigger');
|
|
673
|
+
acc.classList.remove('accordion-open');
|
|
674
|
+
if (btn) btn.setAttribute('aria-expanded', 'false');
|
|
655
675
|
if (panel) panel.setAttribute('hidden', '');
|
|
656
676
|
});
|
|
657
677
|
};
|
|
658
678
|
|
|
659
|
-
// \u2500\u2500 Table: filter \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
679
|
+
// \u2500\u2500 Table: filter \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
680
|
+
// Mark rows with data-filtered attribute instead of display:none so that
|
|
681
|
+
// renderPagination can independently control visibility for pagination.
|
|
660
682
|
window.filterTable = function(tableId, query) {
|
|
661
683
|
var tbl = document.getElementById(tableId);
|
|
662
684
|
if (!tbl) return;
|
|
663
|
-
var q = query.toLowerCase();
|
|
685
|
+
var q = (query || '').toLowerCase().trim();
|
|
664
686
|
var rows = tbl.querySelectorAll('tbody tr');
|
|
665
|
-
var shown = 0;
|
|
666
687
|
rows.forEach(function(row) {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
688
|
+
if (q === '' || row.textContent.toLowerCase().indexOf(q) !== -1) {
|
|
689
|
+
row.removeAttribute('data-filtered');
|
|
690
|
+
} else {
|
|
691
|
+
row.setAttribute('data-filtered', '1');
|
|
692
|
+
}
|
|
670
693
|
});
|
|
671
|
-
|
|
672
|
-
|
|
694
|
+
// Reset to page 1 whenever filter changes
|
|
695
|
+
tbl.setAttribute('data-page', '1');
|
|
673
696
|
renderPagination(tableId);
|
|
674
697
|
};
|
|
675
698
|
|
|
699
|
+
// \u2500\u2500 Language chip filter (syncs search-box input + resets page) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
700
|
+
window.applyLangFilter = function(lang) {
|
|
701
|
+
var inp = document.querySelector('#tbl-missing-wrapper .table-search');
|
|
702
|
+
if (inp) inp.value = lang;
|
|
703
|
+
window.filterTable('tbl-missing', lang);
|
|
704
|
+
};
|
|
705
|
+
|
|
676
706
|
// \u2500\u2500 Table: sort \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
677
707
|
var sortState = {};
|
|
678
708
|
window.sortTable = function(tableId, col) {
|
|
@@ -683,39 +713,52 @@ var scrollY = window.scrollY + 80;
|
|
|
683
713
|
var colIndex = -1;
|
|
684
714
|
tbl.querySelectorAll('thead th').forEach(function(th, i) {
|
|
685
715
|
if (th.getAttribute('data-col') === col) colIndex = i;
|
|
686
|
-
|
|
716
|
+
var icon = th.querySelector('.sort-icon');
|
|
717
|
+
if (icon) icon.textContent = '\\u21C5';
|
|
687
718
|
});
|
|
688
|
-
|
|
719
|
+
if (colIndex === -1) return;
|
|
689
720
|
var th = tbl.querySelector('thead th[data-col="' + col + '"]');
|
|
690
|
-
if (th)
|
|
721
|
+
if (th) {
|
|
722
|
+
var icon = th.querySelector('.sort-icon');
|
|
723
|
+
if (icon) icon.textContent = dir === 'asc' ? '\\u2191' : '\\u2193';
|
|
724
|
+
}
|
|
691
725
|
var tbody = tbl.querySelector('tbody');
|
|
692
726
|
var rows = Array.from(tbody.querySelectorAll('tr'));
|
|
693
727
|
rows.sort(function(a, b) {
|
|
694
728
|
var av = (a.cells[colIndex] || {}).textContent || '';
|
|
695
|
-
|
|
729
|
+
var bv = (b.cells[colIndex] || {}).textContent || '';
|
|
696
730
|
var an = parseFloat(av), bn = parseFloat(bv);
|
|
697
731
|
if (!isNaN(an) && !isNaN(bn)) return dir === 'asc' ? an - bn : bn - an;
|
|
698
732
|
return dir === 'asc' ? av.localeCompare(bv) : bv.localeCompare(av);
|
|
699
733
|
});
|
|
700
734
|
rows.forEach(function(r) { tbody.appendChild(r); });
|
|
701
|
-
renderPagination(tableId);
|
|
735
|
+
renderPagination(tableId);
|
|
702
736
|
};
|
|
703
737
|
|
|
704
738
|
// \u2500\u2500 Table: pagination \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
739
|
+
// Reads data-filtered attribute set by filterTable; does not conflict with
|
|
740
|
+
// filter visibility since rows are never hidden by this function until
|
|
741
|
+
// it has determined which rows belong to the current page.
|
|
705
742
|
function renderPagination(tableId) {
|
|
706
743
|
var tbl = document.getElementById(tableId);
|
|
707
744
|
var pag = document.getElementById(tableId + '-pagination');
|
|
708
745
|
var meta = document.getElementById(tableId + '-meta');
|
|
709
746
|
if (!tbl || !pag) return;
|
|
710
747
|
var pageSize = parseInt(tbl.getAttribute('data-page-size') || '50', 10);
|
|
711
|
-
var
|
|
712
|
-
|
|
713
|
-
var
|
|
748
|
+
var allRows = Array.from(tbl.querySelectorAll('tbody tr'));
|
|
749
|
+
// Rows that passed the current filter
|
|
750
|
+
var visibleRows = allRows.filter(function(r) { return !r.hasAttribute('data-filtered'); });
|
|
751
|
+
var total = visibleRows.length;
|
|
752
|
+
var pages = Math.max(1, Math.ceil(total / pageSize));
|
|
714
753
|
var page = parseInt(tbl.getAttribute('data-page') || '1', 10);
|
|
715
754
|
if (page > pages) page = 1;
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
755
|
+
if (page < 1) page = 1;
|
|
756
|
+
tbl.setAttribute('data-page', String(page));
|
|
757
|
+
// First hide all rows, then show only the current page slice of visible rows
|
|
758
|
+
allRows.forEach(function(r) { r.style.display = 'none'; });
|
|
759
|
+
var startIdx = (page - 1) * pageSize;
|
|
760
|
+
visibleRows.forEach(function(r, i) {
|
|
761
|
+
if (i >= startIdx && i < startIdx + pageSize) r.style.display = '';
|
|
719
762
|
});
|
|
720
763
|
if (meta) meta.textContent = total + ' row' + (total !== 1 ? 's' : '');
|
|
721
764
|
pag.innerHTML = '';
|
|
@@ -725,13 +768,13 @@ renderPagination(tableId);
|
|
|
725
768
|
b.textContent = label;
|
|
726
769
|
b.className = 'pag-btn' + (active ? ' pag-active' : '') + (disabled ? ' pag-disabled' : '');
|
|
727
770
|
b.disabled = disabled;
|
|
728
|
-
b.onclick = function() { tbl.setAttribute('data-page', p); renderPagination(tableId); };
|
|
771
|
+
b.onclick = function() { tbl.setAttribute('data-page', String(p)); renderPagination(tableId); };
|
|
729
772
|
return b;
|
|
730
773
|
}
|
|
731
|
-
|
|
774
|
+
pag.appendChild(btn('\\u00AB', 1, false, page === 1));
|
|
732
775
|
pag.appendChild(btn('\\u2039', page - 1, false, page === 1));
|
|
733
|
-
var
|
|
734
|
-
for (var i =
|
|
776
|
+
var winStart = Math.max(1, page - 2), winEnd = Math.min(pages, page + 2);
|
|
777
|
+
for (var i = winStart; i <= winEnd; i++) pag.appendChild(btn(i, i, i === page, false));
|
|
735
778
|
pag.appendChild(btn('\\u203A', page + 1, false, page === pages));
|
|
736
779
|
pag.appendChild(btn('\\u00BB', pages, false, page === pages));
|
|
737
780
|
var info = document.createElement('span');
|
|
@@ -839,21 +882,33 @@ renderPagination(tableId);
|
|
|
839
882
|
|
|
840
883
|
function downloadFile(filename, content, mimeType) {
|
|
841
884
|
var a = document.createElement('a');
|
|
842
|
-
|
|
885
|
+
a.href = URL.createObjectURL(new Blob([content], { type: mimeType }));
|
|
843
886
|
a.download = filename;
|
|
844
887
|
a.click();
|
|
845
888
|
setTimeout(function() { URL.revokeObjectURL(a.href); }, 1000);
|
|
846
889
|
}
|
|
847
890
|
|
|
848
891
|
// \u2500\u2500 Keyboard navigation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
892
|
+
// Escape: clear all table search inputs and reset filters
|
|
893
|
+
// Cmd/Ctrl+D: toggle dark/light theme
|
|
849
894
|
document.addEventListener('keydown', function(e) {
|
|
850
895
|
if (e.key === 'Escape') {
|
|
851
|
-
document.querySelectorAll('.table-search').forEach(function(inp) {
|
|
896
|
+
document.querySelectorAll('.table-search').forEach(function(inp) {
|
|
897
|
+
var wrapper = inp.closest('.table-wrapper');
|
|
898
|
+
if (!wrapper) return;
|
|
899
|
+
var tbl = wrapper.querySelector('table');
|
|
900
|
+
if (!tbl) return;
|
|
901
|
+
inp.value = '';
|
|
902
|
+
window.filterTable(tbl.id, '');
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'd') {
|
|
906
|
+
e.preventDefault();
|
|
907
|
+
window.toggleTheme();
|
|
852
908
|
}
|
|
853
|
-
if ((e.metaKey || e.ctrlKey) && e.key === 'd') { e.preventDefault(); window.toggleTheme(); }
|
|
854
909
|
});
|
|
855
910
|
|
|
856
|
-
// \u2500\u2500 Smooth scroll for nav \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
911
|
+
// \u2500\u2500 Smooth scroll for nav \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
857
912
|
document.querySelectorAll('a.nav-item[href^="#"]').forEach(function(a) {
|
|
858
913
|
a.addEventListener('click', function(e) {
|
|
859
914
|
e.preventDefault();
|
|
@@ -1200,10 +1255,12 @@ padding: 6px 16px 3px;
|
|
|
1200
1255
|
}
|
|
1201
1256
|
.accordion-subtitle { font-size: 12px; color: var(--text-muted); margin-right: 12px; }
|
|
1202
1257
|
.accordion-chevron {
|
|
1203
|
-
font-size:
|
|
1258
|
+
font-size: 14px;
|
|
1204
1259
|
color: var(--text-muted);
|
|
1205
1260
|
transition: transform var(--transition);
|
|
1261
|
+
display: inline-block;
|
|
1206
1262
|
}
|
|
1263
|
+
/* \u25BE (▼) points down by default; rotates to \u25B4 when open */
|
|
1207
1264
|
.accordion-open .accordion-chevron { transform: rotate(180deg); }
|
|
1208
1265
|
.accordion-panel { border-top: 1px solid var(--border); }
|
|
1209
1266
|
.accordion-body { padding: 18px; }
|
|
@@ -1273,15 +1330,20 @@ border-radius: var(--radius);
|
|
|
1273
1330
|
|
|
1274
1331
|
/* \u2500\u2500 Chip filter row \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
1275
1332
|
.chip-row { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; align-items: center; font-size: 12px; color: var(--text-muted); }
|
|
1333
|
+
.chip-label { font-size: 12px; color: var(--text-muted); white-space: nowrap; }
|
|
1276
1334
|
.chip {
|
|
1277
1335
|
display: inline-flex; align-items: center; gap: 4px;
|
|
1278
1336
|
padding: 3px 10px; border-radius: 14px;
|
|
1279
|
-
font-size: 12px; font-weight: 600;
|
|
1337
|
+
font-size: 12px; font-weight: 600;
|
|
1280
1338
|
cursor: pointer; border: 1px solid transparent;
|
|
1281
|
-
transition: box-shadow var(--transition);
|
|
1339
|
+
transition: box-shadow var(--transition), background var(--transition);
|
|
1340
|
+
font-family: var(--font);
|
|
1282
1341
|
}
|
|
1283
1342
|
.chip:hover { box-shadow: 0 0 0 2px var(--accent); }
|
|
1343
|
+
.chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
1284
1344
|
.chip-red { background: #fee2e2; color: #991b1b; }
|
|
1345
|
+
.chip-clear { background: var(--bg-elevated); color: var(--text-muted); border: 1px solid var(--border); }
|
|
1346
|
+
.chip-clear:hover { background: var(--border); box-shadow: none; }
|
|
1285
1347
|
[data-theme="dark"] .chip-red { background: #450a0a; color: #fca5a5; }
|
|
1286
1348
|
|
|
1287
1349
|
/* \u2500\u2500 Tables \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
@@ -1548,25 +1610,57 @@ font-size: 12px;
|
|
|
1548
1610
|
font-size: 12px;
|
|
1549
1611
|
color: var(--text-subtle);
|
|
1550
1612
|
text-align: center;
|
|
1613
|
+
display: flex;
|
|
1614
|
+
flex-direction: column;
|
|
1615
|
+
align-items: center;
|
|
1616
|
+
gap: 8px;
|
|
1617
|
+
}
|
|
1618
|
+
.footer-shortcuts { font-size: 11px; color: var(--text-subtle); }
|
|
1619
|
+
kbd {
|
|
1620
|
+
display: inline-block;
|
|
1621
|
+
padding: 1px 6px;
|
|
1622
|
+
font-size: 10px;
|
|
1623
|
+
font-family: var(--font-mono);
|
|
1624
|
+
background: var(--bg-elevated);
|
|
1625
|
+
border: 1px solid var(--border);
|
|
1626
|
+
border-bottom-width: 2px;
|
|
1627
|
+
border-radius: 4px;
|
|
1628
|
+
color: var(--text-muted);
|
|
1629
|
+
white-space: nowrap;
|
|
1551
1630
|
}
|
|
1552
1631
|
|
|
1553
1632
|
/* \u2500\u2500 Print \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
1554
1633
|
@media print {
|
|
1555
1634
|
.sidebar, .theme-toggle, .table-controls, .table-pagination, .page-header-right { display: none !important; }
|
|
1556
1635
|
.main { margin-left: 0 !important; padding: 16px; }
|
|
1557
|
-
|
|
1558
|
-
.accordion-panel
|
|
1636
|
+
/* Force accordion panels visible \u2014 the hidden attribute needs !important override */
|
|
1637
|
+
.accordion-panel { display: block !important; visibility: visible !important; }
|
|
1638
|
+
.accordion-panel[hidden] { display: block !important; visibility: visible !important; }
|
|
1559
1639
|
.stat-card, .chart-card, .accordion, .table-wrapper { break-inside: avoid; }
|
|
1560
1640
|
body { background: white; color: black; }
|
|
1641
|
+
a { color: inherit; text-decoration: none; }
|
|
1561
1642
|
}
|
|
1562
1643
|
|
|
1563
|
-
/* \u2500\u2500 Responsive \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1644
|
+
/* \u2500\u2500 Responsive / Mobile \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
1564
1645
|
@media (max-width: 768px) {
|
|
1565
1646
|
body { flex-direction: column; }
|
|
1566
|
-
|
|
1647
|
+
/* On mobile the sidebar becomes an in-flow top-bar, not fixed/absolute */
|
|
1648
|
+
.sidebar {
|
|
1649
|
+
position: relative;
|
|
1650
|
+
width: 100%;
|
|
1651
|
+
height: auto;
|
|
1652
|
+
overflow: visible;
|
|
1653
|
+
border-right: none;
|
|
1654
|
+
border-bottom: 1px solid var(--border);
|
|
1655
|
+
flex-direction: column;
|
|
1656
|
+
}
|
|
1657
|
+
.nav-group { display: flex; flex-wrap: wrap; padding: 4px 8px; border-bottom: none; }
|
|
1658
|
+
.nav-group-label { width: 100%; padding: 4px 8px 2px; }
|
|
1659
|
+
.nav-item { padding: 6px 10px; border-radius: var(--radius-sm); }
|
|
1567
1660
|
.main { margin-left: 0; padding: 16px; }
|
|
1568
1661
|
.stats-grid { grid-template-columns: repeat(2, 1fr); }
|
|
1569
1662
|
.insights-grid { grid-template-columns: 1fr; }
|
|
1663
|
+
.charts-grid { grid-template-columns: 1fr; }
|
|
1570
1664
|
.page-header { flex-direction: column; }
|
|
1571
1665
|
.theme-toggle { top: 8px; right: 8px; }
|
|
1572
1666
|
}
|
|
@@ -2012,21 +2106,33 @@ function printCliSummary(report) {
|
|
|
2012
2106
|
ln(centre(dim("Generated by ") + bold("ai-localize-core") + dim(" \xB7 deterministic, offline-capable i18n tooling")));
|
|
2013
2107
|
ln(hr("\u2550", "cyan"));
|
|
2014
2108
|
ln();
|
|
2015
|
-
|
|
2109
|
+
const nextSteps = [];
|
|
2110
|
+
if (hardcodedTexts > 0) {
|
|
2111
|
+
nextSteps.push(
|
|
2112
|
+
"Run " + c("brightCyan", "ai-localize scan") + dim(" then ") + c("brightCyan", "ai-localize extract") + dim(" to wrap hardcoded strings and generate locale keys.")
|
|
2113
|
+
);
|
|
2114
|
+
}
|
|
2115
|
+
if (missingTranslations > 0) {
|
|
2116
|
+
nextSteps.push(
|
|
2117
|
+
"Run " + c("brightCyan", "ai-localize extract") + dim(" to seed missing translations in target language files.")
|
|
2118
|
+
);
|
|
2119
|
+
}
|
|
2120
|
+
if (unusedKeys > 0) {
|
|
2121
|
+
nextSteps.push(
|
|
2122
|
+
"Run " + c("brightCyan", "ai-localize cleanup") + dim(" to remove unused keys from locale files.")
|
|
2123
|
+
);
|
|
2124
|
+
}
|
|
2125
|
+
if (assets.legacyCdnUrls > 0) {
|
|
2126
|
+
nextSteps.push(
|
|
2127
|
+
"Run " + c("brightCyan", "ai-localize replace-cdn") + dim(" to migrate legacy CDN URLs to CloudFront.")
|
|
2128
|
+
);
|
|
2129
|
+
}
|
|
2130
|
+
if (nextSteps.length > 0) {
|
|
2016
2131
|
ln(bold(" \u26A1 Recommended next steps:"));
|
|
2017
2132
|
ln();
|
|
2018
|
-
|
|
2019
|
-
ln("" + c("cyan",
|
|
2020
|
-
}
|
|
2021
|
-
if (missingTranslations > 0) {
|
|
2022
|
-
ln(" " + c("cyan", "2.") + " Run " + c("brightCyan", "ai-localize extract") + dim(" to seed target language files."));
|
|
2023
|
-
}
|
|
2024
|
-
if (unusedKeys > 0) {
|
|
2025
|
-
ln(" " + c("cyan", "3.") + " Run " + c("brightCyan", "ai-localize cleanup") + dim(" to remove unused keys."));
|
|
2026
|
-
}
|
|
2027
|
-
if (assets.legacyCdnUrls > 0) {
|
|
2028
|
-
ln(" " + c("cyan", "4.") + " Run " + c("brightCyan", "ai-localize replace-cdn") + dim(" to migrate legacy CDN URLs to CloudFront."));
|
|
2029
|
-
}
|
|
2133
|
+
nextSteps.forEach((step, i) => {
|
|
2134
|
+
ln(" " + c("cyan", String(i + 1) + ".") + " " + step);
|
|
2135
|
+
});
|
|
2030
2136
|
ln();
|
|
2031
2137
|
}
|
|
2032
2138
|
const output = out.join("\n");
|
package/package.json
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-localize-reporting",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.7",
|
|
4
4
|
"description": "Localization scan reporting: JSON, HTML analytics dashboard and CLI summary",
|
|
5
|
+
"author": "ai-localize-core contributors",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/ai-localize/ai-localize-core.git",
|
|
10
|
+
"directory": "packages/reporting"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/ai-localize/ai-localize-core/tree/main/packages/reporting#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/ai-localize/ai-localize-core/issues"
|
|
15
|
+
},
|
|
5
16
|
"main": "./dist/index.js",
|
|
6
17
|
"module": "./dist/index.mjs",
|
|
7
18
|
"types": "./dist/index.d.ts",
|
|
19
|
+
"sideEffects": false,
|
|
8
20
|
"files": [
|
|
9
21
|
"dist",
|
|
10
22
|
"README.md",
|
|
@@ -33,14 +45,13 @@
|
|
|
33
45
|
"node": ">=18.0.0"
|
|
34
46
|
},
|
|
35
47
|
"dependencies": {
|
|
36
|
-
"ai-localize-shared": "2.0.
|
|
48
|
+
"ai-localize-shared": "2.0.7"
|
|
37
49
|
},
|
|
38
50
|
"devDependencies": {
|
|
39
51
|
"tsup": "^8.0.1",
|
|
40
52
|
"typescript": "^5.3.3",
|
|
41
53
|
"vitest": "^1.2.1"
|
|
42
54
|
},
|
|
43
|
-
"license": "MIT",
|
|
44
55
|
"publishConfig": {
|
|
45
56
|
"access": "public"
|
|
46
57
|
},
|