docs-i18n 0.6.3 → 0.7.0
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/{src/admin/ui → admin/app}/components/JobDialog.tsx +21 -2
- package/{src/admin/ui → admin/app}/components/JobPanel.tsx +1 -1
- package/{src/admin/ui → admin/app}/components/Preview.tsx +2 -5
- package/{src/admin/ui → admin/app}/lib/api.ts +18 -39
- package/admin/app/routeTree.gen.ts +68 -0
- package/admin/app/router.tsx +23 -0
- package/admin/app/routes/__root.tsx +55 -0
- package/admin/app/routes/index.tsx +416 -0
- package/{src/admin/ui → admin/app}/styles.css +36 -3
- package/admin/package.json +27 -0
- package/admin/server/functions/jobs.ts +53 -0
- package/admin/server/functions/misc.ts +84 -0
- package/{src/admin/server/routes → admin/server/functions}/models.ts +16 -29
- package/admin/server/functions/status.ts +61 -0
- package/admin/server/index.ts +35 -0
- package/admin/server/init.ts +46 -0
- package/{src/admin → admin}/server/services/job-manager.ts +39 -10
- package/{src/admin → admin}/server/services/status.ts +6 -6
- package/admin/tsconfig.json +19 -0
- package/{src/admin → admin}/vite.config.ts +8 -2
- package/dist/{assemble-7H4QCW35.js → assemble-CP2BRYQJ.js} +6 -4
- package/dist/{chunk-A3YQNPKZ.js → chunk-CLYUAWZE.js} +1 -1
- package/dist/{chunk-YN4VJHCQ.js → chunk-JHBSHTXC.js} +1 -1
- package/dist/chunk-L64GJ4OB.js +32 -0
- package/dist/{chunk-SKKZIV3L.js → chunk-PNKVD2UK.js} +1 -29
- package/dist/{chunk-XEOYZUHS.js → chunk-QKIR7RKQ.js} +4 -31
- package/dist/chunk-TRURQFP4.js +31 -0
- package/dist/cli.js +108 -7
- package/dist/index.d.ts +41 -1
- package/dist/index.js +92 -3
- package/dist/{rescan-O5D3CYC2.js → rescan-HXMWFAOC.js} +5 -3
- package/dist/{status-F4MYIAAY.js → status-AGZDXOTZ.js} +4 -2
- package/dist/{translate-ZIVKNAC4.js → translate-A5X6MX4Y.js} +14 -7
- package/dist/upload-XL6KG6S2.js +132 -0
- package/package.json +17 -15
- package/template/app/components/BlogArticle.tsx +159 -0
- package/template/app/components/BlogList.tsx +88 -0
- package/template/app/components/Breadcrumbs.tsx +81 -0
- package/template/app/components/Card.tsx +31 -0
- package/template/app/components/Doc.tsx +191 -0
- package/template/app/components/DocBreadcrumb.tsx +60 -0
- package/template/app/components/DocContainer.tsx +13 -0
- package/template/app/components/DocTitle.tsx +11 -0
- package/template/app/components/DocsLayout.tsx +715 -0
- package/template/app/components/Dropdown.tsx +116 -0
- package/template/app/components/FallbackBanner.tsx +36 -0
- package/template/app/components/Footer.tsx +29 -0
- package/template/app/components/FrameworkSelect.tsx +150 -0
- package/template/app/components/LibraryCard.tsx +178 -0
- package/template/app/components/LocaleSwitcher.tsx +43 -0
- package/template/app/components/Navbar.tsx +430 -0
- package/template/app/components/PostNotFound.tsx +20 -0
- package/template/app/components/SearchButton.tsx +32 -0
- package/template/app/components/Select.tsx +103 -0
- package/template/app/components/Spinner.tsx +18 -0
- package/template/app/components/ThemeProvider.tsx +141 -0
- package/template/app/components/ThemeToggle.tsx +31 -0
- package/template/app/components/Toc.tsx +86 -0
- package/template/app/components/VersionSelect.tsx +118 -0
- package/template/app/components/icons/BSkyIcon.tsx +27 -0
- package/template/app/components/icons/BaseballCapIcon.tsx +25 -0
- package/template/app/components/icons/BrandXIcon.tsx +28 -0
- package/template/app/components/icons/CheckCircleIcon.tsx +28 -0
- package/template/app/components/icons/CogsIcon.tsx +25 -0
- package/template/app/components/icons/DiscordIcon.tsx +24 -0
- package/template/app/components/icons/GithubIcon.tsx +24 -0
- package/template/app/components/icons/GoogleIcon.tsx +24 -0
- package/template/app/components/icons/InstagramIcon.tsx +24 -0
- package/template/app/components/icons/NpmIcon.tsx +26 -0
- package/template/app/components/icons/YinYangIcon.tsx +26 -0
- package/template/app/components/icons/YouTubeIcon.tsx +24 -0
- package/template/app/components/markdown/CodeBlock.tsx +254 -0
- package/template/app/components/markdown/FileTabs.tsx +58 -0
- package/template/app/components/markdown/FrameworkContent.tsx +76 -0
- package/template/app/components/markdown/Markdown.tsx +216 -0
- package/template/app/components/markdown/MarkdownContent.tsx +89 -0
- package/template/app/components/markdown/MarkdownFrameworkHandler.tsx +66 -0
- package/template/app/components/markdown/MarkdownHeadingContext.tsx +35 -0
- package/template/app/components/markdown/MarkdownLink.tsx +46 -0
- package/template/app/components/markdown/MarkdownTabsHandler.tsx +109 -0
- package/template/app/components/markdown/PackageManagerTabs.tsx +95 -0
- package/template/app/components/markdown/Tabs.tsx +139 -0
- package/template/app/components/markdown/index.ts +15 -0
- package/template/app/components/ui/Button.tsx +141 -0
- package/template/app/components/ui/InlineCode.tsx +16 -0
- package/template/app/components/ui/MarkdownImg.tsx +21 -0
- package/template/app/config/frameworks.ts +93 -0
- package/template/app/contexts/SearchContext.tsx +36 -0
- package/template/app/db/index.ts +17 -0
- package/template/app/db/schema.ts +74 -0
- package/template/app/hooks/useClickOutside.ts +106 -0
- package/template/app/routeTree.gen.ts +584 -0
- package/template/app/router.tsx +29 -0
- package/template/app/routes/$lang.$project.$version.docs.$.tsx +128 -0
- package/template/app/routes/$lang.$project.$version.docs.framework.$framework.$.tsx +106 -0
- package/template/app/routes/$lang.$project.$version.docs.framework.$framework.index.tsx +27 -0
- package/template/app/routes/$lang.$project.$version.docs.framework.index.tsx +44 -0
- package/template/app/routes/$lang.$project.$version.docs.index.tsx +27 -0
- package/template/app/routes/$lang.$project.$version.docs.tsx +70 -0
- package/template/app/routes/$lang.$project.$version.tsx +69 -0
- package/template/app/routes/$lang.$project.docs.$.tsx +104 -0
- package/template/app/routes/$lang.$project.docs.index.tsx +20 -0
- package/template/app/routes/$lang.$project.docs.tsx +79 -0
- package/template/app/routes/$lang.$project.tsx +89 -0
- package/template/app/routes/$lang.blog.$.tsx +82 -0
- package/template/app/routes/$lang.blog.index.tsx +56 -0
- package/template/app/routes/$lang.blog.tsx +26 -0
- package/template/app/routes/$lang.docs.$.tsx +100 -0
- package/template/app/routes/$lang.docs.framework.$framework.$.tsx +104 -0
- package/template/app/routes/$lang.docs.framework.$framework.index.tsx +32 -0
- package/template/app/routes/$lang.docs.framework.index.tsx +47 -0
- package/template/app/routes/$lang.docs.index.tsx +20 -0
- package/template/app/routes/$lang.docs.tsx +90 -0
- package/template/app/routes/$lang.tsx +16 -0
- package/template/app/routes/__root.tsx +180 -0
- package/template/app/routes/index.tsx +89 -0
- package/template/app/site.config.ts +182 -0
- package/template/app/styles/app.css +1029 -0
- package/template/app/types/index.ts +77 -0
- package/template/app/utils/blog.server.ts +193 -0
- package/template/app/utils/blog.ts +42 -0
- package/template/app/utils/config.ts +120 -0
- package/template/app/utils/content-loader.ts +400 -0
- package/template/app/utils/dates.ts +29 -0
- package/template/app/utils/docs.server.ts +150 -0
- package/template/app/utils/markdown/filterFrameworkContent.ts +233 -0
- package/template/app/utils/markdown/index.ts +2 -0
- package/template/app/utils/markdown/installCommand.ts +143 -0
- package/template/app/utils/markdown/plugins/collectHeadings.ts +104 -0
- package/template/app/utils/markdown/plugins/extractCodeMeta.ts +57 -0
- package/template/app/utils/markdown/plugins/helpers.ts +33 -0
- package/template/app/utils/markdown/plugins/index.ts +8 -0
- package/template/app/utils/markdown/plugins/parseCommentComponents.ts +103 -0
- package/template/app/utils/markdown/plugins/transformCommentComponents.ts +23 -0
- package/template/app/utils/markdown/plugins/transformFrameworkComponent.ts +217 -0
- package/template/app/utils/markdown/plugins/transformTabsComponent.ts +359 -0
- package/template/app/utils/markdown/processor.ts +75 -0
- package/template/app/utils/site-config.tsx +11 -0
- package/template/app/utils/upload.ts +232 -0
- package/template/app/utils/useLocalStorage.ts +65 -0
- package/template/app/utils/utils.ts +23 -0
- package/template/package.json +54 -0
- package/template/public/favicon.svg +1 -0
- package/template/public/fonts/Inter-latin-ext.woff2 +0 -0
- package/template/public/fonts/Inter-latin.woff2 +0 -0
- package/template/public/images/frameworks/angular-logo.svg +1 -0
- package/template/public/images/frameworks/js-logo.svg +1 -0
- package/template/public/images/frameworks/lit-logo.svg +1 -0
- package/template/public/images/frameworks/preact-logo.svg +6 -0
- package/template/public/images/frameworks/qwik-logo.svg +1 -0
- package/template/public/images/frameworks/react-logo.svg +1 -0
- package/template/public/images/frameworks/solid-logo.svg +1 -0
- package/template/public/images/frameworks/svelte-logo.svg +1 -0
- package/template/public/images/frameworks/vue-logo.svg +4 -0
- package/template/tsconfig.json +24 -0
- package/template/vite.config.ts +43 -0
- package/template/wrangler.jsonc +16 -0
- package/README.md +0 -161
- package/dist/server-73AVSOL5.js +0 -598
- package/src/admin/index.html +0 -13
- package/src/admin/server/index.ts +0 -138
- package/src/admin/server/routes/jobs.ts +0 -113
- package/src/admin/server/routes/status.ts +0 -57
- package/src/admin/ui/App.tsx +0 -332
- package/src/admin/ui/main.tsx +0 -19
- /package/{src/admin/ui → admin/app}/components/FileList.tsx +0 -0
- /package/{src/admin/ui → admin/app}/components/LangGrid.tsx +0 -0
- /package/{src/admin/ui → admin/app}/components/ProgressBar.tsx +0 -0
- /package/{src/admin/ui → admin/app}/lib/flags.ts +0 -0
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import { spawn as nodeSpawn } from 'node:child_process';
|
|
2
|
-
import { createServer } from 'node:http';
|
|
3
|
-
import { readFileSync } from 'node:fs';
|
|
4
|
-
import { resolve, dirname } from 'node:path';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { Hono } from 'hono';
|
|
7
|
-
import type { DocsI18nConfig } from '../../config';
|
|
8
|
-
import { initStatus } from './services/status';
|
|
9
|
-
import jobRoutes from './routes/jobs';
|
|
10
|
-
import modelRoutes from './routes/models';
|
|
11
|
-
import statusRoutes from './routes/status';
|
|
12
|
-
import { setConfig } from './services/job-manager';
|
|
13
|
-
|
|
14
|
-
function loadVersion(): string {
|
|
15
|
-
try {
|
|
16
|
-
// Walk up from dist/ to find package.json
|
|
17
|
-
let dir = dirname(fileURLToPath(import.meta.url));
|
|
18
|
-
for (let i = 0; i < 5; i++) {
|
|
19
|
-
try {
|
|
20
|
-
const pkg = JSON.parse(readFileSync(resolve(dir, 'package.json'), 'utf-8'));
|
|
21
|
-
if (pkg.name === 'docs-i18n') return pkg.version;
|
|
22
|
-
} catch {}
|
|
23
|
-
dir = dirname(dir);
|
|
24
|
-
}
|
|
25
|
-
} catch {}
|
|
26
|
-
return 'unknown';
|
|
27
|
-
}
|
|
28
|
-
const PKG_VERSION = loadVersion();
|
|
29
|
-
|
|
30
|
-
export async function startAdmin(config: DocsI18nConfig, port = 3456) {
|
|
31
|
-
initStatus(config);
|
|
32
|
-
setConfig(config);
|
|
33
|
-
|
|
34
|
-
const app = new Hono();
|
|
35
|
-
app.get('/api/version', (c) => c.json({ version: PKG_VERSION }));
|
|
36
|
-
app.route('/api/status', statusRoutes);
|
|
37
|
-
app.route('/api/jobs', jobRoutes);
|
|
38
|
-
app.route('/api/models', modelRoutes);
|
|
39
|
-
app.get('/api/health', (c) => c.json({ ok: true }));
|
|
40
|
-
app.get('/api/config', (c) => c.json({ projectRoot: process.cwd() }));
|
|
41
|
-
|
|
42
|
-
// Open file in editor
|
|
43
|
-
app.post('/api/open-file', async (c) => {
|
|
44
|
-
const { file } = await c.req.json<{ file: string }>();
|
|
45
|
-
if (!file) return c.json({ error: 'Missing file' }, 400);
|
|
46
|
-
const fullPath = resolve(process.cwd(), file);
|
|
47
|
-
if (!fullPath.startsWith(process.cwd())) return c.json({ error: 'Invalid path' }, 400);
|
|
48
|
-
|
|
49
|
-
const candidates = process.env.EDITOR_CMD ? [process.env.EDITOR_CMD] : ['code', 'cursor', 'zed'];
|
|
50
|
-
for (const cmd of candidates) {
|
|
51
|
-
const found = await new Promise<boolean>((r) => {
|
|
52
|
-
const p = nodeSpawn('which', [cmd], { stdio: 'ignore' });
|
|
53
|
-
p.on('exit', (code) => r(code === 0));
|
|
54
|
-
p.on('error', () => r(false));
|
|
55
|
-
});
|
|
56
|
-
if (found) {
|
|
57
|
-
nodeSpawn(cmd, [fullPath], { stdio: 'ignore', detached: true }).unref();
|
|
58
|
-
return c.json({ opened: fullPath, editor: cmd });
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
const fallback = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
62
|
-
nodeSpawn(fallback, [fullPath], { stdio: 'ignore', detached: true }).unref();
|
|
63
|
-
return c.json({ opened: fullPath, editor: fallback });
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// Resolve admin UI root — works both in dev (src/admin/) and installed (node_modules/docs-i18n/src/admin/)
|
|
67
|
-
const thisFile = new URL(import.meta.url).pathname;
|
|
68
|
-
// In bundled dist: thisFile is like .../dist/cli.js or .../dist/server-XXX.js
|
|
69
|
-
// Admin UI source is at .../src/admin/
|
|
70
|
-
const pkgRoot = resolve(thisFile, '..', '..');
|
|
71
|
-
const adminRoot = resolve(pkgRoot, 'src', 'admin');
|
|
72
|
-
|
|
73
|
-
// Try to load Vite for dev mode (SPA)
|
|
74
|
-
try {
|
|
75
|
-
const { createServer: createViteServer } = await import('vite');
|
|
76
|
-
const vite = await createViteServer({
|
|
77
|
-
root: adminRoot,
|
|
78
|
-
server: { middlewareMode: true },
|
|
79
|
-
appType: 'spa',
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
const server = createServer(async (req, res) => {
|
|
83
|
-
const url = req.url ?? '/';
|
|
84
|
-
if (url.startsWith('/api')) {
|
|
85
|
-
const headers = new Headers();
|
|
86
|
-
for (const [k, v] of Object.entries(req.headers)) {
|
|
87
|
-
if (v) headers.set(k, Array.isArray(v) ? v.join(', ') : v);
|
|
88
|
-
}
|
|
89
|
-
const body = req.method !== 'GET' && req.method !== 'HEAD'
|
|
90
|
-
? await new Promise<string>((r) => {
|
|
91
|
-
let data = '';
|
|
92
|
-
req.on('data', (c: Buffer) => { data += c.toString(); });
|
|
93
|
-
req.on('end', () => r(data));
|
|
94
|
-
})
|
|
95
|
-
: undefined;
|
|
96
|
-
const webReq = new Request(`http://localhost:${port}${url}`, {
|
|
97
|
-
method: req.method,
|
|
98
|
-
headers,
|
|
99
|
-
body,
|
|
100
|
-
});
|
|
101
|
-
const webRes = await app.fetch(webReq);
|
|
102
|
-
res.writeHead(webRes.status, Object.fromEntries(webRes.headers.entries()));
|
|
103
|
-
res.end(Buffer.from(await webRes.arrayBuffer()));
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
vite.middlewares(req, res);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
server.listen(port, () => {
|
|
110
|
-
console.log(`🌐 docs-i18n admin → http://localhost:${port}`);
|
|
111
|
-
});
|
|
112
|
-
} catch (err) {
|
|
113
|
-
console.error('Failed to start admin UI with Vite:', (err as Error).message);
|
|
114
|
-
console.log('Starting API-only mode...');
|
|
115
|
-
// Fallback: Hono on Node http server without Vite
|
|
116
|
-
const server = createServer(async (req, res) => {
|
|
117
|
-
const url = req.url ?? '/';
|
|
118
|
-
const headers = new Headers();
|
|
119
|
-
for (const [k, v] of Object.entries(req.headers)) {
|
|
120
|
-
if (v) headers.set(k, Array.isArray(v) ? v.join(', ') : v);
|
|
121
|
-
}
|
|
122
|
-
const body = req.method !== 'GET' && req.method !== 'HEAD'
|
|
123
|
-
? await new Promise<string>((r) => {
|
|
124
|
-
let data = '';
|
|
125
|
-
req.on('data', (c: Buffer) => { data += c.toString(); });
|
|
126
|
-
req.on('end', () => r(data));
|
|
127
|
-
})
|
|
128
|
-
: undefined;
|
|
129
|
-
const webReq = new Request(`http://localhost:${port}${url}`, { method: req.method, headers, body });
|
|
130
|
-
const webRes = await app.fetch(webReq);
|
|
131
|
-
res.writeHead(webRes.status, Object.fromEntries(webRes.headers.entries()));
|
|
132
|
-
res.end(Buffer.from(await webRes.arrayBuffer()));
|
|
133
|
-
});
|
|
134
|
-
server.listen(port, () => {
|
|
135
|
-
console.log(`🌐 docs-i18n admin (API only) → http://localhost:${port}`);
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
}
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import { Hono } from 'hono';
|
|
2
|
-
import { streamSSE } from 'hono/streaming';
|
|
3
|
-
import { jobManager } from '../services/job-manager';
|
|
4
|
-
|
|
5
|
-
const app = new Hono();
|
|
6
|
-
|
|
7
|
-
/** GET /api/jobs — List all jobs */
|
|
8
|
-
app.get('/', (c) => {
|
|
9
|
-
return c.json(
|
|
10
|
-
jobManager.list().map((j) => ({
|
|
11
|
-
...j,
|
|
12
|
-
logLines: (j.logLines ?? []).slice(-20), // Last 20 lines in list view
|
|
13
|
-
})),
|
|
14
|
-
);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
/** POST /api/jobs — Start a new translation job */
|
|
18
|
-
app.post('/', async (c) => {
|
|
19
|
-
const body = await c.req.json<{
|
|
20
|
-
lang: string;
|
|
21
|
-
version: string;
|
|
22
|
-
max?: number;
|
|
23
|
-
concurrency?: number;
|
|
24
|
-
model?: string;
|
|
25
|
-
modelRotate?: string[];
|
|
26
|
-
md5?: boolean;
|
|
27
|
-
files?: string[];
|
|
28
|
-
}>();
|
|
29
|
-
|
|
30
|
-
if (!body.lang || !body.version) {
|
|
31
|
-
return c.json({ error: 'Missing lang or version' }, 400);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
const job = jobManager.start(body);
|
|
36
|
-
return c.json(job, 201);
|
|
37
|
-
} catch (err) {
|
|
38
|
-
return c.json(
|
|
39
|
-
{ error: err instanceof Error ? err.message : 'Unknown error' },
|
|
40
|
-
409,
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
/** GET /api/jobs/:id — Get job details */
|
|
46
|
-
app.get('/:id', (c) => {
|
|
47
|
-
const job = jobManager.get(c.req.param('id'));
|
|
48
|
-
if (!job) return c.json({ error: 'Job not found' }, 404);
|
|
49
|
-
return c.json(job);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
/** DELETE /api/jobs/:id — Cancel a running job or remove a finished one */
|
|
53
|
-
app.delete('/:id', (c) => {
|
|
54
|
-
const id = c.req.param('id');
|
|
55
|
-
const job = jobManager.get(id);
|
|
56
|
-
if (!job) return c.json({ error: 'Job not found' }, 404);
|
|
57
|
-
|
|
58
|
-
if (job.status === 'running') {
|
|
59
|
-
jobManager.cancel(id);
|
|
60
|
-
} else {
|
|
61
|
-
jobManager.remove(id);
|
|
62
|
-
}
|
|
63
|
-
return c.json({ ok: true });
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
/** GET /api/jobs/:id/stream — SSE stream of job events */
|
|
67
|
-
app.get('/:id/stream', (c) => {
|
|
68
|
-
const id = c.req.param('id');
|
|
69
|
-
const job = jobManager.get(id);
|
|
70
|
-
if (!job) return c.json({ error: 'Job not found' }, 404);
|
|
71
|
-
|
|
72
|
-
return streamSSE(c, async (stream) => {
|
|
73
|
-
// Send current state
|
|
74
|
-
await stream.writeSSE({
|
|
75
|
-
data: JSON.stringify({
|
|
76
|
-
type: 'state',
|
|
77
|
-
data: { ...job, logLines: (job.logLines ?? []).slice(-50) },
|
|
78
|
-
}),
|
|
79
|
-
event: 'message',
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
// Subscribe to future events
|
|
83
|
-
const unsubscribe = jobManager.subscribe(id, async (event) => {
|
|
84
|
-
try {
|
|
85
|
-
await stream.writeSSE({
|
|
86
|
-
data: JSON.stringify(event),
|
|
87
|
-
event: 'message',
|
|
88
|
-
});
|
|
89
|
-
} catch {
|
|
90
|
-
// Client disconnected
|
|
91
|
-
unsubscribe();
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
// Keep alive until client disconnects or job ends
|
|
96
|
-
while (true) {
|
|
97
|
-
const currentJob = jobManager.get(id);
|
|
98
|
-
if (!currentJob || currentJob.status !== 'running') {
|
|
99
|
-
// Send final state
|
|
100
|
-
await stream.writeSSE({
|
|
101
|
-
data: JSON.stringify({ type: 'done', data: currentJob }),
|
|
102
|
-
event: 'message',
|
|
103
|
-
});
|
|
104
|
-
break;
|
|
105
|
-
}
|
|
106
|
-
await stream.sleep(1000);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
unsubscribe();
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
export default app;
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { Hono } from 'hono';
|
|
2
|
-
import {
|
|
3
|
-
getCache,
|
|
4
|
-
getFileBlocks,
|
|
5
|
-
getFileCoverage,
|
|
6
|
-
getFileDetail,
|
|
7
|
-
getLangs,
|
|
8
|
-
getOverview,
|
|
9
|
-
getVersions,
|
|
10
|
-
rescan,
|
|
11
|
-
} from '../services/status';
|
|
12
|
-
|
|
13
|
-
const app = new Hono();
|
|
14
|
-
|
|
15
|
-
app.get('/', (c) => {
|
|
16
|
-
return c.json({
|
|
17
|
-
versions: getVersions(),
|
|
18
|
-
langs: ['en', ...getLangs()],
|
|
19
|
-
data: getOverview(),
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
app.get('/:version/:lang', (c) => {
|
|
24
|
-
const { version, lang } = c.req.param();
|
|
25
|
-
if (lang === 'en') {
|
|
26
|
-
const anyLang = getLangs()[0];
|
|
27
|
-
const coverage = getFileCoverage(version, anyLang);
|
|
28
|
-
return c.json(coverage.map((f) => ({ file: f.file, total: f.total, translated: f.total })));
|
|
29
|
-
}
|
|
30
|
-
return c.json(getFileCoverage(version, lang));
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
app.get('/:version/:lang/blocks', (c) => {
|
|
34
|
-
const { version, lang } = c.req.param();
|
|
35
|
-
const filePath = c.req.query('path');
|
|
36
|
-
if (!filePath) return c.json({ error: 'Missing path query param' }, 400);
|
|
37
|
-
const blocks = getFileBlocks(version, lang, filePath);
|
|
38
|
-
if (!blocks) return c.json({ error: 'File not found' }, 404);
|
|
39
|
-
return c.json({ file: filePath, lang, version, blocks });
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
app.delete('/:version/:lang/cache', (c) => {
|
|
43
|
-
const { lang } = c.req.param();
|
|
44
|
-
const key = c.req.query('key');
|
|
45
|
-
if (!key) return c.json({ error: 'Missing key query param' }, 400);
|
|
46
|
-
const cache = getCache();
|
|
47
|
-
cache.db.prepare('DELETE FROM translations WHERE lang = ? AND key = ?').run(lang, key);
|
|
48
|
-
return c.json({ deleted: key, lang });
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
app.post('/:version/rescan', (c) => {
|
|
52
|
-
const { version } = c.req.param();
|
|
53
|
-
const count = rescan(version);
|
|
54
|
-
return c.json({ version, files: count });
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
export default app;
|
package/src/admin/ui/App.tsx
DELETED
|
@@ -1,332 +0,0 @@
|
|
|
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: versionInfo } = useQuery({
|
|
156
|
-
queryKey: ['version'],
|
|
157
|
-
queryFn: api.version,
|
|
158
|
-
staleTime: Number.POSITIVE_INFINITY,
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
const { data: files } = useQuery({
|
|
162
|
-
queryKey: ['files', version, lang],
|
|
163
|
-
queryFn: () => api.fileCoverage(version, lang as string),
|
|
164
|
-
enabled: !!lang,
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
// ── Handlers ──
|
|
168
|
-
const handleSelectVersion = useCallback(
|
|
169
|
-
(v: string) => {
|
|
170
|
-
setVersion(v);
|
|
171
|
-
setSelected(new Set());
|
|
172
|
-
},
|
|
173
|
-
[setVersion],
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
const handleSelectLang = useCallback(
|
|
177
|
-
(l: string) => {
|
|
178
|
-
setLang(l);
|
|
179
|
-
setSelected(new Set());
|
|
180
|
-
},
|
|
181
|
-
[setLang],
|
|
182
|
-
);
|
|
183
|
-
|
|
184
|
-
const handleToggle = useCallback((f: string) => {
|
|
185
|
-
setSelected((prev) => {
|
|
186
|
-
const next = new Set(prev);
|
|
187
|
-
if (next.has(f)) next.delete(f);
|
|
188
|
-
else next.add(f);
|
|
189
|
-
return next;
|
|
190
|
-
});
|
|
191
|
-
}, []);
|
|
192
|
-
|
|
193
|
-
const handleSelectAll = useCallback(
|
|
194
|
-
(fileList: string[]) => setSelected(new Set(fileList)),
|
|
195
|
-
[],
|
|
196
|
-
);
|
|
197
|
-
|
|
198
|
-
const handleClear = useCallback(() => setSelected(new Set()), []);
|
|
199
|
-
|
|
200
|
-
const handleTranslateSelected = useCallback(() => {
|
|
201
|
-
setDialogFiles([...selected]);
|
|
202
|
-
setShowDialog(true);
|
|
203
|
-
}, [selected]);
|
|
204
|
-
|
|
205
|
-
const handleNewJob = useCallback(() => {
|
|
206
|
-
setDialogFiles(undefined);
|
|
207
|
-
setShowDialog(true);
|
|
208
|
-
}, []);
|
|
209
|
-
|
|
210
|
-
if (!statusData) return <div className="loading">Loading...</div>;
|
|
211
|
-
|
|
212
|
-
return (
|
|
213
|
-
<>
|
|
214
|
-
<nav>
|
|
215
|
-
<h1>🌐 Translation Admin {versionInfo?.version && <span className="version-badge">v{versionInfo.version}</span>}</h1>
|
|
216
|
-
<span className="spacer" />
|
|
217
|
-
<button type="button" className="btn" onClick={handleNewJob}>
|
|
218
|
-
+ New Job
|
|
219
|
-
</button>
|
|
220
|
-
<button
|
|
221
|
-
type="button"
|
|
222
|
-
className="btn btn-icon"
|
|
223
|
-
onClick={() => {
|
|
224
|
-
const next = theme === 'light' ? 'dark' : 'light';
|
|
225
|
-
setThemeState(next);
|
|
226
|
-
localStorage.setItem('theme', next);
|
|
227
|
-
}}
|
|
228
|
-
title="Toggle theme"
|
|
229
|
-
>
|
|
230
|
-
{theme === 'light' ? '🌙' : '☀️'}
|
|
231
|
-
</button>
|
|
232
|
-
</nav>
|
|
233
|
-
|
|
234
|
-
<div className="container">
|
|
235
|
-
{/* Version tabs */}
|
|
236
|
-
<div className="tabs">
|
|
237
|
-
{statusData.versions.map((v) => (
|
|
238
|
-
<button
|
|
239
|
-
key={v}
|
|
240
|
-
type="button"
|
|
241
|
-
className={`tab${v === version ? ' active' : ''}`}
|
|
242
|
-
onClick={() => handleSelectVersion(v)}
|
|
243
|
-
>
|
|
244
|
-
{v}
|
|
245
|
-
</button>
|
|
246
|
-
))}
|
|
247
|
-
</div>
|
|
248
|
-
|
|
249
|
-
{/* Language cards */}
|
|
250
|
-
<LangGrid
|
|
251
|
-
data={statusData}
|
|
252
|
-
version={version}
|
|
253
|
-
selectedLang={lang}
|
|
254
|
-
onSelect={handleSelectLang}
|
|
255
|
-
/>
|
|
256
|
-
|
|
257
|
-
{/* Jobs */}
|
|
258
|
-
<JobPanel />
|
|
259
|
-
|
|
260
|
-
{/* File list + Preview */}
|
|
261
|
-
{lang && files && (
|
|
262
|
-
<>
|
|
263
|
-
<div className="file-panel-toolbar">
|
|
264
|
-
<button
|
|
265
|
-
type="button"
|
|
266
|
-
className={`btn btn-sm${showFiles ? ' active' : ''}`}
|
|
267
|
-
onClick={() => setShowFiles(!showFiles)}
|
|
268
|
-
>
|
|
269
|
-
{showFiles ? '◀ Hide files' : '▶ Show files'}
|
|
270
|
-
</button>
|
|
271
|
-
{file && <span className="file-panel-current">{file}</span>}
|
|
272
|
-
</div>
|
|
273
|
-
<div
|
|
274
|
-
className={`file-panel${!showFiles ? ' no-list' : ''}${!file ? ' no-preview' : ''}`}
|
|
275
|
-
>
|
|
276
|
-
{showFiles && (
|
|
277
|
-
<FileList
|
|
278
|
-
files={files}
|
|
279
|
-
lang={lang}
|
|
280
|
-
activeFile={file}
|
|
281
|
-
selected={selected}
|
|
282
|
-
statusFilter={status}
|
|
283
|
-
sectionFilter={section}
|
|
284
|
-
onStatusFilter={setStatusFilter}
|
|
285
|
-
onSectionFilter={setSectionFilter}
|
|
286
|
-
onSelect={setFile}
|
|
287
|
-
onToggle={handleToggle}
|
|
288
|
-
onSelectAll={handleSelectAll}
|
|
289
|
-
onClear={handleClear}
|
|
290
|
-
onTranslateSelected={handleTranslateSelected}
|
|
291
|
-
/>
|
|
292
|
-
)}
|
|
293
|
-
{file && (
|
|
294
|
-
<Preview
|
|
295
|
-
version={version}
|
|
296
|
-
lang={lang}
|
|
297
|
-
file={file}
|
|
298
|
-
viewMode={view}
|
|
299
|
-
onViewMode={setView}
|
|
300
|
-
showToc={toc}
|
|
301
|
-
onToggleToc={() => setToc(!toc)}
|
|
302
|
-
showNodes={nodes}
|
|
303
|
-
onToggleNodes={() => setNodes(!nodes)}
|
|
304
|
-
onClose={() => setFile(null)}
|
|
305
|
-
/>
|
|
306
|
-
)}
|
|
307
|
-
</div>
|
|
308
|
-
</>
|
|
309
|
-
)}
|
|
310
|
-
</div>
|
|
311
|
-
|
|
312
|
-
{/* Toast */}
|
|
313
|
-
{toast && <div className="toast">{toast}</div>}
|
|
314
|
-
|
|
315
|
-
{/* Job dialog */}
|
|
316
|
-
{showDialog && (
|
|
317
|
-
<JobDialog
|
|
318
|
-
langs={statusData.langs}
|
|
319
|
-
versions={statusData.versions}
|
|
320
|
-
defaultLang={lang || undefined}
|
|
321
|
-
defaultVersion={version}
|
|
322
|
-
files={dialogFiles}
|
|
323
|
-
onClose={() => setShowDialog(false)}
|
|
324
|
-
onSuccess={(msg) => {
|
|
325
|
-
setShowDialog(false);
|
|
326
|
-
setToast(msg);
|
|
327
|
-
}}
|
|
328
|
-
/>
|
|
329
|
-
)}
|
|
330
|
-
</>
|
|
331
|
-
);
|
|
332
|
-
}
|
package/src/admin/ui/main.tsx
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
2
|
-
import { StrictMode } from 'react';
|
|
3
|
-
import { createRoot } from 'react-dom/client';
|
|
4
|
-
import { App } from './App';
|
|
5
|
-
import './styles.css';
|
|
6
|
-
|
|
7
|
-
const queryClient = new QueryClient({
|
|
8
|
-
defaultOptions: {
|
|
9
|
-
queries: { refetchOnWindowFocus: false, retry: 1 },
|
|
10
|
-
},
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
createRoot(document.getElementById('root')!).render(
|
|
14
|
-
<StrictMode>
|
|
15
|
-
<QueryClientProvider client={queryClient}>
|
|
16
|
-
<App />
|
|
17
|
-
</QueryClientProvider>
|
|
18
|
-
</StrictMode>,
|
|
19
|
-
);
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|