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,183 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+ import type { DocsI18nConfig } from '../../../config';
4
+ import { flattenSources } from '../../../config';
5
+ import { TranslationCache } from '../../../core/cache';
6
+ import { parseMdx } from '../../../core/parser';
7
+
8
+ let _config: DocsI18nConfig;
9
+ let _cache: TranslationCache | null = null;
10
+
11
+ export function initStatus(config: DocsI18nConfig) {
12
+ _config = config;
13
+ _cache = null; // reset
14
+ }
15
+
16
+ export function getVersions(): string[] {
17
+ return flattenSources(_config).map((s) => s.versionKey);
18
+ }
19
+
20
+ export function getLangs(): string[] {
21
+ return _config.languages;
22
+ }
23
+
24
+ export function getCache(): TranslationCache {
25
+ if (!_cache) {
26
+ _cache = new TranslationCache(resolve(_config.cacheDir ?? '.cache'));
27
+ }
28
+ return _cache;
29
+ }
30
+
31
+ function getSourcePath(versionKey: string): string | null {
32
+ const source = flattenSources(_config).find((s) => s.versionKey === versionKey);
33
+ return source ? resolve(source.sourcePath) : null;
34
+ }
35
+
36
+ function walkFiles(dir: string): string[] {
37
+ const results: string[] = [];
38
+ if (!existsSync(dir)) return results;
39
+ const exts = (_config.include ?? ['**/*.mdx', '**/*.md']).map((p) => p.replace('**/*', ''));
40
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
41
+ const fullPath = join(dir, entry.name);
42
+ if (entry.isDirectory()) results.push(...walkFiles(fullPath));
43
+ else if (exts.some((ext) => entry.name.endsWith(ext))) results.push(fullPath);
44
+ }
45
+ return results;
46
+ }
47
+
48
+ export function ensureScanned(version: string): void {
49
+ const cache = getCache();
50
+ const enDir = getSourcePath(version);
51
+ if (!enDir || !existsSync(enDir)) return;
52
+
53
+ const files = walkFiles(enDir);
54
+ const currentCount = cache.sourceCount(version);
55
+ if (currentCount >= files.length) return;
56
+
57
+ cache.clearSources('', version);
58
+ for (const file of files) {
59
+ const relPath = file.slice(enDir.length + 1);
60
+ const content = readFileSync(file, 'utf8');
61
+ const nodes = parseMdx(content);
62
+ for (const node of nodes) {
63
+ if (node.needsTranslation && node.md5) {
64
+ const line = content.substring(0, node.startOffset).split('\n').length;
65
+ cache.setSource(node.md5, node.rawText, node.type);
66
+ cache.updateSource('', node.md5, relPath, line, version);
67
+ }
68
+ }
69
+ }
70
+ }
71
+
72
+ export function rescan(version: string): number {
73
+ const cache = getCache();
74
+ const enDir = getSourcePath(version);
75
+ if (!enDir || !existsSync(enDir)) return 0;
76
+
77
+ const files = walkFiles(enDir);
78
+ cache.clearSources('', version);
79
+ for (const file of files) {
80
+ const relPath = file.slice(enDir.length + 1);
81
+ const content = readFileSync(file, 'utf8');
82
+ const nodes = parseMdx(content);
83
+ for (const node of nodes) {
84
+ if (node.needsTranslation && node.md5) {
85
+ const line = content.substring(0, node.startOffset).split('\n').length;
86
+ cache.setSource(node.md5, node.rawText, node.type);
87
+ cache.updateSource('', node.md5, relPath, line, version);
88
+ }
89
+ }
90
+ }
91
+ return files.length;
92
+ }
93
+
94
+ export function getOverview() {
95
+ const cache = getCache();
96
+ const versions = getVersions();
97
+ const langs = getLangs();
98
+
99
+ const result: Record<string, {
100
+ enFileCount: number;
101
+ langs: Record<string, {
102
+ sections: Record<string, any>;
103
+ totalFiles: number;
104
+ translatedFiles: number;
105
+ totalNodes: number;
106
+ translatedNodes: number;
107
+ }>;
108
+ }> = {};
109
+
110
+ for (const version of versions) {
111
+ ensureScanned(version);
112
+ const enDir = getSourcePath(version);
113
+ const enFileCount = enDir && existsSync(enDir) ? walkFiles(enDir).length : 0;
114
+
115
+ const langStats: (typeof result)[string]['langs'] = {};
116
+ for (const lang of langs) {
117
+ const sections = cache.sectionStats(version, lang);
118
+ let totalFiles = 0, translatedFiles = 0, totalNodes = 0, translatedNodes = 0;
119
+ const sectionMap: Record<string, any> = {};
120
+ for (const s of sections) {
121
+ sectionMap[s.section] = s;
122
+ totalFiles += s.totalFiles;
123
+ translatedFiles += s.translatedFiles;
124
+ totalNodes += s.totalNodes;
125
+ translatedNodes += s.translatedNodes;
126
+ }
127
+ langStats[lang] = { sections: sectionMap, totalFiles, translatedFiles, totalNodes, translatedNodes };
128
+ }
129
+ result[version] = { enFileCount, langs: langStats };
130
+ }
131
+ return result;
132
+ }
133
+
134
+ export function getFileCoverage(version: string, lang: string) {
135
+ ensureScanned(version);
136
+ return getCache().fileCoverage(version, lang);
137
+ }
138
+
139
+ export function getFileDetail(version: string, lang: string, file: string) {
140
+ ensureScanned(version);
141
+ return getCache().fileDetail(version, lang, file);
142
+ }
143
+
144
+ export interface FileBlock {
145
+ md5: string | null;
146
+ type: string;
147
+ source: string;
148
+ translation: string | null;
149
+ }
150
+
151
+ export function getFileBlocks(version: string, lang: string, file: string): FileBlock[] | null {
152
+ const enDir = getSourcePath(version);
153
+ if (!enDir) return null;
154
+
155
+ const enPath = join(enDir, file);
156
+ if (!existsSync(enPath)) return null;
157
+
158
+ const cache = getCache();
159
+ const content = readFileSync(enPath, 'utf8');
160
+ const nodes = parseMdx(content);
161
+ const blocks: FileBlock[] = [];
162
+ let lastEnd = 0;
163
+
164
+ for (const node of nodes) {
165
+ if (node.startOffset > lastEnd) {
166
+ const gap = content.substring(lastEnd, node.startOffset);
167
+ blocks.push({ md5: null, type: 'gap', source: gap, translation: null });
168
+ }
169
+ if (node.needsTranslation && node.md5) {
170
+ const translation = lang === 'en' ? null : (cache.get(lang, node.md5) ?? null);
171
+ blocks.push({ md5: node.md5, type: node.type, source: node.rawText, translation });
172
+ } else {
173
+ blocks.push({ md5: null, type: node.type, source: node.rawText, translation: null });
174
+ }
175
+ lastEnd = node.endOffset;
176
+ }
177
+
178
+ if (lastEnd < content.length) {
179
+ const tail = content.substring(lastEnd);
180
+ if (tail.trim()) blocks.push({ md5: null, type: 'gap', source: tail, translation: null });
181
+ }
182
+ return blocks;
183
+ }
@@ -0,0 +1,326 @@
1
+ import { useQuery } from '@tanstack/react-query';
2
+ import { useCallback, useEffect, useState } from 'react';
3
+ import { FileList } from './components/FileList';
4
+ import { JobDialog } from './components/JobDialog';
5
+ import { JobPanel } from './components/JobPanel';
6
+ import { LangGrid } from './components/LangGrid';
7
+ import { Preview } from './components/Preview';
8
+ import { api } from './lib/api';
9
+
10
+ type ViewMode = 'split' | 'en' | 'lang';
11
+ type StatusFilter = 'all' | 'complete' | 'partial' | 'missing';
12
+ type SectionFilter = 'all' | 'docs' | 'blog' | 'learn';
13
+
14
+ // ── URL state helpers ──
15
+
16
+ function readParams() {
17
+ const p = new URLSearchParams(window.location.search);
18
+ return {
19
+ version: p.get('v') || 'latest',
20
+ lang: p.get('lang') || null,
21
+ file: p.get('file') || null,
22
+ showFiles: p.get('files') !== '0',
23
+ view: (p.get('view') as ViewMode) || 'lang',
24
+ toc: p.get('toc') !== '0',
25
+ nodes: p.get('nodes') === '1',
26
+ status: (p.get('status') as StatusFilter) || 'all',
27
+ section: (p.get('section') as SectionFilter) || 'all',
28
+ };
29
+ }
30
+
31
+ function setParams(updates: Record<string, string | null>) {
32
+ const next = new URLSearchParams(window.location.search);
33
+ for (const [k, v] of Object.entries(updates)) {
34
+ if (v === null || v === '') next.delete(k);
35
+ else next.set(k, v);
36
+ }
37
+ const qs = next.toString();
38
+ window.history.replaceState(null, '', qs ? `?${qs}` : '/');
39
+ }
40
+
41
+ export function App() {
42
+ // bump to force re-read from URL
43
+ const [, rerender] = useState(0);
44
+ const bump = useCallback(() => rerender((n) => n + 1), []);
45
+
46
+ // Theme
47
+ const [theme, setThemeState] = useState(() => {
48
+ const saved = localStorage.getItem('theme');
49
+ return saved === 'light' ? 'light' : 'dark';
50
+ });
51
+ // Toast
52
+ const [toast, setToast] = useState<string | null>(null);
53
+
54
+ useEffect(() => {
55
+ document.documentElement.dataset.theme = theme;
56
+ }, [theme]);
57
+
58
+ useEffect(() => {
59
+ if (!toast) return;
60
+ const t = setTimeout(() => setToast(null), 3000);
61
+ return () => clearTimeout(t);
62
+ }, [toast]);
63
+
64
+ const { version, lang, file, showFiles, view, toc, nodes, status, section } =
65
+ readParams();
66
+
67
+ // ── URL setters ──
68
+
69
+ const setVersion = useCallback(
70
+ (v: string) => {
71
+ setParams({
72
+ v: v === 'latest' ? null : v,
73
+ file: null,
74
+ });
75
+ bump();
76
+ },
77
+ [bump],
78
+ );
79
+
80
+ const setLang = useCallback(
81
+ (l: string | null) => {
82
+ setParams({ lang: l });
83
+ bump();
84
+ },
85
+ [bump],
86
+ );
87
+
88
+ const setFile = useCallback(
89
+ (f: string | null) => {
90
+ setParams({ file: f });
91
+ bump();
92
+ },
93
+ [bump],
94
+ );
95
+
96
+ const setShowFiles = useCallback(
97
+ (show: boolean) => {
98
+ setParams({ files: show ? null : '0' });
99
+ bump();
100
+ },
101
+ [bump],
102
+ );
103
+
104
+ const setView = useCallback(
105
+ (m: ViewMode) => {
106
+ setParams({ view: m === 'lang' ? null : m });
107
+ bump();
108
+ },
109
+ [bump],
110
+ );
111
+
112
+ const setNodes = useCallback(
113
+ (show: boolean) => {
114
+ setParams({ nodes: show ? '1' : null });
115
+ bump();
116
+ },
117
+ [bump],
118
+ );
119
+
120
+ const setToc = useCallback(
121
+ (show: boolean) => {
122
+ setParams({ toc: show ? null : '0' });
123
+ bump();
124
+ },
125
+ [bump],
126
+ );
127
+
128
+ const setStatusFilter = useCallback(
129
+ (s: StatusFilter) => {
130
+ setParams({ status: s === 'all' ? null : s });
131
+ bump();
132
+ },
133
+ [bump],
134
+ );
135
+
136
+ const setSectionFilter = useCallback(
137
+ (s: SectionFilter) => {
138
+ setParams({ section: s === 'all' ? null : s });
139
+ bump();
140
+ },
141
+ [bump],
142
+ );
143
+
144
+ // ── Non-URL state ──
145
+ const [selected, setSelected] = useState<Set<string>>(new Set());
146
+ const [showDialog, setShowDialog] = useState(false);
147
+ const [dialogFiles, setDialogFiles] = useState<string[] | undefined>();
148
+
149
+ // ── Queries ──
150
+ const { data: statusData } = useQuery({
151
+ queryKey: ['status'],
152
+ queryFn: api.status,
153
+ });
154
+
155
+ const { data: files } = useQuery({
156
+ queryKey: ['files', version, lang],
157
+ queryFn: () => api.fileCoverage(version, lang as string),
158
+ enabled: !!lang,
159
+ });
160
+
161
+ // ── Handlers ──
162
+ const handleSelectVersion = useCallback(
163
+ (v: string) => {
164
+ setVersion(v);
165
+ setSelected(new Set());
166
+ },
167
+ [setVersion],
168
+ );
169
+
170
+ const handleSelectLang = useCallback(
171
+ (l: string) => {
172
+ setLang(l);
173
+ setSelected(new Set());
174
+ },
175
+ [setLang],
176
+ );
177
+
178
+ const handleToggle = useCallback((f: string) => {
179
+ setSelected((prev) => {
180
+ const next = new Set(prev);
181
+ if (next.has(f)) next.delete(f);
182
+ else next.add(f);
183
+ return next;
184
+ });
185
+ }, []);
186
+
187
+ const handleSelectAll = useCallback(
188
+ (fileList: string[]) => setSelected(new Set(fileList)),
189
+ [],
190
+ );
191
+
192
+ const handleClear = useCallback(() => setSelected(new Set()), []);
193
+
194
+ const handleTranslateSelected = useCallback(() => {
195
+ setDialogFiles([...selected]);
196
+ setShowDialog(true);
197
+ }, [selected]);
198
+
199
+ const handleNewJob = useCallback(() => {
200
+ setDialogFiles(undefined);
201
+ setShowDialog(true);
202
+ }, []);
203
+
204
+ if (!statusData) return <div className="loading">Loading...</div>;
205
+
206
+ return (
207
+ <>
208
+ <nav>
209
+ <h1>🌐 Translation Admin</h1>
210
+ <span className="spacer" />
211
+ <button type="button" className="btn" onClick={handleNewJob}>
212
+ + New Job
213
+ </button>
214
+ <button
215
+ type="button"
216
+ className="btn btn-icon"
217
+ onClick={() => {
218
+ const next = theme === 'light' ? 'dark' : 'light';
219
+ setThemeState(next);
220
+ localStorage.setItem('theme', next);
221
+ }}
222
+ title="Toggle theme"
223
+ >
224
+ {theme === 'light' ? '🌙' : '☀️'}
225
+ </button>
226
+ </nav>
227
+
228
+ <div className="container">
229
+ {/* Version tabs */}
230
+ <div className="tabs">
231
+ {statusData.versions.map((v) => (
232
+ <button
233
+ key={v}
234
+ type="button"
235
+ className={`tab${v === version ? ' active' : ''}`}
236
+ onClick={() => handleSelectVersion(v)}
237
+ >
238
+ {v}
239
+ </button>
240
+ ))}
241
+ </div>
242
+
243
+ {/* Language cards */}
244
+ <LangGrid
245
+ data={statusData}
246
+ version={version}
247
+ selectedLang={lang}
248
+ onSelect={handleSelectLang}
249
+ />
250
+
251
+ {/* Jobs */}
252
+ <JobPanel />
253
+
254
+ {/* File list + Preview */}
255
+ {lang && files && (
256
+ <>
257
+ <div className="file-panel-toolbar">
258
+ <button
259
+ type="button"
260
+ className={`btn btn-sm${showFiles ? ' active' : ''}`}
261
+ onClick={() => setShowFiles(!showFiles)}
262
+ >
263
+ {showFiles ? '◀ Hide files' : '▶ Show files'}
264
+ </button>
265
+ {file && <span className="file-panel-current">{file}</span>}
266
+ </div>
267
+ <div
268
+ className={`file-panel${!showFiles ? ' no-list' : ''}${!file ? ' no-preview' : ''}`}
269
+ >
270
+ {showFiles && (
271
+ <FileList
272
+ files={files}
273
+ lang={lang}
274
+ activeFile={file}
275
+ selected={selected}
276
+ statusFilter={status}
277
+ sectionFilter={section}
278
+ onStatusFilter={setStatusFilter}
279
+ onSectionFilter={setSectionFilter}
280
+ onSelect={setFile}
281
+ onToggle={handleToggle}
282
+ onSelectAll={handleSelectAll}
283
+ onClear={handleClear}
284
+ onTranslateSelected={handleTranslateSelected}
285
+ />
286
+ )}
287
+ {file && (
288
+ <Preview
289
+ version={version}
290
+ lang={lang}
291
+ file={file}
292
+ viewMode={view}
293
+ onViewMode={setView}
294
+ showToc={toc}
295
+ onToggleToc={() => setToc(!toc)}
296
+ showNodes={nodes}
297
+ onToggleNodes={() => setNodes(!nodes)}
298
+ onClose={() => setFile(null)}
299
+ />
300
+ )}
301
+ </div>
302
+ </>
303
+ )}
304
+ </div>
305
+
306
+ {/* Toast */}
307
+ {toast && <div className="toast">{toast}</div>}
308
+
309
+ {/* Job dialog */}
310
+ {showDialog && (
311
+ <JobDialog
312
+ langs={statusData.langs}
313
+ versions={statusData.versions}
314
+ defaultLang={lang || undefined}
315
+ defaultVersion={version}
316
+ files={dialogFiles}
317
+ onClose={() => setShowDialog(false)}
318
+ onSuccess={(msg) => {
319
+ setShowDialog(false);
320
+ setToast(msg);
321
+ }}
322
+ />
323
+ )}
324
+ </>
325
+ );
326
+ }