@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
@@ -1,75 +1,136 @@
1
- import { PrismLight as SyntaxHighlighter } from "react-syntax-highlighter";
2
- // Import only the languages we need (reduces bundle by ~800KB)
3
- import bash from "react-syntax-highlighter/dist/esm/languages/prism/bash";
4
- import css from "react-syntax-highlighter/dist/esm/languages/prism/css";
5
- import diff from "react-syntax-highlighter/dist/esm/languages/prism/diff";
6
- import go from "react-syntax-highlighter/dist/esm/languages/prism/go";
7
- import graphql from "react-syntax-highlighter/dist/esm/languages/prism/graphql";
8
- import javascript from "react-syntax-highlighter/dist/esm/languages/prism/javascript";
9
- import json from "react-syntax-highlighter/dist/esm/languages/prism/json";
10
- import jsx from "react-syntax-highlighter/dist/esm/languages/prism/jsx";
11
- import markdown from "react-syntax-highlighter/dist/esm/languages/prism/markdown";
12
- import python from "react-syntax-highlighter/dist/esm/languages/prism/python";
13
- import rust from "react-syntax-highlighter/dist/esm/languages/prism/rust";
14
- import sql from "react-syntax-highlighter/dist/esm/languages/prism/sql";
15
- import tsx from "react-syntax-highlighter/dist/esm/languages/prism/tsx";
16
- import typescript from "react-syntax-highlighter/dist/esm/languages/prism/typescript";
17
- import yaml from "react-syntax-highlighter/dist/esm/languages/prism/yaml";
18
- import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
1
+ import { useEffect, useState } from "react";
19
2
  import { MermaidDiagram } from "./MermaidDiagram";
20
3
 
21
- // Register languages
22
- SyntaxHighlighter.registerLanguage("bash", bash);
23
- SyntaxHighlighter.registerLanguage("sh", bash);
24
- SyntaxHighlighter.registerLanguage("shell", bash);
25
- SyntaxHighlighter.registerLanguage("css", css);
26
- SyntaxHighlighter.registerLanguage("diff", diff);
27
- SyntaxHighlighter.registerLanguage("go", go);
28
- SyntaxHighlighter.registerLanguage("graphql", graphql);
29
- SyntaxHighlighter.registerLanguage("javascript", javascript);
30
- SyntaxHighlighter.registerLanguage("js", javascript);
31
- SyntaxHighlighter.registerLanguage("json", json);
32
- SyntaxHighlighter.registerLanguage("jsx", jsx);
33
- SyntaxHighlighter.registerLanguage("markdown", markdown);
34
- SyntaxHighlighter.registerLanguage("md", markdown);
35
- SyntaxHighlighter.registerLanguage("python", python);
36
- SyntaxHighlighter.registerLanguage("py", python);
37
- SyntaxHighlighter.registerLanguage("rust", rust);
38
- SyntaxHighlighter.registerLanguage("rs", rust);
39
- SyntaxHighlighter.registerLanguage("sql", sql);
40
- SyntaxHighlighter.registerLanguage("tsx", tsx);
41
- SyntaxHighlighter.registerLanguage("typescript", typescript);
42
- SyntaxHighlighter.registerLanguage("ts", typescript);
43
- SyntaxHighlighter.registerLanguage("yaml", yaml);
44
- SyntaxHighlighter.registerLanguage("yml", yaml);
45
-
46
4
  const CODE_BLOCK_STYLE = {
47
5
  margin: "1.5em 0",
48
6
  borderRadius: "0.5em",
49
7
  fontSize: "0.875em",
50
8
  };
51
9
 
10
+ interface SyntaxHighlighterModule {
11
+ SyntaxHighlighter: typeof import("react-syntax-highlighter").PrismLight;
12
+ oneDark: typeof import("react-syntax-highlighter/dist/esm/styles/prism").oneDark;
13
+ }
14
+
52
15
  interface CodeBlockProps {
53
16
  className?: string;
54
17
  children?: React.ReactNode;
55
18
  }
