@peaske7/readit 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/biome.json +1 -1
  2. package/bun.lock +86 -72
  3. package/docs/plans/2026-03-13-client-mode-design.md +86 -0
  4. package/docs/plans/2026-03-13-client-mode-plan.md +605 -0
  5. package/package.json +12 -11
  6. package/src/App.tsx +23 -6
  7. package/src/cli/index.ts +312 -25
  8. package/src/components/ActionsMenu.tsx +12 -10
  9. package/src/components/DocumentViewer/CodeBlock.tsx +131 -54
  10. package/src/components/DocumentViewer/DocumentViewer.tsx +6 -6
  11. package/src/components/DocumentViewer/InlineCode.tsx +60 -0
  12. package/src/components/FloatingTOC.tsx +4 -2
  13. package/src/components/Header.tsx +3 -1
  14. package/src/components/InlineEditor.tsx +4 -2
  15. package/src/components/MarginNote.tsx +17 -8
  16. package/src/components/RawModal.tsx +9 -7
  17. package/src/components/ReanchorConfirm.tsx +6 -3
  18. package/src/components/SettingsModal.tsx +112 -23
  19. package/src/components/ShortcutCapture.tsx +4 -1
  20. package/src/components/ShortcutList.tsx +50 -9
  21. package/src/components/comments/CommentBadge.tsx +7 -1
  22. package/src/components/comments/CommentInput.tsx +13 -18
  23. package/src/components/comments/CommentListItem.tsx +15 -5
  24. package/src/components/comments/CommentManager.tsx +14 -7
  25. package/src/components/comments/CommentNav.tsx +8 -3
  26. package/src/contexts/CommentContext.tsx +16 -9
  27. package/src/contexts/LayoutContext.tsx +17 -5
  28. package/src/contexts/LocaleContext.tsx +35 -0
  29. package/src/hooks/useClipboard.ts +11 -8
  30. package/src/hooks/useDocument.ts +35 -10
  31. package/src/hooks/useEditorScheme.ts +51 -0
  32. package/src/hooks/useFontPreference.ts +5 -22
  33. package/src/hooks/useKeybindings.ts +6 -18
  34. package/src/hooks/useLocalePreference.ts +42 -0
  35. package/src/index.css +87 -26
  36. package/src/lib/editor-links.ts +59 -0
  37. package/src/lib/highlight/dom.ts +126 -54
  38. package/src/lib/highlight/highlighter.ts +10 -10
  39. package/src/lib/i18n/completeness.test.ts +51 -0
  40. package/src/lib/i18n/en.ts +139 -0
  41. package/src/lib/i18n/index.ts +3 -0
  42. package/src/lib/i18n/ja.ts +141 -0
  43. package/src/lib/i18n/translations.test.ts +39 -0
  44. package/src/lib/i18n/translations.ts +27 -0
  45. package/src/lib/i18n/types.ts +145 -0
  46. package/src/lib/shortcut-registry.ts +1 -1
  47. package/src/lib/utils.ts +11 -0
  48. package/src/main.tsx +4 -1
  49. package/src/server/index.ts +263 -103
  50. package/src/store/index.test.ts +22 -0
  51. package/src/store/index.ts +24 -4
  52. package/src/types/index.ts +12 -0
package/src/App.tsx CHANGED
@@ -13,6 +13,7 @@ import { TableOfContents } from "./components/TableOfContents";
13
13
  import { textVariants } from "./components/ui/Text";
14
14
  import { CommentContext, CommentProvider } from "./contexts/CommentContext";
15
15
  import { LayoutContext, LayoutProvider } from "./contexts/LayoutContext";
16
+ import { useLocale } from "./contexts/LocaleContext";
16
17
  import { useClipboard } from "./hooks/useClipboard";
17
18
  import { useDocument } from "./hooks/useDocument";
18
19
  import { useHeadings } from "./hooks/useHeadings";
@@ -38,6 +39,7 @@ const TOASTER_OPTIONS = {
38
39
  };
39
40
 
