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.
Files changed (169) hide show
  1. package/{src/admin/ui → admin/app}/components/JobDialog.tsx +21 -2
  2. package/{src/admin/ui → admin/app}/components/JobPanel.tsx +1 -1
  3. package/{src/admin/ui → admin/app}/components/Preview.tsx +2 -5
  4. package/{src/admin/ui → admin/app}/lib/api.ts +18 -39
  5. package/admin/app/routeTree.gen.ts +68 -0
  6. package/admin/app/router.tsx +23 -0
  7. package/admin/app/routes/__root.tsx +55 -0
  8. package/admin/app/routes/index.tsx +416 -0
  9. package/{src/admin/ui → admin/app}/styles.css +36 -3
  10. package/admin/package.json +27 -0
  11. package/admin/server/functions/jobs.ts +53 -0
  12. package/admin/server/functions/misc.ts +84 -0
  13. package/{src/admin/server/routes → admin/server/functions}/models.ts +16 -29
  14. package/admin/server/functions/status.ts +61 -0
  15. package/admin/server/index.ts +35 -0
  16. package/admin/server/init.ts +46 -0
  17. package/{src/admin → admin}/server/services/job-manager.ts +39 -10
  18. package/{src/admin → admin}/server/services/status.ts +6 -6
  19. package/admin/tsconfig.json +19 -0
  20. package/{src/admin → admin}/vite.config.ts +8 -2
  21. package/dist/{assemble-7H4QCW35.js → assemble-CP2BRYQJ.js} +6 -4
  22. package/dist/{chunk-A3YQNPKZ.js → chunk-CLYUAWZE.js} +1 -1
  23. package/dist/{chunk-YN4VJHCQ.js → chunk-JHBSHTXC.js} +1 -1
  24. package/dist/chunk-L64GJ4OB.js +32 -0
  25. package/dist/{chunk-SKKZIV3L.js → chunk-PNKVD2UK.js} +1 -29
  26. package/dist/{chunk-XEOYZUHS.js → chunk-QKIR7RKQ.js} +4 -31
  27. package/dist/chunk-TRURQFP4.js +31 -0
  28. package/dist/cli.js +108 -7
  29. package/dist/index.d.ts +41 -1
  30. package/dist/index.js +92 -3
  31. package/dist/{rescan-O5D3CYC2.js → rescan-HXMWFAOC.js} +5 -3
  32. package/dist/{status-F4MYIAAY.js → status-AGZDXOTZ.js} +4 -2
  33. package/dist/{translate-ZIVKNAC4.js → translate-A5X6MX4Y.js} +14 -7
  34. package/dist/upload-XL6KG6S2.js +132 -0
  35. package/package.json +17 -15
  36. package/template/app/components/BlogArticle.tsx +159 -0
  37. package/template/app/components/BlogList.tsx +88 -0
  38. package/template/app/components/Breadcrumbs.tsx +81 -0
  39. package/template/app/components/Card.tsx +31 -0
  40. package/template/app/components/Doc.tsx +191 -0
  41. package/template/app/components/DocBreadcrumb.tsx +60 -0
  42. package/template/app/components/DocContainer.tsx +13 -0
  43. package/template/app/components/DocTitle.tsx +11 -0
  44. package/template/app/components/DocsLayout.tsx +715 -0
  45. package/template/app/components/Dropdown.tsx +116 -0
  46. package/template/app/components/FallbackBanner.tsx +36 -0
  47. package/template/app/components/Footer.tsx +29 -0
  48. package/template/app/components/FrameworkSelect.tsx +150 -0
  49. package/template/app/components/LibraryCard.tsx +178 -0
  50. package/template/app/components/LocaleSwitcher.tsx +43 -0
  51. package/template/app/components/Navbar.tsx +430 -0
  52. package/template/app/components/PostNotFound.tsx +20 -0
  53. package/template/app/components/SearchButton.tsx +32 -0
  54. package/template/app/components/Select.tsx +103 -0
  55. package/template/app/components/Spinner.tsx +18 -0
  56. package/template/app/components/ThemeProvider.tsx +141 -0
  57. package/template/app/components/ThemeToggle.tsx +31 -0
  58. package/template/app/components/Toc.tsx +86 -0
  59. package/template/app/components/VersionSelect.tsx +118 -0
  60. package/template/app/components/icons/BSkyIcon.tsx +27 -0
  61. package/template/app/components/icons/BaseballCapIcon.tsx +25 -0
  62. package/template/app/components/icons/BrandXIcon.tsx +28 -0
  63. package/template/app/components/icons/CheckCircleIcon.tsx +28 -0
  64. package/template/app/components/icons/CogsIcon.tsx +25 -0
  65. package/template/app/components/icons/DiscordIcon.tsx +24 -0
  66. package/template/app/components/icons/GithubIcon.tsx +24 -0
  67. package/template/app/components/icons/GoogleIcon.tsx +24 -0
  68. package/template/app/components/icons/InstagramIcon.tsx +24 -0
  69. package/template/app/components/icons/NpmIcon.tsx +26 -0
  70. package/template/app/components/icons/YinYangIcon.tsx +26 -0
  71. package/template/app/components/icons/YouTubeIcon.tsx +24 -0
  72. package/template/app/components/markdown/CodeBlock.tsx +254 -0
  73. package/template/app/components/markdown/FileTabs.tsx +58 -0
  74. package/template/app/components/markdown/FrameworkContent.tsx +76 -0
  75. package/template/app/components/markdown/Markdown.tsx +216 -0
  76. package/template/app/components/markdown/MarkdownContent.tsx +89 -0
  77. package/template/app/components/markdown/MarkdownFrameworkHandler.tsx +66 -0
  78. package/template/app/components/markdown/MarkdownHeadingContext.tsx +35 -0
  79. package/template/app/components/markdown/MarkdownLink.tsx +46 -0
  80. package/template/app/components/markdown/MarkdownTabsHandler.tsx +109 -0
  81. package/template/app/components/markdown/PackageManagerTabs.tsx +95 -0
  82. package/template/app/components/markdown/Tabs.tsx +139 -0
  83. package/template/app/components/markdown/index.ts +15 -0
  84. package/template/app/components/ui/Button.tsx +141 -0
  85. package/template/app/components/ui/InlineCode.tsx +16 -0
  86. package/template/app/components/ui/MarkdownImg.tsx +21 -0
  87. package/template/app/config/frameworks.ts +93 -0
  88. package/template/app/contexts/SearchContext.tsx +36 -0
  89. package/template/app/db/index.ts +17 -0
  90. package/template/app/db/schema.ts +74 -0
  91. package/template/app/hooks/useClickOutside.ts +106 -0
  92. package/template/app/routeTree.gen.ts +584 -0
  93. package/template/app/router.tsx +29 -0
  94. package/template/app/routes/$lang.$project.$version.docs.$.tsx +128 -0
  95. package/template/app/routes/$lang.$project.$version.docs.framework.$framework.$.tsx +106 -0
  96. package/template/app/routes/$lang.$project.$version.docs.framework.$framework.index.tsx +27 -0
  97. package/template/app/routes/$lang.$project.$version.docs.framework.index.tsx +44 -0
  98. package/template/app/routes/$lang.$project.$version.docs.index.tsx +27 -0
  99. package/template/app/routes/$lang.$project.$version.docs.tsx +70 -0
  100. package/template/app/routes/$lang.$project.$version.tsx +69 -0
  101. package/template/app/routes/$lang.$project.docs.$.tsx +104 -0
  102. package/template/app/routes/$lang.$project.docs.index.tsx +20 -0
  103. package/template/app/routes/$lang.$project.docs.tsx +79 -0
  104. package/template/app/routes/$lang.$project.tsx +89 -0
  105. package/template/app/routes/$lang.blog.$.tsx +82 -0
  106. package/template/app/routes/$lang.blog.index.tsx +56 -0
  107. package/template/app/routes/$lang.blog.tsx +26 -0
  108. package/template/app/routes/$lang.docs.$.tsx +100 -0
  109. package/template/app/routes/$lang.docs.framework.$framework.$.tsx +104 -0
  110. package/template/app/routes/$lang.docs.framework.$framework.index.tsx +32 -0
  111. package/template/app/routes/$lang.docs.framework.index.tsx +47 -0
  112. package/template/app/routes/$lang.docs.index.tsx +20 -0
  113. package/template/app/routes/$lang.docs.tsx +90 -0
  114. package/template/app/routes/$lang.tsx +16 -0
  115. package/template/app/routes/__root.tsx +180 -0
  116. package/template/app/routes/index.tsx +89 -0
  117. package/template/app/site.config.ts +182 -0
  118. package/template/app/styles/app.css +1029 -0
  119. package/template/app/types/index.ts +77 -0
  120. package/template/app/utils/blog.server.ts +193 -0
  121. package/template/app/utils/blog.ts +42 -0
  122. package/template/app/utils/config.ts +120 -0
  123. package/template/app/utils/content-loader.ts +400 -0
  124. package/template/app/utils/dates.ts +29 -0
  125. package/template/app/utils/docs.server.ts +150 -0
  126. package/template/app/utils/markdown/filterFrameworkContent.ts +233 -0
  127. package/template/app/utils/markdown/index.ts +2 -0
  128. package/template/app/utils/markdown/installCommand.ts +143 -0
  129. package/template/app/utils/markdown/plugins/collectHeadings.ts +104 -0
  130. package/template/app/utils/markdown/plugins/extractCodeMeta.ts +57 -0
  131. package/template/app/utils/markdown/plugins/helpers.ts +33 -0
  132. package/template/app/utils/markdown/plugins/index.ts +8 -0
  133. package/template/app/utils/markdown/plugins/parseCommentComponents.ts +103 -0
  134. package/template/app/utils/markdown/plugins/transformCommentComponents.ts +23 -0
  135. package/template/app/utils/markdown/plugins/transformFrameworkComponent.ts +217 -0
  136. package/template/app/utils/markdown/plugins/transformTabsComponent.ts +359 -0
  137. package/template/app/utils/markdown/processor.ts +75 -0
  138. package/template/app/utils/site-config.tsx +11 -0
  139. package/template/app/utils/upload.ts +232 -0
  140. package/template/app/utils/useLocalStorage.ts +65 -0
  141. package/template/app/utils/utils.ts +23 -0
  142. package/template/package.json +54 -0
  143. package/template/public/favicon.svg +1 -0
  144. package/template/public/fonts/Inter-latin-ext.woff2 +0 -0
  145. package/template/public/fonts/Inter-latin.woff2 +0 -0
  146. package/template/public/images/frameworks/angular-logo.svg +1 -0
  147. package/template/public/images/frameworks/js-logo.svg +1 -0
  148. package/template/public/images/frameworks/lit-logo.svg +1 -0
  149. package/template/public/images/frameworks/preact-logo.svg +6 -0
  150. package/template/public/images/frameworks/qwik-logo.svg +1 -0
  151. package/template/public/images/frameworks/react-logo.svg +1 -0
  152. package/template/public/images/frameworks/solid-logo.svg +1 -0
  153. package/template/public/images/frameworks/svelte-logo.svg +1 -0
  154. package/template/public/images/frameworks/vue-logo.svg +4 -0
  155. package/template/tsconfig.json +24 -0
  156. package/template/vite.config.ts +43 -0
  157. package/template/wrangler.jsonc +16 -0
  158. package/README.md +0 -161
  159. package/dist/server-73AVSOL5.js +0 -598
  160. package/src/admin/index.html +0 -13
  161. package/src/admin/server/index.ts +0 -138
  162. package/src/admin/server/routes/jobs.ts +0 -113
  163. package/src/admin/server/routes/status.ts +0 -57
  164. package/src/admin/ui/App.tsx +0 -332
  165. package/src/admin/ui/main.tsx +0 -19
  166. /package/{src/admin/ui → admin/app}/components/FileList.tsx +0 -0
  167. /package/{src/admin/ui → admin/app}/components/LangGrid.tsx +0 -0
  168. /package/{src/admin/ui → admin/app}/components/ProgressBar.tsx +0 -0
  169. /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;
@@ -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
- }
@@ -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