docs-i18n 0.7.4 → 0.7.5

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.
@@ -1,1574 +0,0 @@
1
- import { n as TSS_SERVER_FUNCTION, r as getServerFnById, t as createServerFn } from "../server.js";
2
- import { t as Route } from "./routes-C2UFxDWZ.js";
3
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
- import { useNavigate } from "@tanstack/react-router";
5
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
- import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
7
- //#region app/lib/flags.ts
8
- var FLAGS = {
9
- en: "🇺🇸",
10
- "zh-hans": "🇨🇳",
11
- "zh-hant": "🇭🇰",
12
- ja: "🇯🇵",
13
- ar: "🇸🇦",
14
- de: "🇩🇪",
15
- es: "🇪🇸",
16
- fr: "🇫🇷",
17
- ru: "🇷🇺"
18
- };
19
- function pctColor(pct) {
20
- if (pct >= 95) return "var(--green)";
21
- if (pct > 50) return "var(--yellow)";
22
- return "var(--red)";
23
- }
24
- function statusIcon(pct) {
25
- if (pct >= 100) return "🟢";
26
- if (pct > 0) return "🟡";
27
- return "🔴";
28
- }
29
- function getSection(file) {
30
- if (file.startsWith("docs/")) return "docs";
31
- if (file.startsWith("blog/")) return "blog";
32
- if (file.startsWith("learn/")) return "learn";
33
- return "other";
34
- }
35
- //#endregion
36
- //#region app/components/FileList.tsx
37
- function filePct(f) {
38
- return f.total > 0 ? f.translated / f.total * 100 : 100;
39
- }
40
- function fileStatus(f) {
41
- const pct = filePct(f);
42
- if (pct >= 100) return "complete";
43
- if (pct > 0) return "partial";
44
- return "missing";
45
- }
46
- function buildTree(files) {
47
- const root = {
48
- name: "",
49
- path: "",
50
- children: /* @__PURE__ */ new Map()
51
- };
52
- for (const f of files) {
53
- const parts = f.file.split("/");
54
- let node = root;
55
- for (let i = 0; i < parts.length; i++) {
56
- const part = parts[i];
57
- if (!node.children.has(part)) node.children.set(part, {
58
- name: part,
59
- path: parts.slice(0, i + 1).join("/"),
60
- children: /* @__PURE__ */ new Map()
61
- });
62
- node = node.children.get(part);
63
- }
64
- node.file = f;
65
- }
66
- return root;
67
- }
68
- function dirPct(node) {
69
- let translated = 0;
70
- let total = 0;
71
- if (node.file) {
72
- translated += node.file.translated;
73
- total += node.file.total;
74
- }
75
- for (const child of node.children.values()) {
76
- const sub = dirPct(child);
77
- translated += sub.translated;
78
- total += sub.total;
79
- }
80
- return {
81
- translated,
82
- total
83
- };
84
- }
85
- function TreeRow({ node, depth, activeFile, selected, isEn, collapsed, onToggleCollapse, onSelect, onToggle }) {
86
- const isDir = node.children.size > 0 && !node.file;
87
- const isCollapsed = collapsed.has(node.path);
88
- const children = [...node.children.values()].sort((a, b) => {
89
- const aDir = a.children.size > 0 && !a.file;
90
- if (aDir !== (b.children.size > 0 && !b.file)) return aDir ? -1 : 1;
91
- return a.name.localeCompare(b.name);
92
- });
93
- if (isDir) {
94
- const { translated, total } = dirPct(node);
95
- const pct = total > 0 ? translated / total * 100 : 100;
96
- const color = pct >= 100 ? "var(--green)" : pct > 0 ? "var(--yellow)" : "var(--red)";
97
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
98
- className: "file-row tree-dir",
99
- style: { paddingLeft: `${depth + .25}rem` },
100
- onClick: () => onToggleCollapse(node.path),
101
- children: [
102
- /* @__PURE__ */ jsx("span", {
103
- className: "tree-arrow",
104
- children: isCollapsed ? "▶" : "▼"
105
- }),
106
- /* @__PURE__ */ jsx("span", {
107
- className: "icon",
108
- children: "📁"
109
- }),
110
- /* @__PURE__ */ jsxs("span", {
111
- className: "path",
112
- title: node.path,
113
- children: [node.name, "/"]
114
- }),
115
- /* @__PURE__ */ jsxs("span", {
116
- className: "pct",
117
- children: [pct.toFixed(0), "%"]
118
- }),
119
- /* @__PURE__ */ jsx("div", {
120
- className: "mini-bar",
121
- children: /* @__PURE__ */ jsx("div", {
122
- className: "mini-fill",
123
- style: {
124
- width: `${pct}%`,
125
- background: color
126
- }
127
- })
128
- })
129
- ]
130
- }), !isCollapsed && children.map((child) => /* @__PURE__ */ jsx(TreeRow, {
131
- node: child,
132
- depth: depth + 1,
133
- activeFile,
134
- selected,
135
- isEn,
136
- collapsed,
137
- onToggleCollapse,
138
- onSelect,
139
- onToggle
140
- }, child.path))] });
141
- }
142
- const f = node.file;
143
- if (!f) return null;
144
- f.total - f.translated;
145
- const color = f.status === "complete" ? "var(--green)" : f.status === "partial" ? "var(--yellow)" : "var(--red)";
146
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
147
- className: `file-row${activeFile === f.file ? " active" : ""}`,
148
- style: { paddingLeft: `${depth + .25}rem` },
149
- onClick: () => onSelect(f.file),
150
- children: [
151
- !isEn && /* @__PURE__ */ jsx("input", {
152
- type: "checkbox",
153
- checked: selected.has(f.file),
154
- onChange: (e) => {
155
- e.stopPropagation();
156
- onToggle(f.file);
157
- },
158
- onClick: (e) => e.stopPropagation()
159
- }),
160
- /* @__PURE__ */ jsx("span", {
161
- className: "icon",
162
- children: statusIcon(f.pct)
163
- }),
164
- /* @__PURE__ */ jsx("span", {
165
- className: "path",
166
- title: f.file,
167
- children: node.name
168
- }),
169
- /* @__PURE__ */ jsxs("span", {
170
- className: "pct",
171
- children: [f.pct.toFixed(0), "%"]
172
- }),
173
- /* @__PURE__ */ jsx("div", {
174
- className: "mini-bar",
175
- children: /* @__PURE__ */ jsx("div", {
176
- className: "mini-fill",
177
- style: {
178
- width: `${f.pct}%`,
179
- background: color
180
- }
181
- })
182
- })
183
- ]
184
- }), !isCollapsed && children.map((child) => /* @__PURE__ */ jsx(TreeRow, {
185
- node: child,
186
- depth: depth + 1,
187
- activeFile,
188
- selected,
189
- isEn,
190
- collapsed,
191
- onToggleCollapse,
192
- onSelect,
193
- onToggle
194
- }, child.path))] });
195
- }
196
- function FileList({ files, lang, activeFile, selected, statusFilter, sectionFilter, onStatusFilter, onSectionFilter, onSelect, onToggle, onSelectAll, onClear, onTranslateSelected }) {
197
- const [viewMode, setViewMode] = useState("tree");
198
- const [collapsed, setCollapsed] = useState(/* @__PURE__ */ new Set());
199
- const [search, setSearch] = useState("");
200
- const filtered = useMemo(() => {
201
- let result = files.map((f) => ({
202
- ...f,
203
- pct: filePct(f),
204
- status: fileStatus(f),
205
- section: getSection(f.file)
206
- }));
207
- if (statusFilter !== "all") result = result.filter((f) => f.status === statusFilter);
208
- if (sectionFilter !== "all") result = result.filter((f) => f.section === sectionFilter);
209
- if (search.trim()) {
210
- const q = search.trim().toLowerCase();
211
- result = result.filter((f) => f.file.toLowerCase().includes(q));
212
- }
213
- return result;
214
- }, [
215
- files,
216
- statusFilter,
217
- sectionFilter,
218
- search
219
- ]);
220
- const tree = useMemo(() => buildTree(filtered), [filtered]);
221
- const toggleCollapse = (path) => {
222
- setCollapsed((prev) => {
223
- const next = new Set(prev);
224
- if (next.has(path)) next.delete(path);
225
- else next.add(path);
226
- return next;
227
- });
228
- };
229
- const isEn = lang === "en";
230
- return /* @__PURE__ */ jsxs("div", {
231
- className: "file-list-wrap",
232
- children: [
233
- !isEn && selected.size > 0 && /* @__PURE__ */ jsxs("div", {
234
- className: "sel-bar",
235
- children: [
236
- /* @__PURE__ */ jsxs("span", { children: [selected.size, " selected"] }),
237
- /* @__PURE__ */ jsx("button", {
238
- type: "button",
239
- className: "btn btn-sm",
240
- onClick: onTranslateSelected,
241
- children: "Translate Selected"
242
- }),
243
- /* @__PURE__ */ jsx("button", {
244
- type: "button",
245
- className: "btn btn-sm btn-outline",
246
- onClick: onClear,
247
- children: "Clear"
248
- })
249
- ]
250
- }),
251
- /* @__PURE__ */ jsxs("div", {
252
- className: "file-search",
253
- children: [/* @__PURE__ */ jsx("input", {
254
- type: "text",
255
- placeholder: "Search files...",
256
- value: search,
257
- onChange: (e) => setSearch(e.target.value)
258
- }), search && /* @__PURE__ */ jsx("button", {
259
- type: "button",
260
- className: "file-search-clear",
261
- onClick: () => setSearch(""),
262
- children: "✕"
263
- })]
264
- }),
265
- /* @__PURE__ */ jsxs("div", {
266
- className: "file-list-hdr",
267
- children: [
268
- !isEn && /* @__PURE__ */ jsx("input", {
269
- type: "checkbox",
270
- checked: filtered.length > 0 && filtered.every((f) => selected.has(f.file)),
271
- onChange: (e) => {
272
- if (e.target.checked) onSelectAll(filtered.map((f) => f.file));
273
- else onClear();
274
- },
275
- title: "Select all visible"
276
- }),
277
- /* @__PURE__ */ jsxs("strong", { children: [
278
- lang,
279
- " · ",
280
- filtered.length,
281
- " files"
282
- ] }),
283
- /* @__PURE__ */ jsx("span", { className: "spacer" }),
284
- /* @__PURE__ */ jsx("button", {
285
- type: "button",
286
- className: `btn btn-xs${viewMode === "list" ? " active" : ""}`,
287
- onClick: () => setViewMode("list"),
288
- title: "List view",
289
- children: "☰"
290
- }),
291
- /* @__PURE__ */ jsx("button", {
292
- type: "button",
293
- className: `btn btn-xs${viewMode === "tree" ? " active" : ""}`,
294
- onClick: () => setViewMode("tree"),
295
- title: "Tree view",
296
- children: "🌲"
297
- }),
298
- /* @__PURE__ */ jsxs("select", {
299
- value: statusFilter,
300
- onChange: (e) => onStatusFilter(e.target.value),
301
- children: [
302
- /* @__PURE__ */ jsx("option", {
303
- value: "all",
304
- children: "All"
305
- }),
306
- /* @__PURE__ */ jsx("option", {
307
- value: "complete",
308
- children: "✅ Complete"
309
- }),
310
- /* @__PURE__ */ jsx("option", {
311
- value: "partial",
312
- children: "🟡 Partial"
313
- }),
314
- /* @__PURE__ */ jsx("option", {
315
- value: "missing",
316
- children: "🔴 Missing"
317
- })
318
- ]
319
- }),
320
- /* @__PURE__ */ jsxs("select", {
321
- value: sectionFilter,
322
- onChange: (e) => onSectionFilter(e.target.value),
323
- children: [
324
- /* @__PURE__ */ jsx("option", {
325
- value: "all",
326
- children: "All sections"
327
- }),
328
- /* @__PURE__ */ jsx("option", {
329
- value: "docs",
330
- children: "docs"
331
- }),
332
- /* @__PURE__ */ jsx("option", {
333
- value: "blog",
334
- children: "blog"
335
- }),
336
- /* @__PURE__ */ jsx("option", {
337
- value: "learn",
338
- children: "learn"
339
- })
340
- ]
341
- })
342
- ]
343
- }),
344
- /* @__PURE__ */ jsxs("div", {
345
- className: "file-list-body",
346
- children: [
347
- filtered.length === 0 && /* @__PURE__ */ jsx("div", {
348
- className: "loading",
349
- children: "No files match filter"
350
- }),
351
- viewMode === "list" && filtered.map((f) => {
352
- f.total - f.translated;
353
- const color = f.status === "complete" ? "var(--green)" : f.status === "partial" ? "var(--yellow)" : "var(--red)";
354
- return /* @__PURE__ */ jsxs("div", {
355
- className: `file-row${activeFile === f.file ? " active" : ""}`,
356
- onClick: () => onSelect(f.file),
357
- children: [
358
- !isEn && /* @__PURE__ */ jsx("input", {
359
- type: "checkbox",
360
- checked: selected.has(f.file),
361
- onChange: (e) => {
362
- e.stopPropagation();
363
- onToggle(f.file);
364
- },
365
- onClick: (e) => e.stopPropagation()
366
- }),
367
- /* @__PURE__ */ jsx("span", {
368
- className: "icon",
369
- children: statusIcon(f.pct)
370
- }),
371
- /* @__PURE__ */ jsx("span", {
372
- className: "path",
373
- title: f.file,
374
- children: f.file
375
- }),
376
- /* @__PURE__ */ jsxs("span", {
377
- className: "pct",
378
- children: [f.pct.toFixed(0), "%"]
379
- }),
380
- /* @__PURE__ */ jsx("div", {
381
- className: "mini-bar",
382
- children: /* @__PURE__ */ jsx("div", {
383
- className: "mini-fill",
384
- style: {
385
- width: `${f.pct}%`,
386
- background: color
387
- }
388
- })
389
- })
390
- ]
391
- }, f.file);
392
- }),
393
- viewMode === "tree" && [...tree.children.values()].sort((a, b) => a.name.localeCompare(b.name)).map((child) => /* @__PURE__ */ jsx(TreeRow, {
394
- node: child,
395
- depth: 0,
396
- activeFile,
397
- selected,
398
- isEn,
399
- collapsed,
400
- onToggleCollapse: toggleCollapse,
401
- onSelect,
402
- onToggle
403
- }, child.path))
404
- ]
405
- })
406
- ]
407
- });
408
- }
409
- //#endregion
410
- //#region ../../node_modules/.bun/@tanstack+start-server-core@1.167.2/node_modules/@tanstack/start-server-core/dist/esm/createSsrRpc.js
411
- var createSsrRpc = (functionId, importer) => {
412
- const url = "/_serverFn/" + functionId;
413
- const serverFnMeta = { id: functionId };
414
- const fn = async (...args) => {
415
- return (importer ? await importer() : await getServerFnById(functionId))(...args);
416
- };
417
- return Object.assign(fn, {
418
- url,
419
- serverFnMeta,
420
- [TSS_SERVER_FUNCTION]: true
421
- });
422
- };
423
- //#endregion
424
- //#region server/functions/status.ts
425
- var fetchStatus = createServerFn({ method: "GET" }).handler(createSsrRpc("4e218d79545765572808c7eab33b7663d4496209c15406d0b449366905b6b83f"));
426
- var fetchFileCoverage = createServerFn({ method: "GET" }).inputValidator((d) => d).handler(createSsrRpc("843cd8b59095708a5ae78198708db9850f89fd3dd3830ab236f0bd924417692f"));
427
- var fetchFileBlocks = createServerFn({ method: "GET" }).inputValidator((d) => d).handler(createSsrRpc("e1e7281e45375c67dbe408c58452e7482b139da60e9e361615553227dce95ee0"));
428
- var deleteCacheEntry = createServerFn({ method: "POST" }).inputValidator((d) => d).handler(createSsrRpc("e1d13d8602339a95f559345ccfc82e9f843dd375ce8e9f580637c241e7e44774"));
429
- createServerFn({ method: "POST" }).inputValidator((d) => d).handler(createSsrRpc("1d8e3916f992485c62e62c3693083850b773fdff4ec54277de5a01eb98dab664"));
430
- //#endregion
431
- //#region server/functions/jobs.ts
432
- var fetchJobs = createServerFn({ method: "GET" }).handler(createSsrRpc("421de02ce39dde6e27cf4689e837ec072cbd01e63f8cdd5c2a3f42f0bd5ca613"));
433
- var createJob = createServerFn({ method: "POST" }).inputValidator((d) => d).handler(createSsrRpc("c08559ac758aa0d315deaca7a0d7d923a9a44d997c8cb811151417c1f221ddd6"));
434
- createServerFn({ method: "GET" }).inputValidator((d) => d).handler(createSsrRpc("8a56694c9d7b29422a3e7d2f6b803be100d79d3853d92d465cb55ed572781e62"));
435
- var deleteJob = createServerFn({ method: "POST" }).inputValidator((d) => d).handler(createSsrRpc("88c2855c84e91504070bfecc50ddfa50339d22c305626800b6d9b05d79385d71"));
436
- //#endregion
437
- //#region server/functions/models.ts
438
- var fetchModels = createServerFn({ method: "GET" }).handler(createSsrRpc("5080dc3f2f2309ec6981b94c431969637130c657e8a1dfb10400b4614eecc1ea"));
439
- //#endregion
440
- //#region server/functions/misc.ts
441
- var fetchVersion = createServerFn({ method: "GET" }).handler(createSsrRpc("a3d81974aeece150d4b02be5b91590b8187442ebea56be4a89dcbf053626d22b"));
442
- var fetchLlmConfig = createServerFn({ method: "GET" }).handler(createSsrRpc("3bf4ba50ca8ccc3c8c60d8f2e53307a320940d68c478df494552066904c5cd74"));
443
- createServerFn({ method: "GET" }).handler(createSsrRpc("e0b4116f6b2c8d096830102e36458acf9c616a056fcdddda956a4d66984ef58c"));
444
- var openFile = createServerFn({ method: "POST" }).inputValidator((d) => d).handler(createSsrRpc("a054a04356fe9987891efee8b7a11cd2dedb00f6b2e8f26d1c642e001e553d53"));
445
- //#endregion
446
- //#region app/lib/api.ts
447
- var api = {
448
- version: () => fetchVersion(),
449
- llmConfig: () => fetchLlmConfig(),
450
- status: () => fetchStatus(),
451
- fileCoverage: (version, lang) => fetchFileCoverage({ data: {
452
- version,
453
- lang
454
- } }),
455
- fileBlocks: (version, lang, file) => fetchFileBlocks({ data: {
456
- version,
457
- lang,
458
- path: file
459
- } }),
460
- deleteCache: (version, lang, key) => deleteCacheEntry({ data: {
461
- version,
462
- lang,
463
- key
464
- } }),
465
- models: () => fetchModels(),
466
- jobs: () => fetchJobs(),
467
- createJob: (body) => createJob({ data: body }),
468
- deleteJob: (id) => deleteJob({ data: { id } })
469
- };
470
- //#endregion
471
- //#region app/components/JobDialog.tsx
472
- function formatPrice(price) {
473
- if (price === 0) return "Free";
474
- if (price < .01) return `$${price.toFixed(4)}`;
475
- return `$${price.toFixed(2)}`;
476
- }
477
- function ctxLabel(n) {
478
- if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
479
- return `${(n / 1e3).toFixed(0)}k`;
480
- }
481
- var CTX_STEPS = [
482
- 0,
483
- 8,
484
- 16,
485
- 32,
486
- 64,
487
- 128,
488
- 200,
489
- 512,
490
- 1024,
491
- 2048
492
- ];
493
- function JobDialog({ langs, versions, defaultLang, defaultVersion, files, onClose, onSuccess }) {
494
- const [lang, setLang] = useState(defaultLang || langs[0]);
495
- const [version, setVersion] = useState(defaultVersion || versions[0]);
496
- const [max, setMax] = useState(files?.length || 999);
497
- const [concurrency, setConcurrency] = useState(3);
498
- const [error, setError] = useState(null);
499
- const [selectedModel, setSelectedModel] = useState("");
500
- const [search, setSearch] = useState("");
501
- const [sort, setSort] = useState("context-desc");
502
- const [ctxSlider, setCtxSlider] = useState(0);
503
- const [freeOnly, setFreeOnly] = useState(true);
504
- const [jsonOnly, setJsonOnly] = useState(false);
505
- const minCtx = CTX_STEPS[ctxSlider] * 1e3;
506
- const { data: llmConfig } = useQuery({
507
- queryKey: ["llmConfig"],
508
- queryFn: api.llmConfig,
509
- staleTime: 60 * 1e3
510
- });
511
- const { data: models, isLoading: modelsLoading } = useQuery({
512
- queryKey: ["models"],
513
- queryFn: api.models,
514
- staleTime: 300 * 1e3
515
- });
516
- const filtered = useMemo(() => {
517
- if (!models) return [];
518
- let result = models.filter((m) => {
519
- if (search) {
520
- const q = search.toLowerCase();
521
- if (!m.id.toLowerCase().includes(q) && !m.name.toLowerCase().includes(q)) return false;
522
- }
523
- if (freeOnly && !m.isFree) return false;
524
- if (minCtx > 0 && m.contextLength < minCtx) return false;
525
- if (jsonOnly && !m.supportsJson) return false;
526
- return true;
527
- });
528
- result = [...result];
529
- switch (sort) {
530
- case "price-asc":
531
- result.sort((a, b) => a.promptPrice - b.promptPrice);
532
- break;
533
- case "price-desc":
534
- result.sort((a, b) => b.promptPrice - a.promptPrice);
535
- break;
536
- case "context-desc":
537
- result.sort((a, b) => b.contextLength - a.contextLength);
538
- break;
539
- case "name":
540
- result.sort((a, b) => a.name.localeCompare(b.name));
541
- break;
542
- }
543
- return result;
544
- }, [
545
- models,
546
- search,
547
- freeOnly,
548
- minCtx,
549
- jsonOnly,
550
- sort
551
- ]);
552
- const qc = useQueryClient();
553
- const create = useMutation({
554
- mutationFn: () => {
555
- return api.createJob({
556
- lang,
557
- version,
558
- max,
559
- concurrency,
560
- model: selectedModel || void 0,
561
- md5: true,
562
- files: files?.length ? files : void 0
563
- });
564
- },
565
- onSuccess: () => {
566
- qc.invalidateQueries({ queryKey: ["jobs"] });
567
- if (onSuccess) onSuccess(`✅ Job started: ${lang} / ${version}${files?.length ? ` (${files.length} files)` : ""}`);
568
- else onClose();
569
- },
570
- onError: (err) => setError(err.message)
571
- });
572
- const translatable = langs.filter((l) => l !== "en");
573
- const selectedModelInfo = selectedModel ? models?.find((m) => m.id === selectedModel) : null;
574
- return /* @__PURE__ */ jsx("div", {
575
- className: "dialog-overlay",
576
- onClick: (e) => {
577
- if (e.target === e.currentTarget) onClose();
578
- },
579
- children: /* @__PURE__ */ jsxs("div", {
580
- className: "dialog dialog-wide",
581
- children: [
582
- /* @__PURE__ */ jsx("h3", { children: "Start Translation Job" }),
583
- error && /* @__PURE__ */ jsx("div", {
584
- className: "dialog-error",
585
- children: error
586
- }),
587
- llmConfig && !llmConfig.hasApiKey && /* @__PURE__ */ jsxs("div", {
588
- className: "dialog-error",
589
- children: [
590
- "⚠ No API key found. Set ",
591
- /* @__PURE__ */ jsx("code", { children: "OPENROUTER_API_KEY" }),
592
- " in ",
593
- /* @__PURE__ */ jsx("code", { children: ".env" }),
594
- " or configure ",
595
- /* @__PURE__ */ jsx("code", { children: "llm.apiKey" }),
596
- " in config."
597
- ]
598
- }),
599
- llmConfig?.model && /* @__PURE__ */ jsxs("div", {
600
- className: "dialog-mode-hint",
601
- children: [
602
- "Default model: ",
603
- /* @__PURE__ */ jsx("strong", { children: llmConfig.model }),
604
- llmConfig.provider && /* @__PURE__ */ jsxs("span", { children: [
605
- " (",
606
- llmConfig.provider,
607
- ")"
608
- ] })
609
- ]
610
- }),
611
- files?.length && /* @__PURE__ */ jsxs("div", {
612
- className: "dialog-mode-hint",
613
- children: [
614
- "Translating ",
615
- files.length,
616
- " selected file",
617
- files.length > 1 ? "s" : ""
618
- ]
619
- }),
620
- /* @__PURE__ */ jsxs("div", {
621
- className: "dialog-grid",
622
- children: [
623
- /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", { children: "Language" }), /* @__PURE__ */ jsx("select", {
624
- value: lang,
625
- onChange: (e) => setLang(e.target.value),
626
- children: translatable.map((l) => /* @__PURE__ */ jsxs("option", {
627
- value: l,
628
- children: [
629
- FLAGS[l],
630
- " ",
631
- l
632
- ]
633
- }, l))
634
- })] }),
635
- /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", { children: "Version" }), /* @__PURE__ */ jsx("select", {
636
- value: version,
637
- onChange: (e) => setVersion(e.target.value),
638
- children: versions.map((v) => /* @__PURE__ */ jsx("option", {
639
- value: v,
640
- children: v
641
- }, v))
642
- })] }),
643
- /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", { children: "Max API calls" }), /* @__PURE__ */ jsx("input", {
644
- type: "number",
645
- value: max,
646
- onChange: (e) => setMax(Number(e.target.value))
647
- })] }),
648
- /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", { children: "Concurrency" }), /* @__PURE__ */ jsx("input", {
649
- type: "number",
650
- value: concurrency,
651
- onChange: (e) => setConcurrency(Number(e.target.value))
652
- })] })
653
- ]
654
- }),
655
- /* @__PURE__ */ jsx("div", {
656
- className: "dialog-section-hdr",
657
- children: /* @__PURE__ */ jsx("span", { children: "Model" })
658
- }),
659
- /* @__PURE__ */ jsxs("div", {
660
- className: "model-filters",
661
- children: [
662
- /* @__PURE__ */ jsx("input", {
663
- type: "text",
664
- placeholder: "Search models...",
665
- value: search,
666
- onChange: (e) => setSearch(e.target.value),
667
- className: "model-search-input"
668
- }),
669
- /* @__PURE__ */ jsxs("select", {
670
- value: sort,
671
- onChange: (e) => setSort(e.target.value),
672
- children: [
673
- /* @__PURE__ */ jsx("option", {
674
- value: "context-desc",
675
- children: "Context ↓"
676
- }),
677
- /* @__PURE__ */ jsx("option", {
678
- value: "price-asc",
679
- children: "Price ↑"
680
- }),
681
- /* @__PURE__ */ jsx("option", {
682
- value: "price-desc",
683
- children: "Price ↓"
684
- }),
685
- /* @__PURE__ */ jsx("option", {
686
- value: "name",
687
- children: "Name A-Z"
688
- })
689
- ]
690
- }),
691
- /* @__PURE__ */ jsxs("label", {
692
- className: "model-filter-check",
693
- children: [/* @__PURE__ */ jsx("input", {
694
- type: "checkbox",
695
- checked: freeOnly,
696
- onChange: (e) => setFreeOnly(e.target.checked)
697
- }), "Free"]
698
- }),
699
- /* @__PURE__ */ jsxs("label", {
700
- className: "model-filter-check",
701
- title: "Structured JSON output — required for best translation quality",
702
- children: [/* @__PURE__ */ jsx("input", {
703
- type: "checkbox",
704
- checked: jsonOnly,
705
- onChange: (e) => setJsonOnly(e.target.checked)
706
- }), "JSON mode"]
707
- })
708
- ]
709
- }),
710
- /* @__PURE__ */ jsxs("div", {
711
- className: "model-slider-row",
712
- children: [/* @__PURE__ */ jsxs("span", {
713
- className: "model-slider-label",
714
- children: ["Context ≥ ", minCtx === 0 ? "any" : ctxLabel(minCtx)]
715
- }), /* @__PURE__ */ jsx("input", {
716
- type: "range",
717
- min: 0,
718
- max: CTX_STEPS.length - 1,
719
- value: ctxSlider,
720
- onChange: (e) => setCtxSlider(Number(e.target.value)),
721
- className: "model-slider"
722
- })]
723
- }),
724
- /* @__PURE__ */ jsxs("div", {
725
- className: "model-list",
726
- children: [
727
- /* @__PURE__ */ jsx("div", {
728
- className: `model-item${selectedModel === "" ? " active" : ""}`,
729
- onClick: () => setSelectedModel(""),
730
- children: /* @__PURE__ */ jsx("span", {
731
- className: "model-name",
732
- children: "Default (from .env)"
733
- })
734
- }),
735
- modelsLoading && /* @__PURE__ */ jsx("div", {
736
- className: "model-item disabled",
737
- children: "Loading models..."
738
- }),
739
- filtered.map((m) => /* @__PURE__ */ jsx(ModelRow, {
740
- m,
741
- mode: "single",
742
- active: selectedModel === m.id,
743
- onClick: () => setSelectedModel(m.id)
744
- }, m.id)),
745
- !modelsLoading && filtered.length === 0 && /* @__PURE__ */ jsx("div", {
746
- className: "model-item disabled",
747
- children: "No models match filters"
748
- })
749
- ]
750
- }),
751
- /* @__PURE__ */ jsxs("div", {
752
- className: "model-count",
753
- children: [
754
- filtered.length,
755
- " model",
756
- filtered.length !== 1 ? "s" : "",
757
- models ? ` / ${models.length} total` : ""
758
- ]
759
- }),
760
- selectedModelInfo && /* @__PURE__ */ jsxs("div", {
761
- className: "model-info",
762
- children: [
763
- /* @__PURE__ */ jsx("strong", { children: selectedModelInfo.name }),
764
- selectedModelInfo.isFree && /* @__PURE__ */ jsx("span", {
765
- className: "badge-free",
766
- children: "FREE"
767
- }),
768
- selectedModelInfo.supportsJson && /* @__PURE__ */ jsx("span", {
769
- className: "badge-json",
770
- children: "JSON"
771
- }),
772
- /* @__PURE__ */ jsx("br", {}),
773
- "In: ",
774
- formatPrice(selectedModelInfo.promptPrice),
775
- "/M · Out:",
776
- " ",
777
- formatPrice(selectedModelInfo.completionPrice),
778
- "/M · Ctx:",
779
- " ",
780
- ctxLabel(selectedModelInfo.contextLength),
781
- selectedModelInfo.maxOutput > 0 && ` · Max out: ${ctxLabel(selectedModelInfo.maxOutput)}`
782
- ]
783
- }),
784
- files && files.length > 0 && /* @__PURE__ */ jsxs("div", {
785
- className: "file-list-preview",
786
- children: [/* @__PURE__ */ jsxs("strong", { children: [files.length, " files selected:"] }), files.map((f) => /* @__PURE__ */ jsx("div", { children: f }, f))]
787
- }),
788
- /* @__PURE__ */ jsxs("div", {
789
- className: "actions",
790
- children: [/* @__PURE__ */ jsx("button", {
791
- type: "button",
792
- className: "btn btn-outline",
793
- onClick: onClose,
794
- children: "Cancel"
795
- }), /* @__PURE__ */ jsx("button", {
796
- type: "button",
797
- className: "btn",
798
- disabled: create.isPending || llmConfig && !llmConfig.hasApiKey,
799
- onClick: () => create.mutate(),
800
- children: create.isPending ? "Starting..." : llmConfig && !llmConfig.hasApiKey ? "No API Key" : "Start Job →"
801
- })]
802
- })
803
- ]
804
- })
805
- });
806
- }
807
- function ModelRow({ m, mode, active, onClick }) {
808
- return /* @__PURE__ */ jsxs("div", {
809
- className: `model-item${active ? " active" : ""}`,
810
- onClick,
811
- children: [
812
- mode === "rotate" && /* @__PURE__ */ jsx("input", {
813
- type: "checkbox",
814
- checked: active,
815
- readOnly: true,
816
- className: "model-check"
817
- }),
818
- /* @__PURE__ */ jsxs("span", {
819
- className: "model-name",
820
- children: [m.isFree && "🆓 ", m.name]
821
- }),
822
- /* @__PURE__ */ jsxs("span", {
823
- className: "model-meta",
824
- children: [
825
- m.supportsJson ? "✓ " : "",
826
- ctxLabel(m.contextLength),
827
- " ·",
828
- " ",
829
- m.isFree ? "Free" : `${formatPrice(m.promptPrice)}/${formatPrice(m.completionPrice)}`
830
- ]
831
- })
832
- ]
833
- });
834
- }
835
- //#endregion
836
- //#region app/components/JobPanel.tsx
837
- function escapeHtml(s) {
838
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
839
- }
840
- function JobItem({ job }) {
841
- const qc = useQueryClient();
842
- const del = useMutation({
843
- mutationFn: () => api.deleteJob(job.id),
844
- onSuccess: () => qc.invalidateQueries({ queryKey: ["jobs"] })
845
- });
846
- const pct = job.toTranslate > 0 ? job.translatedFiles / job.toTranslate * 100 : job.status === "completed" ? 100 : 0;
847
- let progress = "";
848
- if (job.toTranslate > 0) {
849
- progress = `${job.translatedFiles}/${job.toTranslate} translated`;
850
- if (job.totalFiles) progress += ` (${job.totalFiles} total)`;
851
- } else if (job.totalFiles > 0) progress = `scanning ${job.totalFiles} files...`;
852
- if (job.errorFiles > 0) progress += ` · ${job.errorFiles} errors`;
853
- const running = job.status === "running";
854
- return /* @__PURE__ */ jsxs("div", {
855
- className: "job-item",
856
- children: [
857
- /* @__PURE__ */ jsxs("div", {
858
- className: "hdr",
859
- children: [
860
- /* @__PURE__ */ jsx("span", {
861
- className: `status-badge ${job.status}`,
862
- children: job.status
863
- }),
864
- /* @__PURE__ */ jsxs("strong", { children: [
865
- job.lang,
866
- "/",
867
- job.version
868
- ] }),
869
- job.currentFile && /* @__PURE__ */ jsx("span", {
870
- style: { color: "var(--fg2)" },
871
- children: job.currentFile
872
- }),
873
- /* @__PURE__ */ jsx("span", { className: "spacer" }),
874
- /* @__PURE__ */ jsx("button", {
875
- type: "button",
876
- className: "btn btn-sm btn-outline",
877
- onClick: () => del.mutate(),
878
- children: running ? "⏹" : "🗑"
879
- })
880
- ]
881
- }),
882
- /* @__PURE__ */ jsx("div", {
883
- className: "progress-bar",
884
- children: /* @__PURE__ */ jsx("div", {
885
- className: "progress-fill",
886
- style: { width: `${Math.min(100, pct)}%` }
887
- })
888
- }),
889
- /* @__PURE__ */ jsxs("div", {
890
- className: "job-meta",
891
- children: [
892
- progress,
893
- " · ",
894
- new Date(job.startedAt).toLocaleTimeString(),
895
- job.finishedAt && ` · done ${new Date(job.finishedAt).toLocaleTimeString()}`,
896
- job.exitCode != null && job.exitCode !== 0 && ` · exit ${job.exitCode}`
897
- ]
898
- }),
899
- job.logLines && job.logLines.length > 0 && /* @__PURE__ */ jsxs("div", {
900
- className: "log-viewer",
901
- children: [/* @__PURE__ */ jsx("button", {
902
- type: "button",
903
- className: "log-copy",
904
- title: "Copy logs",
905
- onClick: () => {
906
- navigator.clipboard.writeText(job.logLines.join("\n"));
907
- },
908
- children: "📋"
909
- }), job.logLines.map((line, i) => /* @__PURE__ */ jsx("div", {
910
- className: line.includes("stderr") || line.includes("rror") ? "err" : void 0,
911
- dangerouslySetInnerHTML: { __html: escapeHtml(line) }
912
- }, i))]
913
- })
914
- ]
915
- });
916
- }
917
- function JobPanel() {
918
- const qc = useQueryClient();
919
- const prevRunning = useRef(/* @__PURE__ */ new Set());
920
- const { data: jobs = [] } = useQuery({
921
- queryKey: ["jobs"],
922
- queryFn: api.jobs,
923
- refetchInterval: 3e3
924
- });
925
- useEffect(() => {
926
- const currentRunning = new Set(jobs.filter((j) => j.status === "running").map((j) => j.id));
927
- if ([...prevRunning.current].filter((id) => !currentRunning.has(id)).length > 0) {
928
- qc.invalidateQueries({ queryKey: ["files"] });
929
- qc.invalidateQueries({ queryKey: ["fileBlocks"] });
930
- qc.invalidateQueries({ queryKey: ["status"] });
931
- }
932
- prevRunning.current = currentRunning;
933
- }, [jobs, qc]);
934
- return /* @__PURE__ */ jsxs("div", {
935
- className: "jobs-panel",
936
- children: [/* @__PURE__ */ jsx("h3", { children: "Jobs" }), jobs.length === 0 ? /* @__PURE__ */ jsx("span", {
937
- style: {
938
- color: "var(--fg2)",
939
- fontSize: "0.85rem"
940
- },
941
- children: "No jobs"
942
- }) : jobs.map((j) => /* @__PURE__ */ jsx(JobItem, { job: j }, j.id))]
943
- });
944
- }
945
- //#endregion
946
- //#region app/components/ProgressBar.tsx
947
- function ProgressBar({ value, color, height = 4 }) {
948
- return /* @__PURE__ */ jsx("div", {
949
- className: "bar-bg",
950
- style: { height },
951
- children: /* @__PURE__ */ jsx("div", {
952
- className: "bar-fill",
953
- style: {
954
- width: `${Math.min(100, value)}%`,
955
- background: color || "var(--accent)"
956
- }
957
- })
958
- });
959
- }
960
- //#endregion
961
- //#region app/components/LangGrid.tsx
962
- function LangGrid({ data, version, selectedLang, onSelect }) {
963
- const vData = data.data[version];
964
- if (!vData) return null;
965
- return /* @__PURE__ */ jsx("div", {
966
- className: "lang-grid",
967
- children: data.langs.map((lang) => {
968
- if (lang === "en") return /* @__PURE__ */ jsxs("div", {
969
- className: `lang-card is-en${selectedLang === "en" ? " selected" : ""}`,
970
- onClick: () => onSelect("en"),
971
- children: [
972
- /* @__PURE__ */ jsxs("div", {
973
- className: "name",
974
- children: [FLAGS.en, " en (source)"]
975
- }),
976
- /* @__PURE__ */ jsxs("div", {
977
- className: "stats",
978
- children: [vData.enFileCount, " files"]
979
- }),
980
- /* @__PURE__ */ jsx(ProgressBar, {
981
- value: 100,
982
- color: "var(--green)"
983
- })
984
- ]
985
- }, "en");
986
- const ls = vData.langs[lang];
987
- if (!ls) return null;
988
- const pct = ls.totalNodes > 0 ? ls.translatedNodes / ls.totalNodes * 100 : 0;
989
- return /* @__PURE__ */ jsxs("div", {
990
- className: `lang-card${selectedLang === lang ? " selected" : ""}`,
991
- onClick: () => onSelect(lang),
992
- children: [
993
- /* @__PURE__ */ jsxs("div", {
994
- className: "name",
995
- children: [
996
- FLAGS[lang],
997
- " ",
998
- lang
999
- ]
1000
- }),
1001
- /* @__PURE__ */ jsxs("div", {
1002
- className: "stats",
1003
- children: [pct.toFixed(1), "%"]
1004
- }),
1005
- /* @__PURE__ */ jsx(ProgressBar, {
1006
- value: pct,
1007
- color: pctColor(pct)
1008
- })
1009
- ]
1010
- }, lang);
1011
- })
1012
- });
1013
- }
1014
- //#endregion
1015
- //#region app/components/Preview.tsx
1016
- function extractHeading(text, prefix, idx) {
1017
- const m = text.split("\n")[0].match(/^(#{1,6})\s+(.+)/);
1018
- if (!m) return null;
1019
- const clean = m[2].replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[]\(#[^)]*\)/g, "").replace(/[`*[\]]/g, "").trim();
1020
- return {
1021
- id: `${prefix}-h-${idx}`,
1022
- level: m[1].length,
1023
- text: clean
1024
- };
1025
- }
1026
- function getHeadings(blocks, prefix, useTranslation = false) {
1027
- const headings = [];
1028
- for (let i = 0; i < blocks.length; i++) {
1029
- const h = extractHeading(useTranslation && blocks[i].translation != null ? blocks[i].translation : blocks[i].source, prefix, i);
1030
- if (h) headings.push(h);
1031
- }
1032
- return headings;
1033
- }
1034
- function Preview({ version, lang, file, viewMode, onViewMode, showToc, onToggleToc, showNodes, onToggleNodes, onClose }) {
1035
- const bodyRef = useRef(null);
1036
- const isEn = lang === "en";
1037
- const mode = isEn ? "en" : viewMode;
1038
- const [highlightMd5, setHighlightMd5] = useState(null);
1039
- const [ctxMenu, setCtxMenu] = useState(null);
1040
- const qc = useQueryClient();
1041
- const deleteCache = useMutation({
1042
- mutationFn: (key) => api.deleteCache(version, lang, key),
1043
- onSuccess: () => {
1044
- qc.invalidateQueries({ queryKey: [
1045
- "fileBlocks",
1046
- version,
1047
- lang,
1048
- file
1049
- ] });
1050
- qc.invalidateQueries({ queryKey: ["files"] });
1051
- qc.invalidateQueries({ queryKey: ["status"] });
1052
- setCtxMenu(null);
1053
- }
1054
- });
1055
- useEffect(() => {
1056
- if (!ctxMenu) return;
1057
- const close = () => setCtxMenu(null);
1058
- window.addEventListener("click", close);
1059
- return () => window.removeEventListener("click", close);
1060
- }, [ctxMenu]);
1061
- const { data: blocksData } = useQuery({
1062
- queryKey: [
1063
- "fileBlocks",
1064
- version,
1065
- lang,
1066
- file
1067
- ],
1068
- queryFn: () => api.fileBlocks(version, lang, file)
1069
- });
1070
- const blocks = blocksData?.blocks ?? [];
1071
- const showEnCol = mode === "split" || mode === "en";
1072
- const showTransCol = !isEn && (mode === "split" || mode === "lang");
1073
- const enHeadings = useMemo(() => getHeadings(blocks, "b"), [blocks]);
1074
- const transHeadings = useMemo(() => getHeadings(blocks, "b", true), [blocks]);
1075
- const headings = showTransCol ? transHeadings : enHeadings;
1076
- const showGutter = showNodes && blocks.length > 0;
1077
- const translatableBlocks = useMemo(() => blocks.filter((b) => b.md5), [blocks]);
1078
- const translatedCount = translatableBlocks.filter((b) => b.translation != null).length;
1079
- const totalCount = translatableBlocks.length;
1080
- function scrollToHeading(idx) {
1081
- const h = headings[idx];
1082
- if (h) document.getElementById(h.id)?.scrollIntoView({
1083
- behavior: "smooth",
1084
- block: "start"
1085
- });
1086
- }
1087
- const cols = [];
1088
- if (showGutter) cols.push("4.5rem");
1089
- if (showEnCol) cols.push("1fr");
1090
- if (showTransCol) cols.push("1fr");
1091
- const gridCols = cols.join(" ");
1092
- return /* @__PURE__ */ jsxs("div", {
1093
- className: "preview-wrap",
1094
- children: [
1095
- /* @__PURE__ */ jsxs("div", {
1096
- className: "preview-hdr",
1097
- children: [
1098
- /* @__PURE__ */ jsx("a", {
1099
- className: "preview-filename",
1100
- href: "#",
1101
- title: "Click to open in editor",
1102
- onClick: (e) => {
1103
- e.preventDefault();
1104
- openFile({ data: { file: `content/${version}/${file}` } });
1105
- },
1106
- children: file
1107
- }),
1108
- !isEn && totalCount > 0 && /* @__PURE__ */ jsxs("span", {
1109
- className: "preview-stats",
1110
- children: [
1111
- translatedCount,
1112
- "/",
1113
- totalCount,
1114
- " (",
1115
- Math.round(translatedCount / totalCount * 100),
1116
- "%)"
1117
- ]
1118
- }),
1119
- onClose && /* @__PURE__ */ jsx("button", {
1120
- type: "button",
1121
- className: "preview-close",
1122
- onClick: onClose,
1123
- title: "Close preview",
1124
- children: "✕"
1125
- }),
1126
- /* @__PURE__ */ jsxs("div", {
1127
- className: "preview-toggle",
1128
- children: [
1129
- !isEn && /* @__PURE__ */ jsx("button", {
1130
- type: "button",
1131
- className: showNodes ? "active" : "",
1132
- onClick: onToggleNodes,
1133
- title: "Toggle MD5",
1134
- children: "#"
1135
- }),
1136
- !isEn && /* @__PURE__ */ jsxs(Fragment, { children: [
1137
- /* @__PURE__ */ jsx("button", {
1138
- type: "button",
1139
- className: mode === "split" ? "active" : "",
1140
- onClick: () => onViewMode("split"),
1141
- title: "Side by side",
1142
- children: "◫"
1143
- }),
1144
- /* @__PURE__ */ jsx("button", {
1145
- type: "button",
1146
- className: mode === "en" ? "active" : "",
1147
- onClick: () => onViewMode("en"),
1148
- title: "EN only",
1149
- children: FLAGS.en
1150
- }),
1151
- /* @__PURE__ */ jsx("button", {
1152
- type: "button",
1153
- className: mode === "lang" ? "active" : "",
1154
- onClick: () => onViewMode("lang"),
1155
- title: `${lang} only`,
1156
- children: FLAGS[lang]
1157
- })
1158
- ] }),
1159
- /* @__PURE__ */ jsx("button", {
1160
- type: "button",
1161
- className: showToc ? "active" : "",
1162
- onClick: onToggleToc,
1163
- title: "Toggle TOC",
1164
- children: "☰"
1165
- })
1166
- ]
1167
- })
1168
- ]
1169
- }),
1170
- /* @__PURE__ */ jsxs("div", {
1171
- className: "preview-content-area",
1172
- children: [/* @__PURE__ */ jsxs("div", {
1173
- className: "preview-body-blocks",
1174
- ref: bodyRef,
1175
- children: [
1176
- showGutter && /* @__PURE__ */ jsxs("div", {
1177
- className: "block-header",
1178
- style: { gridTemplateColumns: gridCols },
1179
- children: [
1180
- /* @__PURE__ */ jsx("span", {
1181
- className: "col-hdr gutter-hdr",
1182
- children: "MD5"
1183
- }),
1184
- showEnCol && /* @__PURE__ */ jsx("span", {
1185
- className: "col-hdr",
1186
- children: "EN"
1187
- }),
1188
- showTransCol && /* @__PURE__ */ jsx("span", {
1189
- className: "col-hdr",
1190
- children: lang
1191
- })
1192
- ]
1193
- }),
1194
- blocks.length === 0 && /* @__PURE__ */ jsx("div", {
1195
- className: "preview-loading",
1196
- children: "Loading..."
1197
- }),
1198
- blocks.map((block, i) => {
1199
- const isGap = !block.md5;
1200
- const isBlank = isGap && !block.source.trim();
1201
- const isHighlighted = block.md5 && block.md5 === highlightMd5;
1202
- const h = extractHeading(block.source, "b", i);
1203
- return /* @__PURE__ */ jsxs("div", {
1204
- id: block.md5 ? `block-${block.md5.slice(0, 8)}` : void 0,
1205
- className: `block-row${isHighlighted ? " block-highlight" : ""}${isBlank ? " block-blank" : ""}${isGap && !isBlank ? " block-gap" : ""}`,
1206
- style: { gridTemplateColumns: gridCols },
1207
- children: [
1208
- showGutter && /* @__PURE__ */ jsx("span", {
1209
- className: "block-gutter",
1210
- children: block.md5 ? /* @__PURE__ */ jsx("code", {
1211
- className: `gutter-md5 ${block.translation != null ? "done" : "miss"}`,
1212
- title: `${block.type} · ${block.md5}`,
1213
- children: block.md5.slice(0, 6)
1214
- }) : isBlank ? /* @__PURE__ */ jsx("span", { className: "gutter-blank" }) : /* @__PURE__ */ jsx("span", {
1215
- className: "gutter-gap",
1216
- children: block.type
1217
- })
1218
- }),
1219
- showEnCol && /* @__PURE__ */ jsx("pre", {
1220
- className: `block-cell${h ? " block-heading" : ""}`,
1221
- id: h?.id,
1222
- children: block.source
1223
- }),
1224
- showTransCol && /* @__PURE__ */ jsx("pre", {
1225
- className: `block-cell${h ? " block-heading" : ""}${block.md5 && block.translation == null ? " block-missing" : ""}`,
1226
- onContextMenu: (e) => {
1227
- if (!block.md5 || isEn) return;
1228
- e.preventDefault();
1229
- setCtxMenu({
1230
- x: e.clientX,
1231
- y: e.clientY,
1232
- md5: block.md5,
1233
- type: block.type
1234
- });
1235
- },
1236
- children: block.translation != null ? block.translation : block.source
1237
- })
1238
- ]
1239
- }, i);
1240
- })
1241
- ]
1242
- }), showToc && headings.length > 0 && /* @__PURE__ */ jsxs("div", {
1243
- className: "preview-toc",
1244
- children: [/* @__PURE__ */ jsx("div", {
1245
- className: "preview-toc-title",
1246
- children: "On this page"
1247
- }), headings.map((h, idx) => /* @__PURE__ */ jsx("a", {
1248
- href: `#${h.id}`,
1249
- className: `h${h.level}`,
1250
- onClick: (e) => {
1251
- e.preventDefault();
1252
- scrollToHeading(idx);
1253
- },
1254
- children: h.text
1255
- }, h.id))]
1256
- })]
1257
- }),
1258
- ctxMenu && /* @__PURE__ */ jsxs("div", {
1259
- className: "ctx-menu",
1260
- style: {
1261
- left: ctxMenu.x,
1262
- top: ctxMenu.y
1263
- },
1264
- onClick: (e) => e.stopPropagation(),
1265
- children: [
1266
- /* @__PURE__ */ jsxs("div", {
1267
- className: "ctx-menu-header",
1268
- children: [/* @__PURE__ */ jsxs("code", { children: [ctxMenu.md5.slice(0, 12), "…"] }), /* @__PURE__ */ jsx("span", {
1269
- className: "ctx-menu-type",
1270
- children: ctxMenu.type
1271
- })]
1272
- }),
1273
- /* @__PURE__ */ jsx("button", {
1274
- type: "button",
1275
- onClick: () => {
1276
- navigator.clipboard.writeText(ctxMenu.md5);
1277
- setCtxMenu(null);
1278
- },
1279
- children: "📋 Copy MD5"
1280
- }),
1281
- /* @__PURE__ */ jsx("button", {
1282
- type: "button",
1283
- className: "ctx-menu-danger",
1284
- onClick: () => deleteCache.mutate(ctxMenu.md5),
1285
- children: "🗑️ Delete cache"
1286
- })
1287
- ]
1288
- })
1289
- ]
1290
- });
1291
- }
1292
- //#endregion
1293
- //#region app/routes/index.tsx?tsr-split=component
1294
- /**
1295
- * Parse version keys into project/version structure.
1296
- * Multi-project keys look like "query/v5", single-project keys look like "v5".
1297
- */
1298
- function parseProjectVersions(versionKeys) {
1299
- if (!versionKeys.some((k) => k.includes("/"))) return {
1300
- projects: [{
1301
- id: "_default",
1302
- versions: versionKeys
1303
- }],
1304
- isMultiProject: false
1305
- };
1306
- const projectMap = /* @__PURE__ */ new Map();
1307
- for (const key of versionKeys) {
1308
- const slashIdx = key.indexOf("/");
1309
- const projectId = slashIdx >= 0 ? key.slice(0, slashIdx) : "_default";
1310
- const version = slashIdx >= 0 ? key.slice(slashIdx + 1) : key;
1311
- if (!projectMap.has(projectId)) projectMap.set(projectId, []);
1312
- projectMap.get(projectId).push(version);
1313
- }
1314
- return {
1315
- projects: Array.from(projectMap.entries()).map(([id, versions]) => ({
1316
- id,
1317
- versions
1318
- })),
1319
- isMultiProject: true
1320
- };
1321
- }
1322
- function AdminPage() {
1323
- const search = Route.useSearch();
1324
- const navigate = useNavigate({ from: "/" });
1325
- const lang = search.lang || null;
1326
- const file = search.file || null;
1327
- const showFiles = search.files !== "0";
1328
- const view = search.view || "lang";
1329
- const toc = search.toc !== "0";
1330
- const nodes = search.nodes === "1";
1331
- const status = search.status || "all";
1332
- const section = search.section || "all";
1333
- const [theme, setThemeState] = useState(() => {
1334
- if (typeof window === "undefined") return "dark";
1335
- return localStorage.getItem("theme") === "light" ? "light" : "dark";
1336
- });
1337
- const [toast, setToast] = useState(null);
1338
- useEffect(() => {
1339
- document.documentElement.dataset.theme = theme;
1340
- }, [theme]);
1341
- useEffect(() => {
1342
- if (!toast) return;
1343
- const t = setTimeout(() => setToast(null), 3e3);
1344
- return () => clearTimeout(t);
1345
- }, [toast]);
1346
- const updateSearch = useCallback((updates) => {
1347
- navigate({
1348
- search: (prev) => {
1349
- const next = { ...prev };
1350
- for (const [k, v] of Object.entries(updates)) if (v === null || v === void 0 || v === "") delete next[k];
1351
- else next[k] = v;
1352
- return next;
1353
- },
1354
- replace: true
1355
- });
1356
- }, [navigate]);
1357
- const setProject = useCallback((p) => updateSearch({
1358
- project: p || void 0,
1359
- v: void 0,
1360
- lang: void 0,
1361
- file: void 0
1362
- }), [updateSearch]);
1363
- const setVersion = useCallback((v) => updateSearch({
1364
- v: v || void 0,
1365
- lang: void 0,
1366
- file: void 0
1367
- }), [updateSearch]);
1368
- const setLang = useCallback((l) => updateSearch({ lang: l || void 0 }), [updateSearch]);
1369
- const setFile = useCallback((f) => updateSearch({ file: f || void 0 }), [updateSearch]);
1370
- const setShowFiles = useCallback((show) => updateSearch({ files: show ? void 0 : "0" }), [updateSearch]);
1371
- const setView = useCallback((m) => updateSearch({ view: m === "lang" ? void 0 : m }), [updateSearch]);
1372
- const setNodes = useCallback((show) => updateSearch({ nodes: show ? "1" : void 0 }), [updateSearch]);
1373
- const setToc = useCallback((show) => updateSearch({ toc: show ? void 0 : "0" }), [updateSearch]);
1374
- const setStatusFilter = useCallback((s) => updateSearch({ status: s === "all" ? void 0 : s }), [updateSearch]);
1375
- const setSectionFilter = useCallback((s) => updateSearch({ section: s === "all" ? void 0 : s }), [updateSearch]);
1376
- const [selected, setSelected] = useState(/* @__PURE__ */ new Set());
1377
- const [showDialog, setShowDialog] = useState(false);
1378
- const [dialogFiles, setDialogFiles] = useState();
1379
- const { data: statusData } = useQuery({
1380
- queryKey: ["status"],
1381
- queryFn: api.status
1382
- });
1383
- const { data: versionInfo } = useQuery({
1384
- queryKey: ["version"],
1385
- queryFn: api.version,
1386
- staleTime: Number.POSITIVE_INFINITY
1387
- });
1388
- const { projectList, isMultiProject, activeProject, activeVersions, version } = useMemo(() => {
1389
- if (!statusData) return {
1390
- projectList: [],
1391
- isMultiProject: false,
1392
- activeProject: null,
1393
- activeVersions: [],
1394
- version: "latest"
1395
- };
1396
- const parsed = parseProjectVersions(statusData.versions);
1397
- const projectList = parsed.projects;
1398
- const isMultiProject = parsed.isMultiProject;
1399
- let activeProject = isMultiProject ? search.project || projectList[0]?.id || null : null;
1400
- if (isMultiProject && activeProject && !projectList.find((p) => p.id === activeProject)) activeProject = projectList[0]?.id || null;
1401
- const activeVersions = isMultiProject ? projectList.find((p) => p.id === activeProject)?.versions || [] : statusData.versions;
1402
- const rawVersion = search.v || activeVersions[0] || "latest";
1403
- const version = isMultiProject && activeProject ? `${activeProject}/${rawVersion}` : rawVersion;
1404
- return {
1405
- projectList,
1406
- isMultiProject,
1407
- activeProject,
1408
- activeVersions,
1409
- version
1410
- };
1411
- }, [
1412
- statusData,
1413
- search.project,
1414
- search.v
1415
- ]);
1416
- const displayVersion = isMultiProject && activeProject && version.startsWith(`${activeProject}/`) ? version.slice(activeProject.length + 1) : version;
1417
- const { data: files } = useQuery({
1418
- queryKey: [
1419
- "files",
1420
- version,
1421
- lang
1422
- ],
1423
- queryFn: () => api.fileCoverage(version, lang),
1424
- enabled: !!lang
1425
- });
1426
- const handleSelectProject = useCallback((p) => {
1427
- setProject(p);
1428
- setSelected(/* @__PURE__ */ new Set());
1429
- }, [setProject]);
1430
- const handleSelectVersion = useCallback((v) => {
1431
- setVersion(v);
1432
- setSelected(/* @__PURE__ */ new Set());
1433
- }, [setVersion]);
1434
- const handleSelectLang = useCallback((l) => {
1435
- setLang(l);
1436
- setSelected(/* @__PURE__ */ new Set());
1437
- }, [setLang]);
1438
- const handleToggle = useCallback((f) => {
1439
- setSelected((prev) => {
1440
- const next = new Set(prev);
1441
- if (next.has(f)) next.delete(f);
1442
- else next.add(f);
1443
- return next;
1444
- });
1445
- }, []);
1446
- const handleSelectAll = useCallback((fileList) => setSelected(new Set(fileList)), []);
1447
- const handleClear = useCallback(() => setSelected(/* @__PURE__ */ new Set()), []);
1448
- const handleTranslateSelected = useCallback(() => {
1449
- setDialogFiles([...selected]);
1450
- setShowDialog(true);
1451
- }, [selected]);
1452
- const handleNewJob = useCallback(() => {
1453
- setDialogFiles(void 0);
1454
- setShowDialog(true);
1455
- }, []);
1456
- if (!statusData) return /* @__PURE__ */ jsx("div", {
1457
- className: "loading",
1458
- children: "Loading..."
1459
- });
1460
- return /* @__PURE__ */ jsxs(Fragment, { children: [
1461
- /* @__PURE__ */ jsxs("nav", { children: [
1462
- /* @__PURE__ */ jsxs("h1", { children: ["🌐 Translation Admin ", versionInfo?.version && /* @__PURE__ */ jsxs("span", {
1463
- className: "version-badge",
1464
- children: ["v", versionInfo.version]
1465
- })] }),
1466
- /* @__PURE__ */ jsx("span", { className: "spacer" }),
1467
- /* @__PURE__ */ jsx("button", {
1468
- type: "button",
1469
- className: "btn",
1470
- onClick: handleNewJob,
1471
- children: "+ New Job"
1472
- }),
1473
- /* @__PURE__ */ jsx("button", {
1474
- type: "button",
1475
- className: "btn btn-icon",
1476
- onClick: () => {
1477
- const next = theme === "light" ? "dark" : "light";
1478
- setThemeState(next);
1479
- localStorage.setItem("theme", next);
1480
- },
1481
- title: "Toggle theme",
1482
- children: theme === "light" ? "🌙" : "☀️"
1483
- })
1484
- ] }),
1485
- /* @__PURE__ */ jsxs("div", {
1486
- className: "container",
1487
- children: [
1488
- isMultiProject && /* @__PURE__ */ jsx("div", {
1489
- className: "project-tabs",
1490
- children: projectList.map((p) => /* @__PURE__ */ jsx("button", {
1491
- type: "button",
1492
- className: `project-tab${p.id === activeProject ? " active" : ""}`,
1493
- onClick: () => handleSelectProject(p.id),
1494
- children: p.id
1495
- }, p.id))
1496
- }),
1497
- /* @__PURE__ */ jsx("div", {
1498
- className: "tabs",
1499
- children: activeVersions.map((v) => /* @__PURE__ */ jsx("button", {
1500
- type: "button",
1501
- className: `tab${v === displayVersion ? " active" : ""}`,
1502
- onClick: () => handleSelectVersion(v),
1503
- children: v
1504
- }, v))
1505
- }),
1506
- /* @__PURE__ */ jsx(LangGrid, {
1507
- data: statusData,
1508
- version,
1509
- selectedLang: lang,
1510
- onSelect: handleSelectLang
1511
- }),
1512
- /* @__PURE__ */ jsx(JobPanel, {}),
1513
- lang && files && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
1514
- className: "file-panel-toolbar",
1515
- children: [/* @__PURE__ */ jsx("button", {
1516
- type: "button",
1517
- className: `btn btn-sm${showFiles ? " active" : ""}`,
1518
- onClick: () => setShowFiles(!showFiles),
1519
- children: showFiles ? "◀ Hide files" : "▶ Show files"
1520
- }), file && /* @__PURE__ */ jsx("span", {
1521
- className: "file-panel-current",
1522
- children: file
1523
- })]
1524
- }), /* @__PURE__ */ jsxs("div", {
1525
- className: `file-panel${!showFiles ? " no-list" : ""}${!file ? " no-preview" : ""}`,
1526
- children: [showFiles && /* @__PURE__ */ jsx(FileList, {
1527
- files,
1528
- lang,
1529
- activeFile: file,
1530
- selected,
1531
- statusFilter: status,
1532
- sectionFilter: section,
1533
- onStatusFilter: setStatusFilter,
1534
- onSectionFilter: setSectionFilter,
1535
- onSelect: setFile,
1536
- onToggle: handleToggle,
1537
- onSelectAll: handleSelectAll,
1538
- onClear: handleClear,
1539
- onTranslateSelected: handleTranslateSelected
1540
- }), file && /* @__PURE__ */ jsx(Preview, {
1541
- version,
1542
- lang,
1543
- file,
1544
- viewMode: view,
1545
- onViewMode: setView,
1546
- showToc: toc,
1547
- onToggleToc: () => setToc(!toc),
1548
- showNodes: nodes,
1549
- onToggleNodes: () => setNodes(!nodes),
1550
- onClose: () => setFile(null)
1551
- })]
1552
- })] })
1553
- ]
1554
- }),
1555
- toast && /* @__PURE__ */ jsx("div", {
1556
- className: "toast",
1557
- children: toast
1558
- }),
1559
- showDialog && /* @__PURE__ */ jsx(JobDialog, {
1560
- langs: statusData.langs,
1561
- versions: statusData.versions,
1562
- defaultLang: lang || void 0,
1563
- defaultVersion: version,
1564
- files: dialogFiles,
1565
- onClose: () => setShowDialog(false),
1566
- onSuccess: (msg) => {
1567
- setShowDialog(false);
1568
- setToast(msg);
1569
- }
1570
- })
1571
- ] });
1572
- }
1573
- //#endregion
1574
- export { AdminPage as component };