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.
Files changed (49) hide show
  1. package/dist/{assemble-IOHQYYHI.js → assemble-ZHDLGVTL.js} +3 -4
  2. package/dist/chunk-I74LIORX.js +11211 -0
  3. package/dist/{chunk-QSVWLTGQ.js → chunk-OSMPWXSQ.js} +1 -1
  4. package/dist/{chunk-AKLW2MUS.js → chunk-PHDMD6EM.js} +29 -7
  5. package/dist/cli.js +6 -7
  6. package/dist/{rescan-VB2PILB2.js → rescan-OJTVWDAP.js} +2 -3
  7. package/dist/server-HNVJP43X.js +2742 -0
  8. package/dist/{status-EWQEACVF.js → status-ZG7F3FRT.js} +1 -2
  9. package/dist/translate-2PCYIWIG.js +14531 -0
  10. package/package.json +3 -2
  11. package/src/admin/index.html +13 -0
  12. package/src/admin/server/index.ts +88 -0
  13. package/src/admin/server/routes/jobs.ts +113 -0
  14. package/src/admin/server/routes/models.ts +87 -0
  15. package/src/admin/server/routes/status.ts +57 -0
  16. package/src/admin/server/services/job-manager.ts +184 -0
  17. package/src/admin/server/services/status.ts +183 -0
  18. package/src/admin/ui/App.tsx +326 -0
  19. package/src/admin/ui/components/FileList.tsx +438 -0
  20. package/src/admin/ui/components/JobDialog.tsx +360 -0
  21. package/src/admin/ui/components/JobPanel.tsx +134 -0
  22. package/src/admin/ui/components/LangGrid.tsx +54 -0
  23. package/src/admin/ui/components/Preview.tsx +369 -0
  24. package/src/admin/ui/components/ProgressBar.tsx +21 -0
  25. package/src/admin/ui/lib/api.ts +154 -0
  26. package/src/admin/ui/lib/flags.ts +30 -0
  27. package/src/admin/ui/main.tsx +19 -0
  28. package/src/admin/ui/styles.css +1096 -0
  29. package/src/admin/vite.config.ts +7 -0
  30. package/dist/build-4EQEL4NI.js +0 -12
  31. package/dist/build2-3W5WMFHZ.js +0 -4901
  32. package/dist/chunk-3YNFMSJH.js +0 -30
  33. package/dist/chunk-55MBYBVK.js +0 -368
  34. package/dist/chunk-FYDB7MZX.js +0 -38944
  35. package/dist/chunk-O35QHRY6.js +0 -6
  36. package/dist/chunk-PTIH4GGE.js +0 -44
  37. package/dist/chunk-SUIDX6IZ.js +0 -122
  38. package/dist/chunk-VKKNQBDN.js +0 -6487
  39. package/dist/dist-6C32URTL.js +0 -19
  40. package/dist/dist-HOWMMQFV.js +0 -6677
  41. package/dist/false-JGP4AGWN.js +0 -7
  42. package/dist/main-QVE5TVA3.js +0 -2505
  43. package/dist/node-4GLCLDJ6.js +0 -875
  44. package/dist/node-NUDVMOF2.js +0 -129
  45. package/dist/postcss-3SK7VUC2.js +0 -5886
  46. package/dist/postcss-import-JD46KA2Z.js +0 -458
  47. package/dist/prompt-BYQIwEjg-TG7DLENB.js +0 -915
  48. package/dist/server-ER56DGPR.js +0 -548
  49. 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
+ }