docs-i18n 0.1.0 → 0.2.1
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/dist/{assemble-IOHQYYHI.js → assemble-ZHDLGVTL.js} +3 -4
- package/dist/chunk-I74LIORX.js +11211 -0
- package/dist/{chunk-QSVWLTGQ.js → chunk-OSMPWXSQ.js} +1 -1
- package/dist/{chunk-AKLW2MUS.js → chunk-PHDMD6EM.js} +29 -7
- package/dist/cli.js +6 -7
- package/dist/{rescan-VB2PILB2.js → rescan-OJTVWDAP.js} +2 -3
- package/dist/server-HNVJP43X.js +2742 -0
- package/dist/{status-EWQEACVF.js → status-ZG7F3FRT.js} +1 -2
- package/dist/translate-2PCYIWIG.js +14531 -0
- package/package.json +3 -2
- package/src/admin/index.html +13 -0
- package/src/admin/server/index.ts +88 -0
- package/src/admin/server/routes/jobs.ts +113 -0
- package/src/admin/server/routes/models.ts +87 -0
- package/src/admin/server/routes/status.ts +57 -0
- package/src/admin/server/services/job-manager.ts +184 -0
- package/src/admin/server/services/status.ts +183 -0
- package/src/admin/ui/App.tsx +326 -0
- package/src/admin/ui/components/FileList.tsx +438 -0
- package/src/admin/ui/components/JobDialog.tsx +360 -0
- package/src/admin/ui/components/JobPanel.tsx +134 -0
- package/src/admin/ui/components/LangGrid.tsx +54 -0
- package/src/admin/ui/components/Preview.tsx +369 -0
- package/src/admin/ui/components/ProgressBar.tsx +21 -0
- package/src/admin/ui/lib/api.ts +154 -0
- package/src/admin/ui/lib/flags.ts +30 -0
- package/src/admin/ui/main.tsx +19 -0
- package/src/admin/ui/styles.css +1096 -0
- package/src/admin/vite.config.ts +7 -0
- package/dist/build-4EQEL4NI.js +0 -12
- package/dist/build2-3W5WMFHZ.js +0 -4901
- package/dist/chunk-3YNFMSJH.js +0 -30
- package/dist/chunk-55MBYBVK.js +0 -368
- package/dist/chunk-FYDB7MZX.js +0 -38944
- package/dist/chunk-O35QHRY6.js +0 -6
- package/dist/chunk-PTIH4GGE.js +0 -44
- package/dist/chunk-SUIDX6IZ.js +0 -122
- package/dist/chunk-VKKNQBDN.js +0 -6487
- package/dist/dist-6C32URTL.js +0 -19
- package/dist/dist-HOWMMQFV.js +0 -6677
- package/dist/false-JGP4AGWN.js +0 -7
- package/dist/main-QVE5TVA3.js +0 -2505
- package/dist/node-4GLCLDJ6.js +0 -875
- package/dist/node-NUDVMOF2.js +0 -129
- package/dist/postcss-3SK7VUC2.js +0 -5886
- package/dist/postcss-import-JD46KA2Z.js +0 -458
- package/dist/prompt-BYQIwEjg-TG7DLENB.js +0 -915
- package/dist/server-ER56DGPR.js +0 -548
- package/dist/translate-F3AQFN6X.js +0 -707
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import { useMemo, useState } from 'react';
|
|
2
|
+
import type { FileCoverage } from '../lib/api';
|
|
3
|
+
import { getSection, statusIcon } from '../lib/flags';
|
|
4
|
+
|
|
5
|
+
export type StatusFilter = 'all' | 'complete' | 'partial' | 'missing';
|
|
6
|
+
export type SectionFilter = 'all' | 'docs' | 'blog' | 'learn';
|
|
7
|
+
type ViewMode = 'list' | 'tree';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
files: FileCoverage[];
|
|
11
|
+
lang: string;
|
|
12
|
+
activeFile: string | null;
|
|
13
|
+
selected: Set<string>;
|
|
14
|
+
statusFilter: StatusFilter;
|
|
15
|
+
sectionFilter: SectionFilter;
|
|
16
|
+
onStatusFilter: (f: StatusFilter) => void;
|
|
17
|
+
onSectionFilter: (f: SectionFilter) => void;
|
|
18
|
+
onSelect: (file: string) => void;
|
|
19
|
+
onToggle: (file: string) => void;
|
|
20
|
+
onSelectAll: (files: string[]) => void;
|
|
21
|
+
onClear: () => void;
|
|
22
|
+
onTranslateSelected: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function filePct(f: FileCoverage) {
|
|
26
|
+
return f.total > 0 ? (f.translated / f.total) * 100 : 100;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function fileStatus(f: FileCoverage): 'complete' | 'partial' | 'missing' {
|
|
30
|
+
const pct = filePct(f);
|
|
31
|
+
if (pct >= 100) return 'complete';
|
|
32
|
+
if (pct > 0) return 'partial';
|
|
33
|
+
return 'missing';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface TreeNode {
|
|
37
|
+
name: string;
|
|
38
|
+
path: string;
|
|
39
|
+
children: Map<string, TreeNode>;
|
|
40
|
+
file?: FileCoverage & { pct: number; status: string; section: string };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function buildTree(
|
|
44
|
+
files: (FileCoverage & { pct: number; status: string; section: string })[],
|
|
45
|
+
): TreeNode {
|
|
46
|
+
const root: TreeNode = { name: '', path: '', children: new Map() };
|
|
47
|
+
for (const f of files) {
|
|
48
|
+
const parts = f.file.split('/');
|
|
49
|
+
let node = root;
|
|
50
|
+
for (let i = 0; i < parts.length; i++) {
|
|
51
|
+
const part = parts[i];
|
|
52
|
+
if (!node.children.has(part)) {
|
|
53
|
+
node.children.set(part, {
|
|
54
|
+
name: part,
|
|
55
|
+
path: parts.slice(0, i + 1).join('/'),
|
|
56
|
+
children: new Map(),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
// biome-ignore lint/style/noNonNullAssertion: just created above
|
|
60
|
+
node = node.children.get(part)!;
|
|
61
|
+
}
|
|
62
|
+
node.file = f;
|
|
63
|
+
}
|
|
64
|
+
return root;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function dirPct(node: TreeNode): { translated: number; total: number } {
|
|
68
|
+
let translated = 0;
|
|
69
|
+
let total = 0;
|
|
70
|
+
if (node.file) {
|
|
71
|
+
translated += node.file.translated;
|
|
72
|
+
total += node.file.total;
|
|
73
|
+
}
|
|
74
|
+
for (const child of node.children.values()) {
|
|
75
|
+
const sub = dirPct(child);
|
|
76
|
+
translated += sub.translated;
|
|
77
|
+
total += sub.total;
|
|
78
|
+
}
|
|
79
|
+
return { translated, total };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function TreeRow({
|
|
83
|
+
node,
|
|
84
|
+
depth,
|
|
85
|
+
activeFile,
|
|
86
|
+
selected,
|
|
87
|
+
isEn,
|
|
88
|
+
collapsed,
|
|
89
|
+
onToggleCollapse,
|
|
90
|
+
onSelect,
|
|
91
|
+
onToggle,
|
|
92
|
+
}: {
|
|
93
|
+
node: TreeNode;
|
|
94
|
+
depth: number;
|
|
95
|
+
activeFile: string | null;
|
|
96
|
+
selected: Set<string>;
|
|
97
|
+
isEn: boolean;
|
|
98
|
+
collapsed: Set<string>;
|
|
99
|
+
onToggleCollapse: (path: string) => void;
|
|
100
|
+
onSelect: (file: string) => void;
|
|
101
|
+
onToggle: (file: string) => void;
|
|
102
|
+
}) {
|
|
103
|
+
const isDir = node.children.size > 0 && !node.file;
|
|
104
|
+
const isCollapsed = collapsed.has(node.path);
|
|
105
|
+
const children = [...node.children.values()].sort((a, b) => {
|
|
106
|
+
// Dirs first, then files
|
|
107
|
+
const aDir = a.children.size > 0 && !a.file;
|
|
108
|
+
const bDir = b.children.size > 0 && !b.file;
|
|
109
|
+
if (aDir !== bDir) return aDir ? -1 : 1;
|
|
110
|
+
return a.name.localeCompare(b.name);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (isDir) {
|
|
114
|
+
const { translated, total } = dirPct(node);
|
|
115
|
+
const pct = total > 0 ? (translated / total) * 100 : 100;
|
|
116
|
+
const color =
|
|
117
|
+
pct >= 100 ? 'var(--green)' : pct > 0 ? 'var(--yellow)' : 'var(--red)';
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<>
|
|
121
|
+
<div
|
|
122
|
+
className="file-row tree-dir"
|
|
123
|
+
style={{ paddingLeft: `${depth + 0.25}rem` }}
|
|
124
|
+
onClick={() => onToggleCollapse(node.path)}
|
|
125
|
+
>
|
|
126
|
+
<span className="tree-arrow">{isCollapsed ? '▶' : '▼'}</span>
|
|
127
|
+
<span className="icon">📁</span>
|
|
128
|
+
<span className="path" title={node.path}>
|
|
129
|
+
{node.name}/
|
|
130
|
+
</span>
|
|
131
|
+
<span className="pct">{pct.toFixed(0)}%</span>
|
|
132
|
+
<div className="mini-bar">
|
|
133
|
+
<div
|
|
134
|
+
className="mini-fill"
|
|
135
|
+
style={{ width: `${pct}%`, background: color }}
|
|
136
|
+
/>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
{!isCollapsed &&
|
|
140
|
+
children.map((child) => (
|
|
141
|
+
<TreeRow
|
|
142
|
+
key={child.path}
|
|
143
|
+
node={child}
|
|
144
|
+
depth={depth + 1}
|
|
145
|
+
activeFile={activeFile}
|
|
146
|
+
selected={selected}
|
|
147
|
+
isEn={isEn}
|
|
148
|
+
collapsed={collapsed}
|
|
149
|
+
onToggleCollapse={onToggleCollapse}
|
|
150
|
+
onSelect={onSelect}
|
|
151
|
+
onToggle={onToggle}
|
|
152
|
+
/>
|
|
153
|
+
))}
|
|
154
|
+
</>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// File node (leaf or file with children like index.mdx in a dir)
|
|
159
|
+
const f = node.file;
|
|
160
|
+
if (!f) return null;
|
|
161
|
+
|
|
162
|
+
const _missing = f.total - f.translated;
|
|
163
|
+
const color =
|
|
164
|
+
f.status === 'complete'
|
|
165
|
+
? 'var(--green)'
|
|
166
|
+
: f.status === 'partial'
|
|
167
|
+
? 'var(--yellow)'
|
|
168
|
+
: 'var(--red)';
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<>
|
|
172
|
+
<div
|
|
173
|
+
className={`file-row${activeFile === f.file ? ' active' : ''}`}
|
|
174
|
+
style={{ paddingLeft: `${depth + 0.25}rem` }}
|
|
175
|
+
onClick={() => onSelect(f.file)}
|
|
176
|
+
>
|
|
177
|
+
{!isEn && (
|
|
178
|
+
<input
|
|
179
|
+
type="checkbox"
|
|
180
|
+
checked={selected.has(f.file)}
|
|
181
|
+
onChange={(e) => {
|
|
182
|
+
e.stopPropagation();
|
|
183
|
+
onToggle(f.file);
|
|
184
|
+
}}
|
|
185
|
+
onClick={(e) => e.stopPropagation()}
|
|
186
|
+
/>
|
|
187
|
+
)}
|
|
188
|
+
<span className="icon">{statusIcon(f.pct)}</span>
|
|
189
|
+
<span className="path" title={f.file}>
|
|
190
|
+
{node.name}
|
|
191
|
+
</span>
|
|
192
|
+
<span className="pct">{f.pct.toFixed(0)}%</span>
|
|
193
|
+
<div className="mini-bar">
|
|
194
|
+
<div
|
|
195
|
+
className="mini-fill"
|
|
196
|
+
style={{ width: `${f.pct}%`, background: color }}
|
|
197
|
+
/>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
{/* Render any children (e.g., file is also a dir parent) */}
|
|
201
|
+
{!isCollapsed &&
|
|
202
|
+
children.map((child) => (
|
|
203
|
+
<TreeRow
|
|
204
|
+
key={child.path}
|
|
205
|
+
node={child}
|
|
206
|
+
depth={depth + 1}
|
|
207
|
+
activeFile={activeFile}
|
|
208
|
+
selected={selected}
|
|
209
|
+
isEn={isEn}
|
|
210
|
+
collapsed={collapsed}
|
|
211
|
+
onToggleCollapse={onToggleCollapse}
|
|
212
|
+
onSelect={onSelect}
|
|
213
|
+
onToggle={onToggle}
|
|
214
|
+
/>
|
|
215
|
+
))}
|
|
216
|
+
</>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function FileList({
|
|
221
|
+
files,
|
|
222
|
+
lang,
|
|
223
|
+
activeFile,
|
|
224
|
+
selected,
|
|
225
|
+
statusFilter,
|
|
226
|
+
sectionFilter,
|
|
227
|
+
onStatusFilter,
|
|
228
|
+
onSectionFilter,
|
|
229
|
+
onSelect,
|
|
230
|
+
onToggle,
|
|
231
|
+
onSelectAll,
|
|
232
|
+
onClear,
|
|
233
|
+
onTranslateSelected,
|
|
234
|
+
}: Props) {
|
|
235
|
+
const [viewMode, setViewMode] = useState<ViewMode>('tree');
|
|
236
|
+
const [collapsed, setCollapsed] = useState<Set<string>>(new Set());
|
|
237
|
+
const [search, setSearch] = useState('');
|
|
238
|
+
|
|
239
|
+
const filtered = useMemo(() => {
|
|
240
|
+
let result = files.map((f) => ({
|
|
241
|
+
...f,
|
|
242
|
+
pct: filePct(f),
|
|
243
|
+
status: fileStatus(f),
|
|
244
|
+
section: getSection(f.file),
|
|
245
|
+
}));
|
|
246
|
+
if (statusFilter !== 'all')
|
|
247
|
+
result = result.filter((f) => f.status === statusFilter);
|
|
248
|
+
if (sectionFilter !== 'all')
|
|
249
|
+
result = result.filter((f) => f.section === sectionFilter);
|
|
250
|
+
if (search.trim()) {
|
|
251
|
+
const q = search.trim().toLowerCase();
|
|
252
|
+
result = result.filter((f) => f.file.toLowerCase().includes(q));
|
|
253
|
+
}
|
|
254
|
+
return result;
|
|
255
|
+
}, [files, statusFilter, sectionFilter, search]);
|
|
256
|
+
|
|
257
|
+
const tree = useMemo(() => buildTree(filtered), [filtered]);
|
|
258
|
+
|
|
259
|
+
const toggleCollapse = (path: string) => {
|
|
260
|
+
setCollapsed((prev) => {
|
|
261
|
+
const next = new Set(prev);
|
|
262
|
+
if (next.has(path)) next.delete(path);
|
|
263
|
+
else next.add(path);
|
|
264
|
+
return next;
|
|
265
|
+
});
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const isEn = lang === 'en';
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<div className="file-list-wrap">
|
|
272
|
+
{/* Selection bar */}
|
|
273
|
+
{!isEn && selected.size > 0 && (
|
|
274
|
+
<div className="sel-bar">
|
|
275
|
+
<span>{selected.size} selected</span>
|
|
276
|
+
<button
|
|
277
|
+
type="button"
|
|
278
|
+
className="btn btn-sm"
|
|
279
|
+
onClick={onTranslateSelected}
|
|
280
|
+
>
|
|
281
|
+
Translate Selected
|
|
282
|
+
</button>
|
|
283
|
+
<button
|
|
284
|
+
type="button"
|
|
285
|
+
className="btn btn-sm btn-outline"
|
|
286
|
+
onClick={onClear}
|
|
287
|
+
>
|
|
288
|
+
Clear
|
|
289
|
+
</button>
|
|
290
|
+
</div>
|
|
291
|
+
)}
|
|
292
|
+
|
|
293
|
+
{/* Search */}
|
|
294
|
+
<div className="file-search">
|
|
295
|
+
<input
|
|
296
|
+
type="text"
|
|
297
|
+
placeholder="Search files..."
|
|
298
|
+
value={search}
|
|
299
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
300
|
+
/>
|
|
301
|
+
{search && (
|
|
302
|
+
<button
|
|
303
|
+
type="button"
|
|
304
|
+
className="file-search-clear"
|
|
305
|
+
onClick={() => setSearch('')}
|
|
306
|
+
>
|
|
307
|
+
✕
|
|
308
|
+
</button>
|
|
309
|
+
)}
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
{/* Header */}
|
|
313
|
+
<div className="file-list-hdr">
|
|
314
|
+
{!isEn && (
|
|
315
|
+
<input
|
|
316
|
+
type="checkbox"
|
|
317
|
+
checked={
|
|
318
|
+
filtered.length > 0 && filtered.every((f) => selected.has(f.file))
|
|
319
|
+
}
|
|
320
|
+
onChange={(e) => {
|
|
321
|
+
if (e.target.checked) {
|
|
322
|
+
onSelectAll(filtered.map((f) => f.file));
|
|
323
|
+
} else {
|
|
324
|
+
onClear();
|
|
325
|
+
}
|
|
326
|
+
}}
|
|
327
|
+
title="Select all visible"
|
|
328
|
+
/>
|
|
329
|
+
)}
|
|
330
|
+
<strong>
|
|
331
|
+
{lang} · {filtered.length} files
|
|
332
|
+
</strong>
|
|
333
|
+
<span className="spacer" />
|
|
334
|
+
<button
|
|
335
|
+
type="button"
|
|
336
|
+
className={`btn btn-xs${viewMode === 'list' ? ' active' : ''}`}
|
|
337
|
+
onClick={() => setViewMode('list')}
|
|
338
|
+
title="List view"
|
|
339
|
+
>
|
|
340
|
+
☰
|
|
341
|
+
</button>
|
|
342
|
+
<button
|
|
343
|
+
type="button"
|
|
344
|
+
className={`btn btn-xs${viewMode === 'tree' ? ' active' : ''}`}
|
|
345
|
+
onClick={() => setViewMode('tree')}
|
|
346
|
+
title="Tree view"
|
|
347
|
+
>
|
|
348
|
+
🌲
|
|
349
|
+
</button>
|
|
350
|
+
<select
|
|
351
|
+
value={statusFilter}
|
|
352
|
+
onChange={(e) => onStatusFilter(e.target.value as StatusFilter)}
|
|
353
|
+
>
|
|
354
|
+
<option value="all">All</option>
|
|
355
|
+
<option value="complete">✅ Complete</option>
|
|
356
|
+
<option value="partial">🟡 Partial</option>
|
|
357
|
+
<option value="missing">🔴 Missing</option>
|
|
358
|
+
</select>
|
|
359
|
+
<select
|
|
360
|
+
value={sectionFilter}
|
|
361
|
+
onChange={(e) => onSectionFilter(e.target.value as SectionFilter)}
|
|
362
|
+
>
|
|
363
|
+
<option value="all">All sections</option>
|
|
364
|
+
<option value="docs">docs</option>
|
|
365
|
+
<option value="blog">blog</option>
|
|
366
|
+
<option value="learn">learn</option>
|
|
367
|
+
</select>
|
|
368
|
+
</div>
|
|
369
|
+
|
|
370
|
+
{/* File rows */}
|
|
371
|
+
<div className="file-list-body">
|
|
372
|
+
{filtered.length === 0 && (
|
|
373
|
+
<div className="loading">No files match filter</div>
|
|
374
|
+
)}
|
|
375
|
+
|
|
376
|
+
{viewMode === 'list' &&
|
|
377
|
+
filtered.map((f) => {
|
|
378
|
+
const _missing = f.total - f.translated;
|
|
379
|
+
const color =
|
|
380
|
+
f.status === 'complete'
|
|
381
|
+
? 'var(--green)'
|
|
382
|
+
: f.status === 'partial'
|
|
383
|
+
? 'var(--yellow)'
|
|
384
|
+
: 'var(--red)';
|
|
385
|
+
|
|
386
|
+
return (
|
|
387
|
+
<div
|
|
388
|
+
key={f.file}
|
|
389
|
+
className={`file-row${activeFile === f.file ? ' active' : ''}`}
|
|
390
|
+
onClick={() => onSelect(f.file)}
|
|
391
|
+
>
|
|
392
|
+
{!isEn && (
|
|
393
|
+
<input
|
|
394
|
+
type="checkbox"
|
|
395
|
+
checked={selected.has(f.file)}
|
|
396
|
+
onChange={(e) => {
|
|
397
|
+
e.stopPropagation();
|
|
398
|
+
onToggle(f.file);
|
|
399
|
+
}}
|
|
400
|
+
onClick={(e) => e.stopPropagation()}
|
|
401
|
+
/>
|
|
402
|
+
)}
|
|
403
|
+
<span className="icon">{statusIcon(f.pct)}</span>
|
|
404
|
+
<span className="path" title={f.file}>
|
|
405
|
+
{f.file}
|
|
406
|
+
</span>
|
|
407
|
+
<span className="pct">{f.pct.toFixed(0)}%</span>
|
|
408
|
+
<div className="mini-bar">
|
|
409
|
+
<div
|
|
410
|
+
className="mini-fill"
|
|
411
|
+
style={{ width: `${f.pct}%`, background: color }}
|
|
412
|
+
/>
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
415
|
+
);
|
|
416
|
+
})}
|
|
417
|
+
|
|
418
|
+
{viewMode === 'tree' &&
|
|
419
|
+
[...tree.children.values()]
|
|
420
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
421
|
+
.map((child) => (
|
|
422
|
+
<TreeRow
|
|
423
|
+
key={child.path}
|
|
424
|
+
node={child}
|
|
425
|
+
depth={0}
|
|
426
|
+
activeFile={activeFile}
|
|
427
|
+
selected={selected}
|
|
428
|
+
isEn={isEn}
|
|
429
|
+
collapsed={collapsed}
|
|
430
|
+
onToggleCollapse={toggleCollapse}
|
|
431
|
+
onSelect={onSelect}
|
|
432
|
+
onToggle={onToggle}
|
|
433
|
+
/>
|
|
434
|
+
))}
|
|
435
|
+
</div>
|
|
436
|
+
</div>
|
|
437
|
+
);
|
|
438
|
+
}
|