@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
@@ -1,7 +1,11 @@
1
- import { Check, ChevronDown } from "lucide-react";
1
+ import { Check, ChevronDown, ExternalLink } from "lucide-react";
2
2
  import { useLayoutContext } from "../contexts/LayoutContext";
3
+ import { useLocale } from "../contexts/LocaleContext";
4
+ import { type Locale, Locales } from "../lib/i18n";
3
5
  import { cn } from "../lib/utils";
4
6
  import {
7
+ type EditorScheme,
8
+ EditorSchemes,
5
9
  FontFamilies,
6
10
  type FontFamily,
7
11
  type ThemeMode,
@@ -28,10 +32,9 @@ interface SettingsModalProps {
28
32
  onClose: () => void;
29
33
  }
30
34
 
31
- const THEME_OPTIONS = [
32
- { value: ThemeModes.SYSTEM, label: "System" },
33
- { value: ThemeModes.LIGHT, label: "Light" },
34
- { value: ThemeModes.DARK, label: "Dark" },
35
+ const LOCALE_OPTIONS = [
36
+ { value: Locales.JA, label: "日本語" },
37
+ { value: Locales.EN, label: "English" },
35
38
  ] as const;
36
39
 
37
40
  function ThemeDot({
@@ -73,15 +76,6 @@ function ThemePreviewBadge() {
73
76
 
74
77
  /* ─── Font selector ──────────────────────────────────────────── */
75
78
 
76
- const FONT_OPTIONS = [
77
- { value: FontFamilies.SERIF, label: "Serif", fontClass: "font-serif" },
78
- {
79
- value: FontFamilies.SANS_SERIF,
80
- label: "Sans-serif",
81
- fontClass: "font-sans",
82
- },
83
- ] as const;
84
-
85
79
  function FontPreviewBadge({ fontClass }: { fontClass: string }) {
86
80
  return (
87
81
  <span
@@ -112,6 +106,8 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
112
106
  const {
113
107
  fontFamily,
114
108
  setFontFamily,
109
+ editorScheme,
110
+ setEditorScheme,
115
111
  themeMode,
116
112
  setThemeMode,
117
113
  shortcuts,
@@ -119,11 +115,45 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
119
115
  toggleShortcutEnabled,
120
116
  resetShortcutsToDefaults,
121
117
  } = useLayoutContext();
118
+ const { locale, setLocale, t } = useLocale();
119
+
120
+ const themeOptions = [
121
+ { value: ThemeModes.SYSTEM, label: t("settings.theme.system") },
122
+ { value: ThemeModes.LIGHT, label: t("settings.theme.light") },
123
+ { value: ThemeModes.DARK, label: t("settings.theme.dark") },
124
+ ];
125
+
126
+ const fontOptions = [
127
+ {
128
+ value: FontFamilies.SERIF,
129
+ label: t("settings.font.serif"),
130
+ fontClass: "font-serif",
131
+ },
132
+ {
133
+ value: FontFamilies.SANS_SERIF,
134
+ label: t("settings.font.sansSerif"),
135
+ fontClass: "font-sans",
136
+ },
137
+ ];
138
+
139
+ const editorOptions = [
140
+ { value: EditorSchemes.NONE, label: t("settings.editor.none") },
141
+ { value: EditorSchemes.VSCODE, label: t("settings.editor.vscode") },
142
+ {
143
+ value: EditorSchemes.VSCODE_INSIDERS,
144
+ label: t("settings.editor.vscodeInsiders"),
145
+ },
146
+ { value: EditorSchemes.CURSOR, label: t("settings.editor.cursor") },
147
+ ];
122
148
 
123
149
  const activeTheme =
124
- THEME_OPTIONS.find((o) => o.value === themeMode) ?? THEME_OPTIONS[0];
150
+ themeOptions.find((o) => o.value === themeMode) ?? themeOptions[0];
125
151
  const activeFont =
126
- FONT_OPTIONS.find((o) => o.value === fontFamily) ?? FONT_OPTIONS[0];
152
+ fontOptions.find((o) => o.value === fontFamily) ?? fontOptions[0];
153
+ const activeEditor =
154
+ editorOptions.find((o) => o.value === editorScheme) ?? editorOptions[0];
155
+ const activeLocale =
156
+ LOCALE_OPTIONS.find((o) => o.value === locale) ?? LOCALE_OPTIONS[0];
127
157
 
128
158
  return (
129
159
  <Dialog
@@ -134,13 +164,13 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
134
164
  >
135
165
  <DialogContent className="max-w-md">
136
166
  <DialogHeader>
137
- <DialogTitle>Settings</DialogTitle>
167
+ <DialogTitle>{t("settings.title")}</DialogTitle>
138
168
  </DialogHeader>
139
169
 
140
170
  <DialogBody className="space-y-4">
141
171
  <div>
142
172
  <Text variant="overline" asChild>
143
- <h3 className="mb-3">Theme</h3>
173
+ <h3 className="mb-3">{t("settings.theme")}</h3>
144
174
  </Text>
145
175
  <DropdownMenu>
146
176
  <DropdownMenuTrigger asChild>
@@ -152,7 +182,7 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
152
182
  </button>
153
183
  </DropdownMenuTrigger>
154
184
  <DropdownMenuContent align="start" className="min-w-[160px]">
155
- {THEME_OPTIONS.map((option) => (
185
+ {themeOptions.map((option) => (
156
186
  <DropdownMenuItem
157
187
  key={option.value}
158
188
  onSelect={() => setThemeMode(option.value)}
@@ -172,7 +202,7 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
172
202
 
173
203
  <div>
174
204
  <Text variant="overline" asChild>
175
- <h3 className="mb-3">Font</h3>
205
+ <h3 className="mb-3">{t("settings.font")}</h3>
176
206
  </Text>
177
207
  <DropdownMenu>
178
208
  <DropdownMenuTrigger asChild>
@@ -183,7 +213,7 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
183
213
  </button>
184
214
  </DropdownMenuTrigger>
185
215
  <DropdownMenuContent align="start" className="min-w-[160px]">
186
- {FONT_OPTIONS.map((option) => (
216
+ {fontOptions.map((option) => (
187
217
  <DropdownMenuItem
188
218
  key={option.value}
189
219
  onSelect={() => setFontFamily(option.value as FontFamily)}
@@ -202,10 +232,69 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
202
232
 
203
233
  <div>
204
234
  <Text variant="overline" asChild>
205
- <h3 className="mb-1">Keyboard Shortcuts</h3>
235
+ <h3 className="mb-3">{t("settings.language")}</h3>
236
+ </Text>
237
+ <DropdownMenu>
238
+ <DropdownMenuTrigger asChild>
239
+ <button type="button" className={triggerClassName}>
240
+ <span>{activeLocale.label}</span>
241
+ <ChevronDown className="size-3 text-zinc-400 dark:text-zinc-500" />
242
+ </button>
243
+ </DropdownMenuTrigger>
244
+ <DropdownMenuContent align="start" className="min-w-[160px]">
245
+ {LOCALE_OPTIONS.map((option) => (
246
+ <DropdownMenuItem
247
+ key={option.value}
248
+ onSelect={() => setLocale(option.value as Locale)}
249
+ className="flex items-center gap-2"
250
+ >
251
+ <span className="flex-1">{option.label}</span>
252
+ {locale === option.value && (
253
+ <Check className="size-3.5 text-zinc-500 dark:text-zinc-400" />
254
+ )}
255
+ </DropdownMenuItem>
256
+ ))}
257
+ </DropdownMenuContent>
258
+ </DropdownMenu>
259
+ </div>
260
+
261
+ <div>
262
+ <Text variant="overline" asChild>
263
+ <h3 className="mb-3">{t("settings.editor")}</h3>
264
+ </Text>
265
+ <DropdownMenu>
266
+ <DropdownMenuTrigger asChild>
267
+ <button type="button" className={triggerClassName}>
268
+ <ExternalLink className="size-3 text-zinc-400 dark:text-zinc-500" />
269
+ <span>{activeEditor.label}</span>
270
+ <ChevronDown className="size-3 text-zinc-400 dark:text-zinc-500" />
271
+ </button>
272
+ </DropdownMenuTrigger>
273
+ <DropdownMenuContent align="start" className="min-w-[160px]">
274
+ {editorOptions.map((option) => (
275
+ <DropdownMenuItem
276
+ key={option.value}
277
+ onSelect={() =>
278
+ setEditorScheme(option.value as EditorScheme)
279
+ }
280
+ className="flex items-center gap-2"
281
+ >
282
+ <span className="flex-1">{option.label}</span>
283
+ {editorScheme === option.value && (
284
+ <Check className="size-3.5 text-zinc-500 dark:text-zinc-400" />
285
+ )}
286
+ </DropdownMenuItem>
287
+ ))}
288
+ </DropdownMenuContent>
289
+ </DropdownMenu>
290
+ </div>
291
+
292
+ <div>
293
+ <Text variant="overline" asChild>
294
+ <h3 className="mb-1">{t("settings.keyboardShortcuts")}</h3>
206
295
  </Text>
207
296
  <p className="text-xs text-zinc-400 dark:text-zinc-500 mb-3">
208
- Click a key to rebind
297
+ {t("settings.clickToRebind")}
209
298
  </p>
210
299
  <ShortcutList
211
300
  shortcuts={shortcuts}
@@ -1,4 +1,5 @@
1
1
  import { useCallback, useEffect } from "react";
2
+ import { useLocale } from "../contexts/LocaleContext";
2
3
  import {
3
4
  eventToBinding,
4
5
  isReservedBinding,
@@ -11,6 +12,8 @@ interface ShortcutCaptureProps {
11
12
  }
12
13
 
13
14
  export function ShortcutCapture({ onCapture, onCancel }: ShortcutCaptureProps) {
15
+ const { t } = useLocale();
16
+
14
17
  const handleKeyDown = useCallback(
15
18
  (e: KeyboardEvent) => {
16
19
  e.preventDefault();
@@ -39,7 +42,7 @@ export function ShortcutCapture({ onCapture, onCancel }: ShortcutCaptureProps) {
39
42
 
40
43
  return (
41
44
  <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded bg-amber-50 border border-amber-200 text-amber-700 text-xs font-medium animate-pulse">
42
- Press keys...
45
+ {t("shortcutCapture.pressKeys")}
43
46
  </span>
44
47
  );
45
48
  }
@@ -1,7 +1,10 @@
1
1
  import { useCallback, useMemo, useState } from "react";
2
+ import { useLocale } from "../contexts/LocaleContext";
3
+ import type { TranslationKey } from "../lib/i18n";
2
4
  import {
3
5
  bindingsEqual,
4
6
  formatBinding,
7
+ type ShortcutAction,
5
8
  type ShortcutBinding,
6
9
  type ShortcutDefinition,
7
10
  } from "../lib/shortcut-registry";
@@ -16,13 +19,50 @@ interface ShortcutListProps {
16
19
 
17
20
  const SHORTCUT_GROUPS = [
18
21
  {
19
- label: "Copy",
22
+ labelKey: "shortcutGroup.copy" as const,
20
23
  ids: ["copyAll", "copyAllRaw", "copySelectionRaw", "copySelectionLLM"],
21
24
  },
22
- { label: "Navigate", ids: ["navigateNext", "navigatePrevious"] },
23
- { label: "Other", ids: ["clearSelection"] },
25
+ {
26
+ labelKey: "shortcutGroup.navigate" as const,
27
+ ids: ["navigateNext", "navigatePrevious"],
28
+ },
29
+ { labelKey: "shortcutGroup.other" as const, ids: ["clearSelection"] },
24
30
  ] as const;
25
31
 
32
+ const SHORTCUT_LABEL_KEYS: Record<
33
+ ShortcutAction,
34
+ { label: TranslationKey; description: TranslationKey }
35
+ > = {
36
+ copyAll: {
37
+ label: "shortcut.copyAll.label",
38
+ description: "shortcut.copyAll.description",
39
+ },
40
+ copyAllRaw: {
41
+ label: "shortcut.copyAllRaw.label",
42
+ description: "shortcut.copyAllRaw.description",
43
+ },
44
+ navigateNext: {
45
+ label: "shortcut.navigateNext.label",
46
+ description: "shortcut.navigateNext.description",
47
+ },
48
+ navigatePrevious: {
49
+ label: "shortcut.navigatePrevious.label",
50
+ description: "shortcut.navigatePrevious.description",
51
+ },
52
+ copySelectionRaw: {
53
+ label: "shortcut.copySelectionRaw.label",
54
+ description: "shortcut.copySelectionRaw.description",
55
+ },
56
+ copySelectionLLM: {
57
+ label: "shortcut.copySelectionLLM.label",
58
+ description: "shortcut.copySelectionLLM.description",
59
+ },
60
+ clearSelection: {
61
+ label: "shortcut.clearSelection.label",
62
+ description: "shortcut.clearSelection.description",
63
+ },
64
+ };
65
+
26
66
  const isMac =
27
67
  typeof navigator !== "undefined" &&
28
68
  /Mac|iPod|iPhone|iPad/.test(navigator.platform);
@@ -33,6 +73,7 @@ export function ShortcutList({
33
73
  onToggleEnabled,
34
74
  onResetToDefaults,
35
75
  }: ShortcutListProps) {
76
+ const { t } = useLocale();
36
77
  const [capturingId, setCapturingId] = useState<string | undefined>();
37
78
 
38
79
  const hasOverrides = useMemo(
@@ -77,9 +118,9 @@ export function ShortcutList({
77
118
  if (groupShortcuts.length === 0) return null;
78
119
 
79
120
  return (
80
- <div key={group.label}>
121
+ <div key={group.labelKey}>
81
122
  <span className="text-[11px] font-medium text-zinc-400 dark:text-zinc-500 uppercase tracking-wider">
82
- {group.label}
123
+ {t(group.labelKey)}
83
124
  </span>
84
125
  <div className="mt-1 space-y-0.5">
85
126
  {groupShortcuts.map((shortcut) => (
@@ -89,9 +130,9 @@ export function ShortcutList({
89
130
  >
90
131
  <span
91
132
  className="flex-1 text-sm text-zinc-700 dark:text-zinc-300 truncate"
92
- title={shortcut.description}
133
+ title={t(SHORTCUT_LABEL_KEYS[shortcut.id].description)}
93
134
  >
94
- {shortcut.label}
135
+ {t(SHORTCUT_LABEL_KEYS[shortcut.id].label)}
95
136
  </span>
96
137
 
97
138
  <div className="flex items-center gap-2.5">
@@ -117,7 +158,7 @@ export function ShortcutList({
117
158
  role="switch"
118
159
  aria-checked={shortcut.enabled}
119
160
  onClick={() => onToggleEnabled(shortcut.id)}
120
- title="Enable/disable shortcut"
161
+ title={t("shortcuts.enableDisable")}
121
162
  className={`relative inline-flex h-4 w-7 shrink-0 items-center rounded-full transition-colors cursor-pointer ${
122
163
  shortcut.enabled
123
164
  ? "bg-zinc-600 dark:bg-zinc-400"
@@ -150,7 +191,7 @@ export function ShortcutList({
150
191
  : "text-xs text-zinc-300 dark:text-zinc-600 cursor-default"
151
192
  }
152
193
  >
153
- Reset to defaults
194
+ {t("shortcuts.resetToDefaults")}
154
195
  </button>
155
196
  </div>
156
197
  );
@@ -1,5 +1,6 @@
1
1
  import { useState } from "react";
2
2
  import { useCommentContext } from "../../contexts/CommentContext";
3
+ import { useLocale } from "../../contexts/LocaleContext";
3
4
  import { cn } from "../../lib/utils";
4
5
  import {
5
6
  DropdownMenu,
@@ -9,6 +10,7 @@ import {
9
10
  import { CommentManager } from "./CommentManager";
10
11
 
11
12
  export function CommentBadge() {
13
+ const { t } = useLocale();
12
14
  const { commentCount } = useCommentContext();
13
15
 
14
16
  const [commentsOpen, setCommentsOpen] = useState(false);
@@ -26,7 +28,11 @@ export function CommentBadge() {
26
28
  ? "text-zinc-600"
27
29
  : "text-zinc-400 hover:text-zinc-600",
28
30
  )}
29
- title={`${commentCount} comment${commentCount !== 1 ? "s" : ""}`}
31
+ title={
32
+ commentCount === 1
33
+ ? t("commentBadge.title", { count: commentCount })
34
+ : t("commentBadge.titlePlural", { count: commentCount })
35
+ }
30
36
  >
31
37
  <span className="text-zinc-300">·</span>
32
38
  {commentCount}
@@ -1,6 +1,7 @@
1
1
  import { BotMessageSquare, Copy } from "lucide-react";
2
2
  import { use, useEffect, useRef, useState } from "react";
3
3
  import { LayoutContext } from "../../contexts/LayoutContext";
4
+ import { useLocale } from "../../contexts/LocaleContext";
4
5
  import { cn } from "../../lib/utils";
5
6
  import { FontFamilies } from "../../types";
6
7
  import { Button } from "../ui/Button";
@@ -21,6 +22,7 @@ export function CommentInput({
21
22
  onCopyRaw,
22
23
  onCopyForLLM,
23
24
  }: CommentInputProps) {
25
+ const { t } = useLocale();
24
26
  const layout = use(LayoutContext);
25
27
  const fontClass = layout
26
28
  ? layout.fontFamily === FontFamilies.SANS_SERIF
@@ -32,18 +34,11 @@ export function CommentInput({
32
34
  const textareaRef = useRef<HTMLTextAreaElement>(null);
33
35
 
34
36
  useEffect(() => {
35
- if (selectedText && textareaRef.current) {
36
- // Only auto-focus on devices with precise pointing (desktop)
37
- if (window.matchMedia("(pointer: fine)").matches) {
38
- textareaRef.current.focus();
39
- }
37
+ // Only auto-focus on devices with precise pointing (desktop)
38
+ if (textareaRef.current && window.matchMedia("(pointer: fine)").matches) {
39
+ textareaRef.current.focus();
40
40
  }
41
- }, [selectedText]);
42
-
43
- // biome-ignore lint/correctness/useExhaustiveDependencies: intentionally reset when selection changes
44
- useEffect(() => {
45
- setCommentText("");
46
- }, [selectedText]);
41
+ }, []);
47
42
 
48
43
  const handleSubmit = () => {
49
44
  onSubmit(commentText.trim());
@@ -76,7 +71,7 @@ export function CommentInput({
76
71
  ref={textareaRef}
77
72
  value={commentText}
78
73
  onChange={(e) => setCommentText(e.target.value)}
79
- placeholder="Add your comment..."
74
+ placeholder={t("comment.placeholder")}
80
75
  className={cn(
81
76
  fontClass,
82
77
  "w-full px-2 py-1.5 text-sm border border-zinc-200 dark:border-zinc-700 dark:bg-zinc-800 resize-none focus:outline-none focus:border-zinc-400 dark:focus:border-zinc-500",
@@ -91,8 +86,8 @@ export function CommentInput({
91
86
  size="icon"
92
87
  className="size-7 text-zinc-300 dark:text-zinc-600 hover:text-zinc-500 dark:hover:text-zinc-400"
93
88
  onClick={onCopyRaw}
94
- title="Copy raw text (⌘C)"
95
- aria-label="Copy raw text"
89
+ title={t("comment.copyRawTitle")}
90
+ aria-label={t("comment.copyRawLabel")}
96
91
  >
97
92
  <Copy size={14} />
98
93
  </Button>
@@ -101,17 +96,17 @@ export function CommentInput({
101
96
  size="icon"
102
97
  className="size-7 text-zinc-300 dark:text-zinc-600 hover:text-zinc-500 dark:hover:text-zinc-400"
103
98
  onClick={onCopyForLLM}
104
- title="Copy with context for LLM (⌘⇧C)"
105
- aria-label="Copy for LLM"
99
+ title={t("comment.copyLLMTitle")}
100
+ aria-label={t("comment.copyLLMLabel")}
106
101
  >
107
102
  <BotMessageSquare size={14} />
108
103
  </Button>
109
104
  </div>
110
105
  <Button variant="ghost" size="sm" onClick={onCancel}>
111
- Cancel
106
+ {t("comment.cancel")}
112
107
  </Button>
113
108
  <Button variant="link" size="sm" onClick={handleSubmit} title="⌘↵">
114
- {commentText.trim() ? "Add Note" : "Highlight"}
109
+ {commentText.trim() ? t("comment.addNote") : t("comment.highlight")}
115
110
  </Button>
116
111
  </div>
117
112
  </div>
@@ -1,5 +1,6 @@
1
1
  import { useState } from "react";
2
2
  import { useCommentContext } from "../../contexts/CommentContext";
3
+ import { useLocale } from "../../contexts/LocaleContext";
3
4
  import { cn } from "../../lib/utils";
4
5
  import type { Comment } from "../../types";
5
6
  import { InlineEditor } from "../InlineEditor";
@@ -14,6 +15,7 @@ interface CommentListItemProps {
14
15
  }
15
16
 
16
17
  export function CommentListItem({ comment, onAction }: CommentListItemProps) {
18
+ const { t } = useLocale();
17
19
  const { editComment, deleteComment, navigateToComment, startReanchor } =
18
20
  useCommentContext();
19
21
 
@@ -45,7 +47,7 @@ export function CommentListItem({ comment, onAction }: CommentListItemProps) {
45
47
  </Text>
46
48
  {isUnresolved && (
47
49
  <Text variant="caption" asChild>
48
- <span className="shrink-0">· unresolved</span>
50
+ <span className="shrink-0">· {t("commentList.unresolved")}</span>
49
51
  </Text>
50
52
  )}
51
53
  </div>
@@ -66,13 +68,21 @@ export function CommentListItem({ comment, onAction }: CommentListItemProps) {
66
68
  </Text>
67
69
 
68
70
  <ActionBar className="gap-3 mt-1.5">
69
- <ActionLink onClick={() => setIsEditing(true)}>Edit</ActionLink>
71
+ <ActionLink onClick={() => setIsEditing(true)}>
72
+ {t("commentList.edit")}
73
+ </ActionLink>
70
74
  <ActionLink onClick={() => deleteComment(comment.id)}>
71
- Delete
75
+ {t("commentList.delete")}
72
76
  </ActionLink>
73
- {canGoTo && <ActionLink onClick={handleGoTo}>Go to</ActionLink>}
77
+ {canGoTo && (
78
+ <ActionLink onClick={handleGoTo}>
79
+ {t("commentList.goTo")}
80
+ </ActionLink>
81
+ )}
74
82
  {isUnresolved && (
75
- <ActionLink onClick={handleReanchor}>Re-anchor</ActionLink>
83
+ <ActionLink onClick={handleReanchor}>
84
+ {t("commentList.reanchor")}
85
+ </ActionLink>
76
86
  )}
77
87
  </ActionBar>
78
88
  </>
@@ -1,6 +1,7 @@
1
1
  import { Copy, Trash2 } from "lucide-react";
2
2
  import { useState } from "react";
3
3
  import { useCommentContext } from "../../contexts/CommentContext";
4
+ import { useLocale } from "../../contexts/LocaleContext";
4
5
  import { Button } from "../ui/Button";
5
6
  import { Text } from "../ui/Text";
6
7
  import { CommentListItem } from "./CommentListItem";
@@ -10,6 +11,7 @@ interface CommentManagerProps {
10
11
  }
11
12
 
12
13
  export function CommentManager({ onClose }: CommentManagerProps) {
14
+ const { t } = useLocale();
13
15
  const { comments, copyAllForLLM, deleteAll } = useCommentContext();
14
16
  const [confirmingDelete, setConfirmingDelete] = useState(false);
15
17
 
@@ -31,7 +33,7 @@ export function CommentManager({ onClose }: CommentManagerProps) {
31
33
  {confirmingDelete ? (
32
34
  <div className="px-3 py-2 border-b border-zinc-100">
33
35
  <Text variant="caption" className="mb-1.5">
34
- Delete all {comments.length} comments?
36
+ {t("commentManager.deleteAllConfirm", { count: comments.length })}
35
37
  </Text>
36
38
  <div className="flex gap-3">
37
39
  <Button
@@ -43,7 +45,7 @@ export function CommentManager({ onClose }: CommentManagerProps) {
43
45
  onClose();
44
46
  }}
45
47
  >
46
- Delete
48
+ {t("commentManager.delete")}
47
49
  </Button>
48
50
  <Button
49
51
  variant="ghost"
@@ -51,7 +53,7 @@ export function CommentManager({ onClose }: CommentManagerProps) {
51
53
  className="h-auto p-0 text-xs"
52
54
  onClick={() => setConfirmingDelete(false)}
53
55
  >
54
- Cancel
56
+ {t("commentManager.cancel")}
55
57
  </Button>
56
58
  </div>
57
59
  </div>
@@ -61,7 +63,10 @@ export function CommentManager({ onClose }: CommentManagerProps) {
61
63
  <span>
62
64
  {resolvedCount}
63
65
  {unresolvedCount > 0 && (
64
- <span> · {unresolvedCount} unresolved</span>
66
+ <span>
67
+ {" "}
68
+ · {unresolvedCount} {t("commentManager.unresolved")}
69
+ </span>
65
70
  )}
66
71
  </span>
67
72
  <span className="flex items-center gap-1">
@@ -69,7 +74,7 @@ export function CommentManager({ onClose }: CommentManagerProps) {
69
74
  type="button"
70
75
  className="p-1 rounded hover:bg-zinc-100 text-zinc-400 hover:text-zinc-600 transition-colors"
71
76
  onClick={copyAllForLLM}
72
- title="Copy all comments"
77
+ title={t("commentManager.copyAllTitle")}
73
78
  >
74
79
  <Copy size={13} />
75
80
  </button>
@@ -77,7 +82,7 @@ export function CommentManager({ onClose }: CommentManagerProps) {
77
82
  type="button"
78
83
  className="p-1 rounded hover:bg-zinc-100 text-zinc-400 hover:text-red-500 transition-colors"
79
84
  onClick={() => setConfirmingDelete(true)}
80
- title="Delete all comments"
85
+ title={t("commentManager.deleteAllTitle")}
81
86
  >
82
87
  <Trash2 size={13} />
83
88
  </button>
@@ -89,7 +94,9 @@ export function CommentManager({ onClose }: CommentManagerProps) {
89
94
  <div className="overflow-y-auto max-h-80">
90
95
  {sortedComments.length === 0 ? (
91
96
  <Text variant="caption" asChild>
92
- <div className="px-3 py-4 text-center">No comments yet</div>
97
+ <div className="px-3 py-4 text-center">
98
+ {t("commentManager.noComments")}
99
+ </div>
93
100
  </Text>
94
101
  ) : (
95
102
  sortedComments.map((comment) => (
@@ -1,6 +1,7 @@
1
1
  import { ChevronLeft, ChevronRight } from "lucide-react";
2
2
  import { useEffect, useRef, useState } from "react";
3
3
  import { useCommentContext } from "../../contexts/CommentContext";
4
+ import { useLocale } from "../../contexts/LocaleContext";
4
5
  import { cn } from "../../lib/utils";
5
6
  import { Button } from "../ui/Button";
6
7
  import { Text } from "../ui/Text";
@@ -8,6 +9,7 @@ import { Text } from "../ui/Text";
8
9
  const ANIMATION_DURATION_MS = 200;
9
10
 
10
11
  export function CommentNav() {
12
+ const { t } = useLocale();
11
13
  const { currentIndex, sortedComments, navigatePrevious, navigateNext } =
12
14
  useCommentContext();
13
15
  const totalComments = sortedComments.length;
@@ -67,7 +69,7 @@ export function CommentNav() {
67
69
  "scale-90 bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-300",
68
70
  )}
69
71
  onClick={handlePrevious}
70
- title="Previous comment (Alt+↑)"
72
+ title={t("commentNav.previous")}
71
73
  >
72
74
  <ChevronLeft className="w-4 h-4" />
73
75
  </Button>
@@ -81,7 +83,10 @@ export function CommentNav() {
81
83
  animating === "next" && "translate-x-0.5",
82
84
  )}
83
85
  >
84
- {currentIndex + 1} of {totalComments}
86
+ {t("commentNav.of", {
87
+ current: currentIndex + 1,
88
+ total: totalComments,
89
+ })}
85
90
  </span>
86
91
  </Text>
87
92
 
@@ -94,7 +99,7 @@ export function CommentNav() {
94
99
  "scale-90 bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-300",
95
100
  )}
96
101
  onClick={handleNext}
97
- title="Next comment (Alt+↓)"
102
+ title={t("commentNav.next")}
98
103
  >
99
104
  <ChevronRight className="w-4 h-4" />
100
105
  </Button>