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
@@ -0,0 +1,416 @@
1
+ import { createFileRoute, useNavigate } from '@tanstack/react-router';
2
+ import { useQuery } from '@tanstack/react-query';
3
+ import { useCallback, useEffect, useMemo, useState } from 'react';
4
+ import { FileList } from '../components/FileList';
5
+ import { JobDialog } from '../components/JobDialog';
6
+ import { JobPanel } from '../components/JobPanel';
7
+ import { LangGrid } from '../components/LangGrid';
8
+ import { Preview } from '../components/Preview';
9
+ import { api } from '../lib/api';
10
+
11
+ type ViewMode = 'split' | 'en' | 'lang';
12
+ type StatusFilter = 'all' | 'complete' | 'partial' | 'missing';
13
+ type SectionFilter = 'all' | 'docs' | 'blog' | 'learn';
14
+
15
+ export interface AdminSearch {
16
+ project?: string;
17
+ v?: string;
18
+ lang?: string;
19
+ file?: string;
20
+ files?: string;
21
+ view?: ViewMode;
22
+ toc?: string;
23
+ nodes?: string;
24
+ status?: StatusFilter;
25
+ section?: SectionFilter;
26
+ }
27
+
28
+ /**
29
+ * Parse version keys into project/version structure.
30
+ * Multi-project keys look like "query/v5", single-project keys look like "v5".
31
+ */
32
+ function parseProjectVersions(versionKeys: string[]) {
33
+ const isMultiProject = versionKeys.some((k) => k.includes('/'));
34
+
35
+ if (!isMultiProject) {
36
+ return { projects: [{ id: '_default', versions: versionKeys }], isMultiProject: false };
37
+ }
38
+
39
+ const projectMap = new Map<string, string[]>();
40
+ for (const key of versionKeys) {
41
+ const slashIdx = key.indexOf('/');
42
+ const projectId = slashIdx >= 0 ? key.slice(0, slashIdx) : '_default';
43
+ const version = slashIdx >= 0 ? key.slice(slashIdx + 1) : key;
44
+ if (!projectMap.has(projectId)) projectMap.set(projectId, []);
45
+ projectMap.get(projectId)!.push(version);
46
+ }
47
+
48
+ return {
49
+ projects: Array.from(projectMap.entries()).map(([id, versions]) => ({ id, versions })),
50
+ isMultiProject: true,
51
+ };
52
+ }
53
+
54
+ export const Route = createFileRoute('/')({
55
+ validateSearch: (search: Record<string, unknown>): AdminSearch => ({
56
+ project: (search.project as string) || undefined,
57
+ v: (search.v as string) || undefined,
58
+ lang: (search.lang as string) || undefined,
59
+ file: (search.file as string) || undefined,
60
+ files: (search.files as string) || undefined,
61
+ view: (search.view as ViewMode) || undefined,
62
+ toc: (search.toc as string) || undefined,
63
+ nodes: (search.nodes as string) || undefined,
64
+ status: (search.status as StatusFilter) || undefined,
65
+ section: (search.section as SectionFilter) || undefined,
66
+ }),
67
+ component: AdminPage,
68
+ });
69
+
70
+ function AdminPage() {
71
+ const search = Route.useSearch();
72
+ const navigate = useNavigate({ from: '/' });
73
+
74
+ const lang = search.lang || null;
75
+ const file = search.file || null;
76
+ const showFiles = search.files !== '0';
77
+ const view: ViewMode = search.view || 'lang';
78
+ const toc = search.toc !== '0';
79
+ const nodes = search.nodes === '1';
80
+ const status: StatusFilter = search.status || 'all';
81
+ const section: SectionFilter = search.section || 'all';
82
+
83
+ const [theme, setThemeState] = useState(() => {
84
+ if (typeof window === 'undefined') return 'dark';
85
+ const saved = localStorage.getItem('theme');
86
+ return saved === 'light' ? 'light' : 'dark';
87
+ });
88
+ const [toast, setToast] = useState<string | null>(null);
89
+
90
+ useEffect(() => {
91
+ document.documentElement.dataset.theme = theme;
92
+ }, [theme]);
93
+
94
+ useEffect(() => {
95
+ if (!toast) return;
96
+ const t = setTimeout(() => setToast(null), 3000);
97
+ return () => clearTimeout(t);
98
+ }, [toast]);
99
+
100
+ const updateSearch = useCallback(
101
+ (updates: Partial<AdminSearch>) => {
102
+ navigate({
103
+ search: (prev: AdminSearch) => {
104
+ const next = { ...prev };
105
+ for (const [k, v] of Object.entries(updates)) {
106
+ if (v === null || v === undefined || v === '') {
107
+ delete (next as Record<string, unknown>)[k];
108
+ } else {
109
+ (next as Record<string, unknown>)[k] = v;
110
+ }
111
+ }
112
+ return next;
113
+ },
114
+ replace: true,
115
+ });
116
+ },
117
+ [navigate],
118
+ );
119
+
120
+ // ── URL setters ──
121
+ const setProject = useCallback(
122
+ (p: string | null) => updateSearch({ project: p || undefined, v: undefined, lang: undefined, file: undefined }),
123
+ [updateSearch],
124
+ );
125
+ const setVersion = useCallback(
126
+ (v: string) => updateSearch({ v: v || undefined, lang: undefined, file: undefined }),
127
+ [updateSearch],
128
+ );
129
+ const setLang = useCallback(
130
+ (l: string | null) => updateSearch({ lang: l || undefined }),
131
+ [updateSearch],
132
+ );
133
+ const setFile = useCallback(
134
+ (f: string | null) => updateSearch({ file: f || undefined }),
135
+ [updateSearch],
136
+ );
137
+ const setShowFiles = useCallback(
138
+ (show: boolean) => updateSearch({ files: show ? undefined : '0' }),
139
+ [updateSearch],
140
+ );
141
+ const setView = useCallback(
142
+ (m: ViewMode) => updateSearch({ view: m === 'lang' ? undefined : m }),
143
+ [updateSearch],
144
+ );
145
+ const setNodes = useCallback(
146
+ (show: boolean) => updateSearch({ nodes: show ? '1' : undefined }),
147
+ [updateSearch],
148
+ );
149
+ const setToc = useCallback(
150
+ (show: boolean) => updateSearch({ toc: show ? undefined : '0' }),
151
+ [updateSearch],
152
+ );
153
+ const setStatusFilter = useCallback(
154
+ (s: StatusFilter) => updateSearch({ status: s === 'all' ? undefined : s }),
155
+ [updateSearch],
156
+ );
157
+ const setSectionFilter = useCallback(
158
+ (s: SectionFilter) => updateSearch({ section: s === 'all' ? undefined : s }),
159
+ [updateSearch],
160
+ );
161
+
162
+ // ── Non-URL state ──
163
+ const [selected, setSelected] = useState<Set<string>>(new Set());
164
+ const [showDialog, setShowDialog] = useState(false);
165
+ const [dialogFiles, setDialogFiles] = useState<string[] | undefined>();
166
+
167
+ // ── Queries ──
168
+ const { data: statusData } = useQuery({
169
+ queryKey: ['status'],
170
+ queryFn: api.status,
171
+ });
172
+
173
+ const { data: versionInfo } = useQuery({
174
+ queryKey: ['version'],
175
+ queryFn: api.version,
176
+ staleTime: Number.POSITIVE_INFINITY,
177
+ });
178
+
179
+ // ── Derive project/version structure ──
180
+ const { projectList, isMultiProject, activeProject, activeVersions, version } = useMemo(() => {
181
+ if (!statusData) {
182
+ return { projectList: [], isMultiProject: false, activeProject: null, activeVersions: [] as string[], version: 'latest' };
183
+ }
184
+ const parsed = parseProjectVersions(statusData.versions);
185
+ const projectList = parsed.projects;
186
+ const isMultiProject = parsed.isMultiProject;
187
+
188
+ // Determine active project
189
+ let activeProject = isMultiProject
190
+ ? (search.project || projectList[0]?.id || null)
191
+ : null;
192
+ // Validate the project exists
193
+ if (isMultiProject && activeProject && !projectList.find((p) => p.id === activeProject)) {
194
+ activeProject = projectList[0]?.id || null;
195
+ }
196
+
197
+ // Determine versions for active project
198
+ const activeVersions = isMultiProject
199
+ ? (projectList.find((p) => p.id === activeProject)?.versions || [])
200
+ : statusData.versions;
201
+
202
+ // Determine active version — for multi-project, version key is "project/version"
203
+ const rawVersion = search.v || activeVersions[0] || 'latest';
204
+ const version = isMultiProject && activeProject
205
+ ? `${activeProject}/${rawVersion}`
206
+ : rawVersion;
207
+
208
+ return { projectList, isMultiProject, activeProject, activeVersions, version };
209
+ }, [statusData, search.project, search.v]);
210
+
211
+ // Short version (without project prefix) for display
212
+ const displayVersion = isMultiProject && activeProject && version.startsWith(`${activeProject}/`)
213
+ ? version.slice(activeProject.length + 1)
214
+ : version;
215
+
216
+ const { data: files } = useQuery({
217
+ queryKey: ['files', version, lang],
218
+ queryFn: () => api.fileCoverage(version, lang as string),
219
+ enabled: !!lang,
220
+ });
221
+
222
+ // ── Handlers ──
223
+ const handleSelectProject = useCallback(
224
+ (p: string) => {
225
+ setProject(p);
226
+ setSelected(new Set());
227
+ },
228
+ [setProject],
229
+ );
230
+
231
+ const handleSelectVersion = useCallback(
232
+ (v: string) => {
233
+ setVersion(v);
234
+ setSelected(new Set());
235
+ },
236
+ [setVersion],
237
+ );
238
+
239
+ const handleSelectLang = useCallback(
240
+ (l: string) => {
241
+ setLang(l);
242
+ setSelected(new Set());
243
+ },
244
+ [setLang],
245
+ );
246
+
247
+ const handleToggle = useCallback((f: string) => {
248
+ setSelected((prev) => {
249
+ const next = new Set(prev);
250
+ if (next.has(f)) next.delete(f);
251
+ else next.add(f);
252
+ return next;
253
+ });
254
+ }, []);
255
+
256
+ const handleSelectAll = useCallback(
257
+ (fileList: string[]) => setSelected(new Set(fileList)),
258
+ [],
259
+ );
260
+
261
+ const handleClear = useCallback(() => setSelected(new Set()), []);
262
+
263
+ const handleTranslateSelected = useCallback(() => {
264
+ setDialogFiles([...selected]);
265
+ setShowDialog(true);
266
+ }, [selected]);
267
+
268
+ const handleNewJob = useCallback(() => {
269
+ setDialogFiles(undefined);
270
+ setShowDialog(true);
271
+ }, []);
272
+
273
+ if (!statusData) return <div className="loading">Loading...</div>;
274
+
275
+ return (
276
+ <>
277
+ <nav>
278
+ <h1>
279
+ {'🌐 Translation Admin '}
280
+ {versionInfo?.version && (
281
+ <span className="version-badge">v{versionInfo.version}</span>
282
+ )}
283
+ </h1>
284
+ <span className="spacer" />
285
+ <button type="button" className="btn" onClick={handleNewJob}>
286
+ + New Job
287
+ </button>
288
+ <button
289
+ type="button"
290
+ className="btn btn-icon"
291
+ onClick={() => {
292
+ const next = theme === 'light' ? 'dark' : 'light';
293
+ setThemeState(next);
294
+ localStorage.setItem('theme', next);
295
+ }}
296
+ title="Toggle theme"
297
+ >
298
+ {theme === 'light' ? '🌙' : '☀️'}
299
+ </button>
300
+ </nav>
301
+
302
+ <div className="container">
303
+ {/* Project tabs (multi-project only) */}
304
+ {isMultiProject && (
305
+ <div className="project-tabs">
306
+ {projectList.map((p) => (
307
+ <button
308
+ key={p.id}
309
+ type="button"
310
+ className={`project-tab${p.id === activeProject ? ' active' : ''}`}
311
+ onClick={() => handleSelectProject(p.id)}
312
+ >
313
+ {p.id}
314
+ </button>
315
+ ))}
316
+ </div>
317
+ )}
318
+
319
+ {/* Version tabs */}
320
+ <div className="tabs">
321
+ {activeVersions.map((v) => (
322
+ <button
323
+ key={v}
324
+ type="button"
325
+ className={`tab${v === displayVersion ? ' active' : ''}`}
326
+ onClick={() => handleSelectVersion(v)}
327
+ >
328
+ {v}
329
+ </button>
330
+ ))}
331
+ </div>
332
+
333
+ {/* Language cards */}
334
+ <LangGrid
335
+ data={statusData}
336
+ version={version}
337
+ selectedLang={lang}
338
+ onSelect={handleSelectLang}
339
+ />
340
+
341
+ {/* Jobs */}
342
+ <JobPanel />
343
+
344
+ {/* File list + Preview */}
345
+ {lang && files && (
346
+ <>
347
+ <div className="file-panel-toolbar">
348
+ <button
349
+ type="button"
350
+ className={`btn btn-sm${showFiles ? ' active' : ''}`}
351
+ onClick={() => setShowFiles(!showFiles)}
352
+ >
353
+ {showFiles ? '◀ Hide files' : '▶ Show files'}
354
+ </button>
355
+ {file && <span className="file-panel-current">{file}</span>}
356
+ </div>
357
+ <div
358
+ className={`file-panel${!showFiles ? ' no-list' : ''}${!file ? ' no-preview' : ''}`}
359
+ >
360
+ {showFiles && (
361
+ <FileList
362
+ files={files}
363
+ lang={lang}
364
+ activeFile={file}
365
+ selected={selected}
366
+ statusFilter={status}
367
+ sectionFilter={section}
368
+ onStatusFilter={setStatusFilter}
369
+ onSectionFilter={setSectionFilter}
370
+ onSelect={setFile}
371
+ onToggle={handleToggle}
372
+ onSelectAll={handleSelectAll}
373
+ onClear={handleClear}
374
+ onTranslateSelected={handleTranslateSelected}
375
+ />
376
+ )}
377
+ {file && (
378
+ <Preview
379
+ version={version}
380
+ lang={lang}
381
+ file={file}
382
+ viewMode={view}
383
+ onViewMode={setView}
384
+ showToc={toc}
385
+ onToggleToc={() => setToc(!toc)}
386
+ showNodes={nodes}
387
+ onToggleNodes={() => setNodes(!nodes)}
388
+ onClose={() => setFile(null)}
389
+ />
390
+ )}
391
+ </div>
392
+ </>
393
+ )}
394
+ </div>
395
+
396
+ {/* Toast */}
397
+ {toast && <div className="toast">{toast}</div>}
398
+
399
+ {/* Job dialog */}
400
+ {showDialog && (
401
+ <JobDialog
402
+ langs={statusData.langs}
403
+ versions={statusData.versions}
404
+ defaultLang={lang || undefined}
405
+ defaultVersion={version}
406
+ files={dialogFiles}
407
+ onClose={() => setShowDialog(false)}
408
+ onSuccess={(msg) => {
409
+ setShowDialog(false);
410
+ setToast(msg);
411
+ }}
412
+ />
413
+ )}
414
+ </>
415
+ );
416
+ }
@@ -148,7 +148,37 @@ nav h1 {
148
148
  }