56
19
 
57
- export function CodeBlock({ className, children }: CodeBlockProps) {
58
- // Extract language from className (e.g., "language-typescript" -> "typescript")
59
- const langMatch = className?.match(/language-(\w+)/);
60
- const language = langMatch?.[1] ?? "";
61
- const codeString = String(children).replace(/\n$/, "");
20
+ let syntaxHighlighterPromise: Promise<SyntaxHighlighterModule> | null = null;
62
21
 
63
- // Mermaid diagrams
64
- if (language === "mermaid") {
65
- return <MermaidDiagram code={codeString} />;
22
+ async function loadSyntaxHighlighter(): Promise<SyntaxHighlighterModule> {
23
+ if (syntaxHighlighterPromise) {
24
+ return syntaxHighlighterPromise;
66
25
  }
67
26
 
68
- // Inline code (no language specified and no newlines)
69
- if (!langMatch && !String(children).includes("\n")) {
70
- return <code className={className}>{children}</code>;
27
+ syntaxHighlighterPromise = Promise.all([
28
+ import("react-syntax-highlighter"),
29
+ import("react-syntax-highlighter/dist/esm/styles/prism"),
30
+ import("react-syntax-highlighter/dist/esm/languages/prism/bash"),
31
+ import("react-syntax-highlighter/dist/esm/languages/prism/css"),
32
+ import("react-syntax-highlighter/dist/esm/languages/prism/diff"),
33
+ import("react-syntax-highlighter/dist/esm/languages/prism/go"),
34
+ import("react-syntax-highlighter/dist/esm/languages/prism/graphql"),
35
+ import("react-syntax-highlighter/dist/esm/languages/prism/javascript"),
36
+ import("react-syntax-highlighter/dist/esm/languages/prism/json"),
37
+ import("react-syntax-highlighter/dist/esm/languages/prism/jsx"),
38
+ import("react-syntax-highlighter/dist/esm/languages/prism/markdown"),
39
+ import("react-syntax-highlighter/dist/esm/languages/prism/python"),
40
+ import("react-syntax-highlighter/dist/esm/languages/prism/rust"),
41
+ import("react-syntax-highlighter/dist/esm/languages/prism/sql"),
42
+ import("react-syntax-highlighter/dist/esm/languages/prism/tsx"),
43
+ import("react-syntax-highlighter/dist/esm/languages/prism/typescript"),
44
+ import("react-syntax-highlighter/dist/esm/languages/prism/yaml"),
45
+ ]).then(
46
+ ([
47
+ syntaxModule,
48
+ styleModule,
49
+ bash,
50
+ css,
51
+ diff,
52
+ go,
53
+ graphql,
54
+ javascript,
55
+ json,
56
+ jsx,
57
+ markdown,
58
+ python,
59
+ rust,
60
+ sql,
61
+ tsx,
62
+ typescript,
63
+ yaml,
64
+ ]) => {
65
+ const SyntaxHighlighter = syntaxModule.PrismLight;
66
+
67
+ SyntaxHighlighter.registerLanguage("bash", bash.default);
68
+ SyntaxHighlighter.registerLanguage("sh", bash.default);
69
+ SyntaxHighlighter.registerLanguage("shell", bash.default);
70
+ SyntaxHighlighter.registerLanguage("css", css.default);
71
+ SyntaxHighlighter.registerLanguage("diff", diff.default);
72
+ SyntaxHighlighter.registerLanguage("go", go.default);
73
+ SyntaxHighlighter.registerLanguage("graphql", graphql.default);
74
+ SyntaxHighlighter.registerLanguage("javascript", javascript.default);
75
+ SyntaxHighlighter.registerLanguage("js", javascript.default);
76
+ SyntaxHighlighter.registerLanguage("json", json.default);
77
+ SyntaxHighlighter.registerLanguage("jsx", jsx.default);
78
+ SyntaxHighlighter.registerLanguage("markdown", markdown.default);
79
+ SyntaxHighlighter.registerLanguage("md", markdown.default);
80
+ SyntaxHighlighter.registerLanguage("python", python.default);
81
+ SyntaxHighlighter.registerLanguage("py", python.default);
82
+ SyntaxHighlighter.registerLanguage("rust", rust.default);
83
+ SyntaxHighlighter.registerLanguage("rs", rust.default);
84
+ SyntaxHighlighter.registerLanguage("sql", sql.default);
85
+ SyntaxHighlighter.registerLanguage("tsx", tsx.default);
86
+ SyntaxHighlighter.registerLanguage("typescript", typescript.default);
87
+ SyntaxHighlighter.registerLanguage("ts", typescript.default);
88
+ SyntaxHighlighter.registerLanguage("yaml", yaml.default);
89
+ SyntaxHighlighter.registerLanguage("yml", yaml.default);
90
+
91
+ return {
92
+ SyntaxHighlighter,
93
+ oneDark: styleModule.oneDark,
94
+ };
95
+ },
96
+ );
97
+
98
+ return syntaxHighlighterPromise;
99
+ }
100
+
101
+ function LazySyntaxCodeBlock({
102
+ codeString,
103
+ language,
104
+ }: {
105
+ codeString: string;
106
+ language: string;
107
+ }) {
108
+ const [module, setModule] = useState<SyntaxHighlighterModule | null>(null);
109
+
110
+ useEffect(() => {
111
+ let cancelled = false;
112
+
113
+ loadSyntaxHighlighter().then((loaded) => {
114
+ if (!cancelled) {
115
+ setModule(loaded);
116
+ }
117
+ });
118
+
119
+ return () => {
120
+ cancelled = true;
121
+ };
122
+ }, []);
123
+
124
+ if (!module) {
125
+ return (
126
+ <pre style={CODE_BLOCK_STYLE}>
127
+ <code>{codeString}</code>
128
+ </pre>
129
+ );
71
130
  }
72
131
 
132
+ const { SyntaxHighlighter, oneDark } = module;
133
+
73
134
  return (
74
135
  <SyntaxHighlighter
75
136
  style={oneDark}
@@ -81,3 +142,19 @@ export function CodeBlock({ className, children }: CodeBlockProps) {
81
142
  </SyntaxHighlighter>
82
143
  );
83
144
  }
145
+
146
+ export function CodeBlock({ className, children }: CodeBlockProps) {
147
+ const langMatch = className?.match(/language-(\w+)/);
148
+ const language = langMatch?.[1] ?? "";
149
+ const codeString = String(children).replace(/\n$/, "");
150
+
151
+ if (language === "mermaid") {
152
+ return <MermaidDiagram code={codeString} />;
153
+ }
154
+
155
+ if (!langMatch && !String(children).includes("\n")) {
156
+ return <code className={className}>{children}</code>;
157
+ }
158
+
159
+ return <LazySyntaxCodeBlock codeString={codeString} language={language} />;
160
+ }
@@ -16,6 +16,7 @@ import {
16
16
  type Highlighter,
17
17
  } from "../../lib/highlight";
18
18
  import { cn, getTextContent } from "../../lib/utils";
19
+ import { useAppStore } from "../../store";
19
20
  import {
20
21
  AnchorConfidences,
21
22
  type Comment,
@@ -23,8 +24,8 @@ import {
23
24
  FontFamilies,
24
25
  type SelectionRange,
25
26
  } from "../../types";
26
- import { CodeBlock } from "./CodeBlock";
27
27
  import { IframeContainer } from "./IframeContainer";
28
+ import { createCodeComponent } from "./InlineCode";
28
29
 
29
30
  function createHeadingComponent(
30
31
  level: 1 | 2 | 3 | 4 | 5 | 6,
@@ -101,7 +102,8 @@ export function DocumentViewer({
101
102
  onHighlightHover,
102
103
  onHighlightClick,
103
104
  }: DocumentViewerProps) {
104
- const { isFullscreen, fontFamily } = useLayoutContext();
105
+ const { isFullscreen, fontFamily, editorScheme } = useLayoutContext();
106
+ const workingDirectory = useAppStore((s) => s.workingDirectory);
105
107
  const contentRef = useRef<HTMLDivElement>(null);
106
108
  const containerRef = useRef<HTMLDivElement>(null);
107
109
  const adapterRef = useRef<Highlighter | null>(null);
@@ -208,16 +210,15 @@ export function DocumentViewer({
208
210
  h4: createHeadingComponent(4, headings, headingIndexRef),
209
211
  h5: createHeadingComponent(5, headings, headingIndexRef),
210
212
  h6: createHeadingComponent(6, headings, headingIndexRef),
211
- code: CodeBlock,
213
+ code: createCodeComponent(editorScheme, workingDirectory),
212
214
  }),
213
- [headings],
215
+ [headings, editorScheme, workingDirectory],
214
216
  );
215
217
 
216
218
  if (type === "html") {
217
219
  return (
218
220
  <main className="flex-1 min-w-0 flex flex-col">
219
221
  <IframeContainer
220
- key={content}
221
222
  html={content}
222
223
  comments={comments}
223
224
  pendingSelection={pendingSelection}
@@ -244,7 +245,6 @@ export function DocumentViewer({
244
245
  )}
245
246
  >
246
247
  <Markdown
247
- key={content}
248
248
  components={markdownComponents}
249
249
  remarkPlugins={[remarkGfm]}
250
250
  rehypePlugins={[rehypeRaw]}
@@ -0,0 +1,60 @@
1
+ import type { ComponentPropsWithoutRef } from "react";
2
+ import {
3
+ buildEditorUri,
4
+ parseFilePath,
5
+ resolveAbsolutePath,
6
+ } from "../../lib/editor-links";
7
+ import type { EditorScheme } from "../../types";
8
+ import { EditorSchemes } from "../../types";
9
+ import { CodeBlock } from "./CodeBlock";
10
+
11
+ /**
12
+ * Creates a combined code component for react-markdown that:
13
+ * - Routes fenced code blocks to CodeBlock (syntax highlighting)
14
+ * - Wraps inline code containing file paths with editor links
15
+ * - Falls back to plain <code> for non-file-path inline code
16
+ */
17
+ export function createCodeComponent(
18
+ editorScheme: EditorScheme,
19
+ workingDirectory: string | null,
20
+ ) {
21
+ return function CodeComponent({
22
+ children,
23
+ className,
24
+ ...props
25
+ }: ComponentPropsWithoutRef<"code">) {
26
+ // Fenced code blocks have className (e.g., "language-ts") or contain newlines
27
+ if (className || String(children).includes("\n")) {
28
+ return <CodeBlock className={className}>{children}</CodeBlock>;
29
+ }
30
+
31
+ // Inline code — check for file path patterns
32
+ if (editorScheme === EditorSchemes.NONE || !workingDirectory) {
33
+ return <code {...props}>{children}</code>;
34
+ }
35
+
36
+ const text = typeof children === "string" ? children : "";
37
+ if (!text) {
38
+ return <code {...props}>{children}</code>;
39
+ }
40
+
41
+ const match = parseFilePath(text);
42
+ if (!match) {
43
+ return <code {...props}>{children}</code>;
44
+ }
45
+
46
+ const absolutePath = resolveAbsolutePath(match.path, workingDirectory);
47
+ const uri = buildEditorUri(
48
+ editorScheme,
49
+ absolutePath,
50
+ match.line,
51
+ match.col,
52
+ );
53
+
54
+ return (
55
+ <a href={uri} title={`Open in ${editorScheme}`} className="editor-link">
56
+ <code {...props}>{children}</code>
57
+ </a>
58
+ );
59
+ };
60
+ }
@@ -1,5 +1,6 @@
1
1
  import { List } from "lucide-react";
2
2
  import { useState } from "react";
3
+ import { useLocale } from "../contexts/LocaleContext";
3
4
  import type { Heading } from "../hooks/useHeadings";
4
5
  import { cn } from "../lib/utils";
5
6
  import { TableOfContents } from "./TableOfContents";
@@ -15,6 +16,7 @@ export function FloatingTOC({
15
16
  activeId,
16
17
  onHeadingClick,
17
18
  }: FloatingTOCProps) {
19
+ const { t } = useLocale();
18
20
  const [isExpanded, setIsExpanded] = useState(false);
19
21
 
20
22
  if (headings.length === 0) return null;
@@ -24,7 +26,7 @@ export function FloatingTOC({
24
26
  className="fixed left-4 top-16 z-40"
25
27
  onMouseEnter={() => setIsExpanded(true)}
26
28
  onMouseLeave={() => setIsExpanded(false)}
27
- aria-label="Table of contents"
29
+ aria-label={t("floatingTOC.label")}
28
30
  >
29
31
  {/* Collapsed state: circular button */}
30
32
  <button
@@ -34,7 +36,7 @@ export function FloatingTOC({
34
36
  "w-10 h-10 rounded-full bg-white dark:bg-zinc-900 shadow-lg border border-zinc-100 dark:border-zinc-800 flex items-center justify-center text-zinc-400 dark:text-zinc-500 hover:text-zinc-600 dark:hover:text-zinc-300 transition-colors duration-150",
35
37
  isExpanded && "opacity-0 pointer-events-none",
36
38
  )}
37
- aria-label="Table of Contents"
39
+ aria-label={t("floatingTOC.label")}
38
40
  >
39
41
  <List className="w-5 h-5" />
40
42
  </button>
@@ -1,5 +1,6 @@
1
1
  import { useCommentContext } from "../contexts/CommentContext";
2
2
  import { useLayoutContext } from "../contexts/LayoutContext";
3
+ import { useLocale } from "../contexts/LocaleContext";
3
4
  import { cn } from "../lib/utils";
4
5
  import { ActionsMenu } from "./ActionsMenu";
5
6
  import { CommentBadge } from "./comments/CommentBadge";
@@ -22,6 +23,7 @@ export function Header({
22
23
  }: HeaderProps) {
23
24
  const { reanchorTarget } = useCommentContext();
24
25
  const { isFullscreen } = useLayoutContext();
26
+ const { t } = useLocale();
25
27
 
26
28
  return (
27
29
  <header className="sticky top-0 z-50 bg-white/95 dark:bg-zinc-900/95 backdrop-blur-sm border-b border-zinc-100 dark:border-zinc-800">
@@ -44,7 +46,7 @@ export function Header({
44
46
  <div className="flex items-center gap-3">
45
47
  {reanchorTarget && (
46
48
  <Text variant="caption" asChild>
47
- <span className="italic">Select text to re-anchor</span>
49
+ <span className="italic">{t("header.selectTextToReanchor")}</span>
48
50
  </Text>
49
51
  )}
50
52
 
@@ -1,5 +1,6 @@
1
1
  import { use, useEffect, useRef, useState } from "react";
2
2
  import { LayoutContext } from "../contexts/LayoutContext";
3
+ import { useLocale } from "../contexts/LocaleContext";
3
4
  import { cn } from "../lib/utils";
4
5
  import { FontFamilies } from "../types";
5
6
  import { Button } from "./ui/Button";
@@ -20,6 +21,7 @@ export function InlineEditor({
20
21
  className,
21
22
  }: InlineEditorProps) {
22
23
  const layout = use(LayoutContext);
24
+ const { t } = useLocale();
23
25
  const fontClass = layout
24
26
  ? layout.fontFamily === FontFamilies.SANS_SERIF
25
27
  ? "font-sans"
@@ -61,10 +63,10 @@ export function InlineEditor({
61
63
  />
62
64
  <div className="flex gap-3 text-sm">
63
65
  <Button variant="link" size="sm" onClick={handleSave}>
64
- Save
66
+ {t("editor.save")}
65
67
  </Button>
66
68
  <Button variant="ghost" size="sm" onClick={onCancel}>
67
- Cancel
69
+ {t("editor.cancel")}
68
70
  </Button>
69
71
  </div>
70
72
  </div>
@@ -2,6 +2,7 @@ import { cva } from "class-variance-authority";
2
2
  import { useState } from "react";
3
3
  import { useCommentContext } from "../contexts/CommentContext";
4
4
  import { useLayoutContext } from "../contexts/LayoutContext";
5
+ import { useLocale } from "../contexts/LocaleContext";
5
6
  import { cn } from "../lib/utils";
6
7
  import { type Comment, FontFamilies } from "../types";
7
8
  import { InlineEditor } from "./InlineEditor";
@@ -60,6 +61,7 @@ export function MarginNote({
60
61
  commentIndex = 0,
61
62
  }: MarginNoteProps) {
62
63
  const { fontFamily } = useLayoutContext();
64
+ const { t } = useLocale();
63
65
  const {
64
66
  editComment,
65
67
  deleteComment,
@@ -100,13 +102,15 @@ export function MarginNote({
100
102
  <ActionBar
101
103
  className={cn("gap-1.5 duration-150", isHovered && "opacity-100")}
102
104
  >
103
- <ActionLink onClick={() => setIsEditing(true)}>Add note</ActionLink>
105
+ <ActionLink onClick={() => setIsEditing(true)}>
106
+ {t("marginNote.addNote")}
107
+ </ActionLink>
104
108
  <SeparatorDot />
105
109
  <ActionLink
106
110
  variant="destructive"
107
111
  onClick={() => deleteComment(comment.id)}
108
112
  >
109
- Delete
113
+ {t("marginNote.delete")}
110
114
  </ActionLink>
111
115
  </ActionBar>
112
116
  </div>
@@ -170,24 +174,29 @@ export function MarginNote({
170
174
  {comment.comment}
171
175
  </p>
172
176
  <ActionBar className="gap-1.5 mt-2">
173
- <ActionLink onClick={() => setIsEditing(true)}>Edit</ActionLink>
177
+ <ActionLink onClick={() => setIsEditing(true)}>
178
+ {t("marginNote.edit")}
179
+ </ActionLink>
174
180
  <SeparatorDot />
175
181
  <ActionLink
176
182
  variant="destructive"
177
183
  onClick={() => deleteComment(comment.id)}
178
184
  >
179
- Delete
185
+ {t("marginNote.delete")}
180
186
  </ActionLink>
181
187
  <SeparatorDot />
182
- <ActionLink onClick={handleCopy} title="Copy raw text (⌘C)">
183
- Copy
188
+ <ActionLink
189
+ onClick={handleCopy}
190
+ title={t("marginNote.copyTitle")}
191
+ >
192
+ {t("marginNote.copy")}
184
193
  </ActionLink>
185
194
  <SeparatorDot />
186
195
  <ActionLink
187
196
  onClick={() => copyCommentForLLM(comment)}
188
- title="Copy with context for LLM (⌘⇧C)"
197
+ title={t("marginNote.llmTitle")}
189
198
  >
190
- LLM
199
+ {t("marginNote.llm")}
191
200
  </ActionLink>
192
201
  </ActionBar>
193
202
  </>
@@ -1,6 +1,7 @@
1
1
  import { Copy } from "lucide-react";
2
2
  import { useCallback, useEffect, useState } from "react";
3
3
  import { toast } from "sonner";
4
+ import { useLocale } from "../contexts/LocaleContext";
4
5
  import { useAppStore } from "../store";
5
6
  import { Button } from "./ui/Button";
6
7
  import {
@@ -26,6 +27,7 @@ type ModalState =
26
27
  | { status: "success"; content: string; path: string };
27
28
 
28
29
  export function RawModal({ isOpen, onClose }: RawModalProps) {
30
+ const { t } = useLocale();
29
31
  const [state, setState] = useState<ModalState>({ status: "idle" });
30
32
  const activeDocumentPath = useAppStore((s) => s.activeDocumentPath);
31
33
 
@@ -73,11 +75,11 @@ export function RawModal({ isOpen, onClose }: RawModalProps) {
73
75
 
74
76
  try {
75
77
  await navigator.clipboard.writeText(state.content);
76
- toast.success("Copied to clipboard");
78
+ toast.success(t("rawModal.copiedToClipboard"));
77
79
  } catch {
78
- toast.error("Failed to copy");
80
+ toast.error(t("rawModal.failedToCopy"));
79
81
  }
80
- }, [state]);
82
+ }, [state, t]);
81
83
 
82
84
  return (
83
85
  <Dialog
@@ -88,14 +90,14 @@ export function RawModal({ isOpen, onClose }: RawModalProps) {
88
90
  >
89
91
  <DialogContent className="max-w-2xl max-h-[80vh]">
90
92
  <DialogHeader>
91
- <DialogTitle>Raw Comments</DialogTitle>
93
+ <DialogTitle>{t("rawModal.title")}</DialogTitle>
92
94
  {state.status === "success" && (
93
95
  <Button
94
96
  variant="ghost"
95
97
  size="icon"
96
98
  className="size-7"
97
99
  onClick={handleCopy}
98
- title="Copy to clipboard"
100
+ title={t("rawModal.copyTitle")}
99
101
  >
100
102
  <Copy className="w-4 h-4" />
101
103
  </Button>
@@ -111,7 +113,7 @@ export function RawModal({ isOpen, onClose }: RawModalProps) {
111
113
  <DialogBody>
112
114
  {state.status === "loading" && (
113
115
  <Text variant="caption" className="text-center py-8">
114
- Loading...
116
+ {t("rawModal.loading")}
115
117
  </Text>
116
118
  )}
117
119
 
@@ -123,7 +125,7 @@ export function RawModal({ isOpen, onClose }: RawModalProps) {
123
125
 
124
126
  {state.status === "empty" && (
125
127
  <Text variant="caption" className="text-center py-8">
126
- No comments file yet. Add comments to create one.
128
+ {t("rawModal.noComments")}
127
129
  </Text>
128
130
  )}
129
131
 
@@ -1,3 +1,4 @@
1
+ import { useLocale } from "../contexts/LocaleContext";
1
2
  import { Button } from "./ui/Button";
2
3
  import { Text } from "./ui/Text";
3
4
 
@@ -12,20 +13,22 @@ export function ReanchorConfirm({
12
13
  onConfirm,
13
14
  onCancel,
14
15
  }: ReanchorConfirmProps) {
16
+ const { t } = useLocale();
17
+
15
18
  return (
16
19
  <div className="border-t border-zinc-200 dark:border-zinc-700 pt-2 pb-3 pl-6">
17
20
  <Text variant="body" className="mb-2">
18
- Re-anchor to this selection?
21
+ {t("reanchor.question")}
19
22
  </Text>
20
23
  <Text variant="caption" asChild>
21
24
  <p className="italic line-clamp-2 mb-2">"{selectionText}"</p>
22
25
  </Text>
23
26
  <div className="flex gap-3 text-sm">
24
27
  <Button variant="link" size="sm" onClick={onConfirm}>
25
- Confirm
28
+ {t("reanchor.confirm")}
26
29
  </Button>
27
30
  <Button variant="ghost" size="sm" onClick={onCancel}>
28
- Cancel
31
+ {t("reanchor.cancel")}
29
32
  </Button>
30
33
  </div>
31
34
  </div>