40
41
  function AppContent() {
42
+ const { t } = useLocale();
41
43
  const {
42
44
  comments,
43
45
  sortedComments,
@@ -74,6 +76,7 @@ function AppContent() {
74
76
  document: document ?? undefined,
75
77
  selection: selection ?? undefined,
76
78
  clearSelection,
79
+ t,
77
80
  });
78
81
 
79
82
  const { shortcuts, isFullscreen } = use(LayoutContext)!;
@@ -275,6 +278,7 @@ function AppContent() {
275
278
  />
276
279
  ) : (
277
280
  <CommentInput
281
+ key={selection.text}
278
282
  selectedText={selection.text}
279
283
  onSubmit={handleAddComment}
280
284
  onCancel={clearSelection}
@@ -302,7 +306,7 @@ function AppContent() {
302
306
  <CommentNav />
303
307
 
304
308
  <footer className="py-4 text-center text-sm text-zinc-400 dark:text-zinc-500">
305
- Made with ❤️ by Jay and Claude
309
+ {t("app.footer")}
306
310
  </footer>
307
311
  </div>
308
312
  );
@@ -333,6 +337,7 @@ function useTabKeyboardShortcuts() {
333
337
  }
334
338
 
335
339
  function App() {
340
+ const { t } = useLocale();
336
341
  const { document, error, isInitialized } = useDocument();
337
342
  const documentOrder = useAppStore((s) => s.documentOrder);
338
343
 
@@ -349,7 +354,9 @@ function App() {
349
354
  if (!isInitialized) {
350
355
  return (
351
356
  <div className="min-h-screen bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100 flex items-center justify-center">
352
- <div className="text-zinc-500 dark:text-zinc-400">Loading...</div>
357
+ <div className="text-zinc-500 dark:text-zinc-400">
358
+ {t("app.loading")}
359
+ </div>
353
360
  </div>
354
361
  );
355
362
  }
@@ -358,9 +365,17 @@ function App() {
358
365
  return (
359
366
  <div className="min-h-screen bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100 flex flex-col">
360
367
  <TabBar />
361
- <div className="flex-1 flex items-center justify-center">
368
+ <div className="flex-1 flex flex-col items-center justify-center gap-3">
362
369
  <p className="text-zinc-400 dark:text-zinc-500 text-sm">
363
- No documents open.
370
+ {t("app.noDocuments")}
371
+ </p>
372
+ <p className="text-zinc-400 dark:text-zinc-500 text-xs">
373
+ {t("app.noDocumentsHintPrefix")}
374
+ {t("app.noDocumentsHintPrefix") && " "}
375
+ <code className="bg-zinc-100 dark:bg-zinc-800 px-1.5 py-0.5 rounded text-xs">
376
+ readit open &lt;file.md&gt;
377
+ </code>{" "}
378
+ {t("app.noDocumentsHintSuffix")}
364
379
  </p>
365
380
  </div>
366
381
  </div>
@@ -370,7 +385,9 @@ function App() {
370
385
  if (!document) {
371
386
  return (
372
387
  <div className="min-h-screen bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100 flex items-center justify-center">
373
- <div className="text-zinc-500 dark:text-zinc-400">Loading...</div>
388
+ <div className="text-zinc-500 dark:text-zinc-400">
389
+ {t("app.loading")}
390
+ </div>
374
391
  </div>
375
392
  );
376
393
  }
@@ -378,7 +395,7 @@ function App() {
378
395
  return (
379
396
  <>
380
397
  <TabBar />
381
- <LayoutProvider filePath={document.filePath}>
398
+ <LayoutProvider>
382
399
  <CommentProvider
383
400
  filePath={document.filePath}
384
401
  clean={document.clean}
package/src/cli/index.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  lstatSync,
6
6
  readdirSync,
7
7
  readFileSync,
8
+ realpathSync,
8
9
  statSync,
9
10
  } from "node:fs";
10
11
  import * as fs from "node:fs/promises";
@@ -13,22 +14,13 @@ import { join, resolve } from "node:path";
13
14
  import { Command } from "commander";
14
15
  import open from "open";
15
16
  import { getCommentPath, parseCommentFile } from "../lib/comment-storage.js";
17
+ import { getFileType } from "../lib/utils.js";
16
18
  import type { FileEntry } from "../server/index.js";
17
- import { startServer } from "../server/index.js";
19
+ import { removeServerInfo, startServer } from "../server/index.js";
18
20
  import type { DocumentType } from "../types/index.js";
19
21
 
20
22
  const program = new Command();
21
23
 
22
- function getFileType(filePath: string): DocumentType | null {
23
- if (filePath.endsWith(".md") || filePath.endsWith(".markdown")) {
24
- return "markdown";
25
- }
26
- if (filePath.endsWith(".html") || filePath.endsWith(".htm")) {
27
- return "html";
28
- }
29
- return null;
30
- }
31
-
32
24
  function isPermissionError(err: unknown): boolean {
33
25
  return (
34
26
  err instanceof Error &&
@@ -37,6 +29,39 @@ function isPermissionError(err: unknown): boolean {
37
29
  );
38
30
  }
39
31
 
32
+ interface ServerInfo {
33
+ port: number;
34
+ pid: number;
35
+ }
36
+
37
+ async function discoverServer(): Promise<ServerInfo | null> {
38
+ const serverInfoPath = join(os.homedir(), ".readit", "server.json");
39
+
40
+ try {
41
+ const content = readFileSync(serverInfoPath, "utf-8");
42
+ const info: ServerInfo = JSON.parse(content);
43
+
44
+ // Verify the process is alive
45
+ try {
46
+ process.kill(info.pid, 0);
47
+ } catch {
48
+ return null;
49
+ }
50
+
51
+ // Verify health endpoint responds
52
+ try {
53
+ const res = await fetch(`http://127.0.0.1:${info.port}/api/health`);
54
+ if (!res.ok) return null;
55
+ } catch {
56
+ return null;
57
+ }
58
+
59
+ return info;
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
40
65
  /**
41
66
  * Recursively find all .comments.md files in a directory.
42
67
  */
@@ -92,7 +117,6 @@ function findReviewableFiles(dir: string): FileEntry[] {
92
117
  const type = getFileType(entry);
93
118
  if (type) {
94
119
  results.push({
95
- content: readFileSync(fullPath, "utf-8"),
96
120
  type,
97
121
  filePath: fullPath,
98
122
  });
@@ -121,13 +145,15 @@ function resolveFiles(args: string[]): FileEntry[] {
121
145
  const files: FileEntry[] = [];
122
146
 
123
147
  for (const arg of args) {
124
- const filePath = resolve(process.cwd(), arg);
148
+ const inputPath = resolve(process.cwd(), arg);
125
149
 
126
- if (!existsSync(filePath)) {
127
- console.error(`error: not found: ${filePath}`);
150
+ if (!existsSync(inputPath)) {
151
+ console.error(`error: not found: ${inputPath}`);
128
152
  process.exit(1);
129
153
  }
130
154
 
155
+ const filePath = realpathSync(inputPath);
156
+
131
157
  const stat = statSync(filePath);
132
158
 
133
159
  if (stat.isDirectory()) {
@@ -151,7 +177,6 @@ function resolveFiles(args: string[]): FileEntry[] {
151
177
 
152
178
  seen.add(filePath);
153
179
  files.push({
154
- content: readFileSync(filePath, "utf-8"),
155
180
  type,
156
181
  filePath,
157
182
  });
@@ -161,6 +186,114 @@ function resolveFiles(args: string[]): FileEntry[] {
161
186
  return files;
162
187
  }
163
188
 
189
+ // ─── Onboarding ──────────────────────────────────────────────────────
190
+
191
+ const SETTINGS_PATH = join(os.homedir(), ".readit", "settings.json");
192
+
193
+ function isOnboarded(): boolean {
194
+ try {
195
+ const content = readFileSync(SETTINGS_PATH, "utf-8");
196
+ const settings = JSON.parse(content);
197
+ return settings.onboarded === true;
198
+ } catch {
199
+ return false;
200
+ }
201
+ }
202
+
203
+ async function markOnboarded(): Promise<void> {
204
+ let settings: Record<string, unknown> = {};
205
+ try {
206
+ const content = readFileSync(SETTINGS_PATH, "utf-8");
207
+ settings = JSON.parse(content);
208
+ } catch {
209
+ // No existing settings
210
+ }
211
+ settings.onboarded = true;
212
+ const dir = join(os.homedir(), ".readit");
213
+ await fs.mkdir(dir, { recursive: true });
214
+ await fs.writeFile(SETTINGS_PATH, JSON.stringify(settings, null, 2), "utf-8");
215
+ }
216
+
217
+ const WELCOME_CONTENT = `# Welcome to readit
218
+
219
+ A simple tool for reviewing markdown with inline comments.
220
+
221
+ ---
222
+
223
+ ## How It Works
224
+
225
+ readit follows a simple loop: **read → comment → extract**.
226
+
227
+ ### 1. Read
228
+
229
+ You're already doing this. Open any markdown file with \`readit <file.md>\` and it renders in your browser with a clean reading experience.
230
+
231
+ ### 2. Comment
232
+
233
+ Select any text to add a comment. Try it now — **select this sentence** and type your first comment.
234
+
235
+ Your comments appear as margin notes next to the highlighted text, just like reviewing a document in Google Docs. Add as many as you need.
236
+
237
+ ### 3. Extract
238
+
239
+ When you're done reviewing, click the menu in the top-right and choose **Copy as Prompt**. This exports all your comments in a format ready for Claude, ChatGPT, or any AI assistant.
240
+
241
+ You can also export as JSON if you prefer structured data.
242
+
243
+ ---
244
+
245
+ ## Everything is Plain Markdown
246
+
247
+ Your comments are saved as \`.comments.md\` files in \`~/.readit/comments/\`. No database, no lock-in — just readable markdown files you can version control, search, or edit by hand.
248
+
249
+ Each comment file looks something like this:
250
+
251
+ \`\`\`markdown
252
+ ## Comment 1
253
+ **Selected:** "select this sentence"
254
+ **Comment:** This is my first comment!
255
+ **Created:** 2024-01-15T10:30:00Z
256
+ \`\`\`
257
+
258
+ ---
259
+
260
+ ## Navigating Comments
261
+
262
+ Once you have multiple comments, use the navigation bar at the bottom of the screen to jump between them. You can also use keyboard shortcuts:
263
+
264
+ | Shortcut | Action |
265
+ |----------|--------|
266
+ | \`Alt + ↑\` | Previous comment |
267
+ | \`Alt + ↓\` | Next comment |
268
+ | \`⌘ + C\` | Copy selected text (raw) |
269
+ | \`⌘ + Shift + C\` | Copy selected text with context (for AI) |
270
+
271
+ ---
272
+
273
+ ## Quick Start
274
+
275
+ \`\`\`bash
276
+ # Review a markdown file
277
+ readit document.md
278
+
279
+ # Use a custom port
280
+ readit document.md --port 3000
281
+
282
+ # Start fresh (clear existing comments)
283
+ readit document.md --clean
284
+ \`\`\`
285
+
286
+ ---
287
+
288
+ ## Try It Now
289
+
290
+ Go ahead and add a few comments to this document. When you're done, export them and see the output. That's the entire workflow — simple, transparent, and designed for reviewing AI-generated content.
291
+ `;
292
+
293
+ const WELCOME_PATH = join(os.homedir(), ".readit", "welcome.md");
294
+
295
+ // ─── Program ─────────────────────────────────────────────────────────
296
+
164
297
  program
165
298
  .name("readit")
166
299
  .description("Review Markdown and HTML documents with inline comments")
@@ -249,9 +382,9 @@ program
249
382
  }
250
383
  });
251
384
 
252
- // Main review command (default) — accepts one or more files/directories
385
+ // Main review command (default) — accepts zero or more files/directories
253
386
  program
254
- .argument("<files...>", "Markdown or HTML files/directories to review")
387
+ .argument("[files...]", "Markdown or HTML files/directories to review")
255
388
  .option("-p, --port <number>", "Port to run server on", "4567")
256
389
  .option("--host <address>", "Host address to bind to", "127.0.0.1")
257
390
  .option("--no-open", "Don't automatically open browser")
@@ -266,11 +399,27 @@ program
266
399
  clean: boolean;
267
400
  },
268
401
  ) => {
269
- const files = resolveFiles(fileArgs);
402
+ let files: FileEntry[];
270
403
 
271
- if (files.length === 0) {
272
- console.error("error: no reviewable files found");
273
- process.exit(1);
404
+ if (fileArgs.length === 0) {
405
+ if (isOnboarded()) {
406
+ files = [];
407
+ } else {
408
+ files = [
409
+ {
410
+ content: WELCOME_CONTENT,
411
+ type: "markdown" as DocumentType,
412
+ filePath: WELCOME_PATH,
413
+ },
414
+ ];
415
+ }
416
+ } else {
417
+ files = resolveFiles(fileArgs);
418
+
419
+ if (files.length === 0) {
420
+ console.error("error: no reviewable files found");
421
+ process.exit(1);
422
+ }
274
423
  }
275
424
 
276
425
  const preferredPort = Number.parseInt(options.port, 10);
@@ -292,9 +441,21 @@ program
292
441
  clean: options.clean,
293
442
  });
294
443
 
295
- const fileList = files.map((f) => ` ${f.filePath} (${f.type})`);
444
+ if (files.length === 0) {
445
+ console.log(`
446
+ readit - Document Review Tool
296
447
 
297
- console.log(`
448
+ URL: ${url}
449
+
450
+ No files specified. Add files with:
451
+ readit open <file.md>
452
+
453
+ Server running. Press Ctrl+C to stop.
454
+ `);
455
+ } else {
456
+ const fileList = files.map((f) => ` ${f.filePath} (${f.type})`);
457
+
458
+ console.log(`
298
459
  readit - Document Review Tool
299
460
 
300
461
  ${files.length === 1 ? "File:" : "Files:"}
@@ -304,15 +465,141 @@ ${fileList.join("\n")}
304
465
  Server running. Close browser tab to stop.
305
466
  Press Ctrl+C to force stop.
306
467
  `);
468
+ }
307
469
 
308
470
  if (options.open) {
309
471
  open(url);
310
472
  }
311
473
 
474
+ // Mark onboarding complete on first server start
475
+ if (fileArgs.length === 0) {
476
+ await markOnboarded();
477
+ }
478
+
312
479
  // Graceful shutdown on Ctrl+C
313
- process.on("SIGINT", () => {
480
+ process.on("SIGINT", async () => {
314
481
  console.log("\n\nShutting down...");
315
482
  server.stop();
483
+ await removeServerInfo();
484
+ process.exit(0);
485
+ });
486
+ } catch (error) {
487
+ console.error(
488
+ "error: failed to start server:",
489
+ error instanceof Error ? error.message : error,
490
+ );
491
+ process.exit(1);
492
+ }
493
+ },
494
+ );
495
+
496
+ // Open command: add files to running server or start new one
497
+ program
498
+ .command("open")
499
+ .argument("<files...>", "Markdown or HTML files to add to running server")
500
+ .description("Add files to a running readit server, or start a new one")
501
+ .option("-p, --port <number>", "Port for new server (if starting)", "4567")
502
+ .option("--host <address>", "Host for new server (if starting)", "127.0.0.1")
503
+ .action(
504
+ async (fileArgs: string[], options: { port: string; host: string }) => {
505
+ // Resolve and validate files
506
+ const resolvedFiles: { path: string; type: DocumentType }[] = [];
507
+ for (const arg of fileArgs) {
508
+ const inputPath = resolve(process.cwd(), arg);
509
+
510
+ if (!existsSync(inputPath)) {
511
+ console.error(`error: not found: ${inputPath}`);
512
+ process.exit(1);
513
+ }
514
+
515
+ const filePath = realpathSync(inputPath);
516
+
517
+ const type = getFileType(filePath);
518
+ if (!type) {
519
+ console.error(
520
+ `error: unsupported file type: ${arg} (expected .md, .markdown, .html, or .htm)`,
521
+ );
522
+ process.exit(1);
523
+ }
524
+
525
+ resolvedFiles.push({ path: filePath, type });
526
+ }
527
+
528
+ // Try to find running server
529
+ const server = await discoverServer();
530
+
531
+ if (server) {
532
+ // Send files to running server
533
+ for (const file of resolvedFiles) {
534
+ try {
535
+ const res = await fetch(
536
+ `http://127.0.0.1:${server.port}/api/documents`,
537
+ {
538
+ method: "POST",
539
+ headers: { "Content-Type": "application/json" },
540
+ body: JSON.stringify({ path: file.path }),
541
+ },
542
+ );
543
+
544
+ if (!res.ok) {
545
+ const data = await res.json();
546
+ console.error(`error: failed to add ${file.path}: ${data.error}`);
547
+ process.exit(1);
548
+ }
549
+
550
+ const data = await res.json();
551
+ if (data.status === "added") {
552
+ console.log(`Added: ${data.fileName} (${data.type})`);
553
+ } else {
554
+ console.log(`Present: ${data.fileName} (${data.type})`);
555
+ }
556
+ } catch (err) {
557
+ console.error(
558
+ "error: failed to connect to server:",
559
+ err instanceof Error ? err.message : err,
560
+ );
561
+ process.exit(1);
562
+ }
563
+ }
564
+
565
+ console.log(`\nServer: http://127.0.0.1:${server.port}`);
566
+ return;
567
+ }
568
+
569
+ // No running server — start one
570
+ console.log("No running server found, starting new one...\n");
571
+
572
+ const files = resolvedFiles.map((f) => ({
573
+ type: f.type,
574
+ filePath: f.path,
575
+ }));
576
+
577
+ const preferredPort = Number.parseInt(options.port, 10);
578
+ try {
579
+ const { url, server: newServer } = await startServer({
580
+ files,
581
+ port: preferredPort,
582
+ host: options.host,
583
+ });
584
+
585
+ const fileList = files.map((f) => ` ${f.filePath} (${f.type})`);
586
+ console.log(`
587
+ readit - Document Review Tool
588
+
589
+ ${files.length === 1 ? "File:" : "Files:"}
590
+ ${fileList.join("\n")}
591
+ URL: ${url}
592
+
593
+ Server running. Close browser tab to stop.
594
+ Press Ctrl+C to force stop.
595
+ `);
596
+
597
+ open(url);
598
+
599
+ process.on("SIGINT", async () => {
600
+ console.log("\n\nShutting down...");
601
+ newServer.stop();
602
+ await removeServerInfo();
316
603
  process.exit(0);
317
604
  });
318
605
  } catch (error) {
@@ -12,6 +12,7 @@ import {
12
12
  import { useState } from "react";
13
13
  import { useCommentContext } from "../contexts/CommentContext";
14
14
  import { useLayoutContext } from "../contexts/LayoutContext";
15
+ import { useLocale } from "../contexts/LocaleContext";
15
16
  import { RawModal } from "./RawModal";
16
17
  import { SettingsModal } from "./SettingsModal";
17
18
  import { Button } from "./ui/Button";
@@ -38,6 +39,7 @@ export function ActionsMenu({
38
39
  }: ActionsMenuProps) {
39
40
  const { commentCount } = useCommentContext();
40
41
  const { isFullscreen, toggleLayoutMode } = useLayoutContext();
42
+ const { t } = useLocale();
41
43
 
42
44
  const [menuOpen, setMenuOpen] = useState(false);
43
45
  const [rawModalOpen, setRawModalOpen] = useState(false);
@@ -51,7 +53,7 @@ export function ActionsMenu({
51
53
  variant="ghost"
52
54
  size="icon"
53
55
  className="size-7"
54
- aria-label="Actions menu"
56
+ aria-label={t("actions.ariaLabel")}
55
57
  >
56
58
  <MoreHorizontal className="w-4 h-4" />
57
59
  </Button>
@@ -59,40 +61,40 @@ export function ActionsMenu({
59
61
  <DropdownMenuContent align="end" className="min-w-[160px]">
60
62
  <DropdownMenuItem onSelect={() => toggleLayoutMode()}>
61
63
  {isFullscreen ? <Minimize2 /> : <Maximize2 />}
62
- {isFullscreen ? "Centered" : "Fullscreen"}
64
+ {isFullscreen ? t("actions.centered") : t("actions.fullscreen")}
63
65
  </DropdownMenuItem>
64
66
  <DropdownMenuItem onSelect={() => setSettingsOpen(true)}>
65
67
  <Settings />
66
- Settings
68
+ {t("actions.settings")}
67
69
  </DropdownMenuItem>
68
70
  <DropdownMenuSeparator />
69
71
  <DropdownMenuItem onSelect={() => onReload()}>
70
72
  <RefreshCw />
71
- Reload
73
+ {t("actions.reload")}
72
74
  </DropdownMenuItem>
73
75
  {commentCount > 0 && (
74
76
  <>
75
77
  <DropdownMenuItem
76
78
  onSelect={() => onCopyAll()}
77
- title="Copy in prompt format for AI assistants"
79
+ title={t("actions.copyAllAITitle")}
78
80
  >
79
81
  <BotMessageSquare />
80
- Copy All (AI)
82
+ {t("actions.copyAllAI")}
81
83
  </DropdownMenuItem>
82
84
  <DropdownMenuItem
83
85
  onSelect={() => onCopyAllRaw()}
84
- title="Copy as plain text"
86
+ title={t("actions.copyAllRawTitle")}
85
87
  >
86
88
  <TextQuote />
87
- Copy All (Raw)
89
+ {t("actions.copyAllRaw")}
88
90
  </DropdownMenuItem>
89
91
  <DropdownMenuItem onSelect={() => onExportJson()}>
90
92
  <FileDown />
91
- Export JSON
93
+ {t("actions.exportJson")}
92
94
  </DropdownMenuItem>
93
95
  <DropdownMenuItem onSelect={() => setRawModalOpen(true)}>
94
96
  <FileText />
95
- View Raw
97
+ {t("actions.viewRaw")}
96
98
  </DropdownMenuItem>
97
99
  </>
98
100
  )}