@peaske7/readit 0.1.6 → 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 (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 +23 -6
  5. package/src/cli/index.ts +167 -19
  6. package/src/components/ActionsMenu.tsx +12 -10
  7. package/src/components/DocumentViewer/CodeBlock.tsx +131 -54
  8. package/src/components/DocumentViewer/DocumentViewer.tsx +6 -6
  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 +160 -117
  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,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>
@@ -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}