docs-i18n 0.7.2 β†’ 0.7.4

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.
@@ -0,0 +1,1574 @@
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 };