149
149
  }
150
150
 
151
- /* Tabs */
151
+ /* Project tabs */
152
+ .project-tabs {
153
+ display: flex;
154
+ gap: 0.25rem;
155
+ margin-bottom: 0.75rem;
156
+ border-bottom: 1px solid var(--border);
157
+ padding-bottom: 0.75rem;
158
+ }
159
+ .project-tab {
160
+ padding: 0.4rem 1rem;
161
+ border-radius: 9999px;
162
+ border: 1px solid var(--border);
163
+ background: transparent;
164
+ color: var(--fg2);
165
+ cursor: pointer;
166
+ font-size: 0.8rem;
167
+ font-weight: 500;
168
+ text-transform: capitalize;
169
+ transition: background 0.15s, color 0.15s, border-color 0.15s;
170
+ }
171
+ .project-tab:hover {
172
+ background: var(--hover);
173
+ color: var(--fg);
174
+ }
175
+ .project-tab.active {
176
+ background: var(--accent);
177
+ color: var(--btn-fg);
178
+ border-color: var(--accent);
179
+ }
180
+
181
+ /* Version tabs */
152
182
  .tabs {
153
183
  display: flex;
154
184
  gap: 0.5rem;
@@ -272,6 +302,8 @@ nav h1 {
272
302
  background: var(--border);
273
303
  border-radius: 2px;
274
304
  margin-top: 0.5rem;
305
+ overflow: hidden;
306
+ max-width: 100%;
275
307
  }
276
308
  .progress-fill {
277
309
  height: 100%;
@@ -1088,9 +1120,10 @@ a.preview-filename:hover {
1088
1120
  position: relative;
1089
1121
  }
1090
1122
  .log-copy {
1091
- position: absolute;
1123
+ position: sticky;
1092
1124
  top: 0.25rem;
1093
- right: 0.25rem;
1125
+ float: right;
1126
+ margin-right: 0.25rem;
1094
1127
  background: var(--card);
1095
1128
  border: 1px solid var(--border);
1096
1129
  border-radius: 0.25rem;
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@docs-i18n/admin",
3
+ "version": "0.6.3",
4
+ "private": true,
5
+ "type": "module",
6
+ "exports": {
7
+ "./server": "./server/index.ts"
8
+ },
9
+ "scripts": {
10
+ "dev": "vite dev",
11
+ "build": "vite build"
12
+ },
13
+ "dependencies": {
14
+ "docs-i18n": "workspace:*",
15
+ "hono": "^4.12.0",
16
+ "react": "^19.1.0",
17
+ "react-dom": "^19.1.0",
18
+ "vite": "^8.0.1",
19
+ "@vitejs/plugin-react": "^6.0.1"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^22.0.0",
23
+ "@types/react": "^19.2.14",
24
+ "@types/react-dom": "^19.2.3",
25
+ "typescript": "^5.8.0"
26
+ }
27
+ }
@@ -0,0 +1,53 @@
1
+ import { createServerFn } from '@tanstack/react-start';
2
+ import { jobManager } from '../services/job-manager';
3
+ import { ensureInit } from '../init';
4
+
5
+ export const fetchJobs = createServerFn({ method: 'GET' }).handler(async () => {
6
+ await ensureInit();
7
+ return jobManager.list().map((j) => ({
8
+ ...j,
9
+ logLines: (j.logLines ?? []).slice(-20),
10
+ }));
11
+ });
12
+
13
+ export const createJob = createServerFn({ method: 'POST' })
14
+ .inputValidator(
15
+ (d: {
16
+ lang: string;
17
+ version: string;
18
+ max?: number;
19
+ concurrency?: number;
20
+ model?: string;
21
+ modelRotate?: string[];
22
+ md5?: boolean;
23
+ files?: string[];
24
+ }) => d,
25
+ )
26
+ .handler(async ({ data }) => {
27
+ await ensureInit();
28
+ if (!data.lang || !data.version) {
29
+ throw new Error('Missing lang or version');
30
+ }
31
+ return jobManager.start(data);
32
+ });
33
+
34
+ export const fetchJob = createServerFn({ method: 'GET' })
35
+ .inputValidator((d: { id: string }) => d)
36
+ .handler(async ({ data }) => {
37
+ const job = jobManager.get(data.id);
38
+ if (!job) throw new Error('Job not found');
39
+ return job;
40
+ });
41
+
42
+ export const deleteJob = createServerFn({ method: 'POST' })
43
+ .inputValidator((d: { id: string }) => d)
44
+ .handler(async ({ data }) => {
45
+ const job = jobManager.get(data.id);
46
+ if (!job) throw new Error('Job not found');
47
+ if (job.status === 'running') {
48
+ jobManager.cancel(data.id);
49
+ } else {
50
+ jobManager.remove(data.id);
51
+ }
52
+ return { ok: true };
53
+ });
@@ -0,0 +1,84 @@
1
+ import { createServerFn } from '@tanstack/react-start';
2
+ import { readFileSync, existsSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import { spawn as nodeSpawn } from 'node:child_process';
5
+ import { getPackageVersion } from 'docs-i18n';
6
+ import { ensureInit, getConfig } from '../init';
7
+
8
+ export const fetchVersion = createServerFn({ method: 'GET' }).handler(async () => {
9
+ return { version: getPackageVersion() };
10
+ });
11
+
12
+ export const fetchLlmConfig = createServerFn({ method: 'GET' }).handler(async () => {
13
+ await ensureInit();
14
+ const config = getConfig();
15
+ const llm = config?.llm;
16
+
17
+ // Read .env from project root for env vars not yet in process.env
18
+ let envVars: Record<string, string> = {};
19
+ try {
20
+ const { getProjectRoot } = await import('../init');
21
+ const projectRoot = getProjectRoot();
22
+ const envPath = resolve(projectRoot, '.env');
23
+ if (existsSync(envPath)) {
24
+ const envContent = readFileSync(envPath, 'utf-8');
25
+ for (const line of envContent.split('\n')) {
26
+ const match = line.match(/^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*)\s*$/);
27
+ if (match) envVars[match[1]] = match[2];
28
+ }
29
+ }
30
+ } catch {}
31
+
32
+ // Check if API key is available (from config, env, or .env file)
33
+ const hasApiKey = !!(
34
+ llm?.apiKey ||
35
+ process.env.OPENROUTER_API_KEY || process.env.OPENAI_API_KEY ||
36
+ envVars.OPENROUTER_API_KEY || envVars.OPENAI_API_KEY
37
+ );
38
+
39
+ // Resolve the effective model: config > env > .env > hardcoded default
40
+ const DEFAULT_MODEL = 'deepseek/deepseek-chat-v3-0324:free';
41
+ const effectiveModel =
42
+ llm?.model ||
43
+ process.env.OPENROUTER_MODEL ||
44
+ envVars.OPENROUTER_MODEL ||
45
+ DEFAULT_MODEL;
46
+
47
+ const effectiveProvider = llm?.provider || 'openrouter';
48
+
49
+ return {
50
+ model: effectiveModel,
51
+ provider: effectiveProvider,
52
+ hasApiKey,
53
+ };
54
+ });
55
+
56
+ export const fetchConfig = createServerFn({ method: 'GET' }).handler(async () => {
57
+ return { projectRoot: process.cwd() };
58
+ });
59
+
60
+ export const openFile = createServerFn({ method: 'POST' })
61
+ .inputValidator((d: { file: string }) => d)
62
+ .handler(async ({ data }) => {
63
+ const { file } = data;
64
+ if (!file) throw new Error('Missing file');
65
+ const fullPath = resolve(process.cwd(), file);
66
+ if (!fullPath.startsWith(process.cwd())) throw new Error('Invalid path');
67
+
68
+ const candidates = process.env.EDITOR_CMD ? [process.env.EDITOR_CMD] : ['code', 'cursor', 'zed'];
69
+ for (const cmd of candidates) {
70
+ const found = await new Promise<boolean>((r) => {
71
+ const p = nodeSpawn('which', [cmd], { stdio: 'ignore' });
72
+ p.on('exit', (code) => r(code === 0));
73
+ p.on('error', () => r(false));
74
+ });
75
+ if (found) {
76
+ nodeSpawn(cmd, [fullPath], { stdio: 'ignore', detached: true }).unref();
77
+ return { opened: fullPath, editor: cmd };
78
+ }
79
+ }
80
+ const platformOpeners: Record<string, string> = { darwin: 'open', win32: 'start' };
81
+ const fallback = platformOpeners[process.platform] || 'xdg-open';
82
+ nodeSpawn(fallback, [fullPath], { stdio: 'ignore', detached: true }).unref();
83
+ return { opened: fullPath, editor: fallback };
84
+ });
@@ -1,6 +1,4 @@
1
- import { Hono } from 'hono';
2
-
3
- const app = new Hono();
1
+ import { createServerFn } from '@tanstack/react-start';
4
2
 
5
3
  interface OpenRouterModel {
6
4
  id: string;
@@ -27,30 +25,6 @@ let cachedResult: ReturnType<typeof formatModels> | null = null;
27
25
  let cacheTime = 0;
28
26
  const CACHE_TTL = 5 * 60 * 1000;
29
27
 
30
- /** GET /api/models — List OpenRouter models */
31
- app.get('/', async (c) => {
32
- const now = Date.now();
33
- if (cachedResult && now - cacheTime < CACHE_TTL) {
34
- return c.json(cachedResult);
35
- }
36
-
37
- try {
38
- const res = await fetch('https://openrouter.ai/api/v1/models');
39
- if (!res.ok) {
40
- return c.json({ error: `OpenRouter API error: ${res.status}` }, 502);
41
- }
42
- const { data } = (await res.json()) as { data: OpenRouterModel[] };
43
- cachedResult = formatModels(data);
44
- cacheTime = now;
45
- return c.json(cachedResult);
46
- } catch (err) {
47
- return c.json(
48
- { error: err instanceof Error ? err.message : 'Failed to fetch models' },
49
- 500,
50
- );
51
- }
52
- });
53
-
54
28
  function formatModels(models: OpenRouterModel[]) {
55
29
  return models
56
30
  .filter((m) => {
@@ -58,7 +32,6 @@ function formatModels(models: OpenRouterModel[]) {
58
32
  const pp = Number.parseFloat(m.pricing.prompt);
59
33
  const cp = Number.parseFloat(m.pricing.completion);
60
34
  if (pp < 0 || cp < 0) return false;
61
- // Only text→text models
62
35
  if (!m.architecture?.modality?.startsWith('text')) return false;
63
36
  if (!m.architecture.output_modalities?.includes('text')) return false;
64
37
  return true;
@@ -84,4 +57,18 @@ function formatModels(models: OpenRouterModel[]) {
84
57
  .sort((a, b) => a.promptPrice - b.promptPrice);
85
58
  }
86
59
 
87
- export default app;
60
+ export const fetchModels = createServerFn({ method: 'GET' }).handler(async () => {
61
+ const now = Date.now();
62
+ if (cachedResult && now - cacheTime < CACHE_TTL) {
63
+ return cachedResult;
64
+ }
65
+
66
+ const res = await fetch('https://openrouter.ai/api/v1/models');
67
+ if (!res.ok) {
68
+ throw new Error(`OpenRouter API error: ${res.status}`);
69
+ }
70
+ const { data } = (await res.json()) as { data: OpenRouterModel[] };
71
+ cachedResult = formatModels(data);
72
+ cacheTime = now;
73
+ return cachedResult;
74
+ });