@peaske7/readit 0.1.6 → 0.1.8

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 (49) hide show
  1. package/biome.json +1 -1
  2. package/bun.lock +86 -72
  3. package/package.json +12 -11
  4. package/src/App.tsx +36 -16
  5. package/src/cli/index.ts +338 -70
  6. package/src/components/ActionsMenu.tsx +12 -10
  7. package/src/components/DocumentViewer/CodeBlock.tsx +131 -54
  8. package/src/components/DocumentViewer/DocumentViewer.tsx +10 -8
  9. package/src/components/DocumentViewer/InlineCode.tsx +60 -0
  10. package/src/components/FloatingTOC.tsx +4 -2
  11. package/src/components/Header.tsx +3 -1
  12. package/src/components/InlineEditor.tsx +4 -2
  13. package/src/components/MarginNote.tsx +17 -8
  14. package/src/components/RawModal.tsx +9 -7
  15. package/src/components/ReanchorConfirm.tsx +6 -3
  16. package/src/components/SettingsModal.tsx +112 -23
  17. package/src/components/ShortcutCapture.tsx +4 -1
  18. package/src/components/ShortcutList.tsx +50 -9
  19. package/src/components/comments/CommentBadge.tsx +7 -1
  20. package/src/components/comments/CommentInput.tsx +13 -18
  21. package/src/components/comments/CommentListItem.tsx +15 -5
  22. package/src/components/comments/CommentManager.tsx +14 -7
  23. package/src/components/comments/CommentNav.tsx +8 -3
  24. package/src/contexts/CommentContext.tsx +16 -9
  25. package/src/contexts/LayoutContext.tsx +17 -5
  26. package/src/contexts/LocaleContext.tsx +35 -0
  27. package/src/hooks/useClipboard.ts +11 -8
  28. package/src/hooks/useDocument.ts +33 -18
  29. package/src/hooks/useEditorScheme.ts +51 -0
  30. package/src/hooks/useFontPreference.ts +5 -22
  31. package/src/hooks/useKeybindings.ts +6 -18
  32. package/src/hooks/useLocalePreference.ts +42 -0
  33. package/src/index.css +87 -26
  34. package/src/lib/editor-links.ts +59 -0
  35. package/src/lib/highlight/dom.ts +126 -54
  36. package/src/lib/highlight/highlighter.ts +10 -10
  37. package/src/lib/i18n/completeness.test.ts +51 -0
  38. package/src/lib/i18n/en.ts +139 -0
  39. package/src/lib/i18n/index.ts +3 -0
  40. package/src/lib/i18n/ja.ts +141 -0
  41. package/src/lib/i18n/translations.test.ts +39 -0
  42. package/src/lib/i18n/translations.ts +27 -0
  43. package/src/lib/i18n/types.ts +145 -0
  44. package/src/lib/shortcut-registry.ts +1 -1
  45. package/src/main.tsx +4 -1
  46. package/src/server/index.ts +197 -124
  47. package/src/store/index.test.ts +22 -0
  48. package/src/store/index.ts +24 -4
  49. package/src/types/index.ts +12 -0
@@ -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
  )}
@@ -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);
@@ -155,7 +157,7 @@ export function DocumentViewer({
155
157
 
156
158
  // Double RAF: ensures React commit phase completes before DOM queries.
157
159
  // See: https://github.com/facebook/react/issues/20863
158
- // biome-ignore lint/correctness/useExhaustiveDependencies: must reapply highlights when content changes
160
+ // biome-ignore lint/correctness/useExhaustiveDependencies: must reapply highlights when content or components change
159
161
  useEffect(() => {
160
162
  if (type !== "markdown") return;
161
163
 
@@ -184,7 +186,9 @@ export function DocumentViewer({
184
186
  cancelAnimationFrame(outerFrameId);
185
187
  cancelAnimationFrame(innerFrameId);
186
188
  };
187
- }, [comments, content, type]);
189
+ // editorScheme/workingDirectory: when these change, markdownComponents memo recomputes,
190
+ // react-markdown replaces the DOM, so highlights must be reapplied
191
+ }, [comments, content, type, editorScheme, workingDirectory]);
188
192
 
189
193
  useEffect(() => {
190
194
  if (type !== "markdown") return;
@@ -208,16 +212,15 @@ export function DocumentViewer({
208
212
  h4: createHeadingComponent(4, headings, headingIndexRef),
209
213
  h5: createHeadingComponent(5, headings, headingIndexRef),
210
214
  h6: createHeadingComponent(6, headings, headingIndexRef),
211
- code: CodeBlock,
215
+ code: createCodeComponent(editorScheme, workingDirectory),
212
216
  }),
213
- [headings],
217
+ [headings, editorScheme, workingDirectory],
214
218
  );
215
219
 
216
220
  if (type === "html") {
217
221
  return (
218
222
  <main className="flex-1 min-w-0 flex flex-col">
219
223
  <IframeContainer
220
- key={content}
221
224
  html={content}
222
225
  comments={comments}
223
226
  pendingSelection={pendingSelection}
@@ -244,7 +247,6 @@ export function DocumentViewer({
244
247
  )}
245
248
  >
246
249
  <Markdown
247
- key={content}
248
250
  components={markdownComponents}
249
251
  remarkPlugins={[remarkGfm]}
250
252
  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>