newpr 1.0.26 → 1.0.28

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 (30) hide show
  1. package/package.json +1 -1
  2. package/src/analyzer/pipeline.ts +1 -0
  3. package/src/config/index.ts +1 -0
  4. package/src/config/store.ts +1 -0
  5. package/src/llm/prompts.ts +10 -4
  6. package/src/types/config.ts +1 -0
  7. package/src/web/client/components/AppShell.tsx +29 -22
  8. package/src/web/client/components/ChatSection.tsx +18 -12
  9. package/src/web/client/components/DetailPane.tsx +10 -6
  10. package/src/web/client/components/ErrorScreen.tsx +15 -41
  11. package/src/web/client/components/FeasibilityAlert.tsx +5 -3
  12. package/src/web/client/components/InputScreen.tsx +21 -17
  13. package/src/web/client/components/LoadingTimeline.tsx +22 -17
  14. package/src/web/client/components/ResultsScreen.tsx +31 -20
  15. package/src/web/client/components/ReviewModal.tsx +23 -15
  16. package/src/web/client/components/SettingsPanel.tsx +100 -25
  17. package/src/web/client/lib/i18n/context.tsx +76 -0
  18. package/src/web/client/lib/i18n/en.ts +276 -0
  19. package/src/web/client/lib/i18n/index.ts +3 -0
  20. package/src/web/client/lib/i18n/ko.ts +274 -0
  21. package/src/web/client/main.tsx +4 -1
  22. package/src/web/client/panels/CartoonPanel.tsx +12 -10
  23. package/src/web/client/panels/DiscussionPanel.tsx +14 -12
  24. package/src/web/client/panels/FilesPanel.tsx +14 -9
  25. package/src/web/client/panels/GroupsPanel.tsx +3 -1
  26. package/src/web/client/panels/SlidesPanel.tsx +17 -15
  27. package/src/web/client/panels/StackPanel.tsx +50 -44
  28. package/src/web/client/panels/StoryPanel.tsx +5 -3
  29. package/src/web/server/routes.ts +27 -21
  30. package/src/web/styles/built.css +1 -1
@@ -2,6 +2,7 @@ import { useState, useEffect } from "react";
2
2
  import { CornerDownLeft, GitPullRequest, ExternalLink, ChevronUp } from "lucide-react";
3
3
  import type { SessionRecord } from "../../../history/types.ts";
4
4
  import { analytics } from "../lib/analytics.ts";
5
+ import { useI18n, type TranslationKey } from "../lib/i18n/index.ts";
5
6
 
6
7
  interface ToolStatus {
7
8
  name: string;
@@ -23,16 +24,16 @@ const RISK_DOT: Record<string, string> = {
23
24
  critical: "bg-red-600",
24
25
  };
25
26
 
26
- function timeAgo(date: string): string {
27
+ function timeAgo(date: string, t: (key: TranslationKey, params?: Record<string, string | number>) => string): string {
27
28
  const s = Math.floor((Date.now() - new Date(date).getTime()) / 1000);
28
- if (s < 60) return "just now";
29
+ if (s < 60) return t("time.justNow");
29
30
  const m = Math.floor(s / 60);
30
- if (m < 60) return `${m}m ago`;
31
+ if (m < 60) return t("time.minutesAgo", { n: m });
31
32
  const h = Math.floor(m / 60);
32
- if (h < 24) return `${h}h ago`;
33
+ if (h < 24) return t("time.hoursAgo", { n: h });
33
34
  const d = Math.floor(h / 24);
34
- if (d < 30) return `${d}d ago`;
35
- return `${Math.floor(d / 30)}mo ago`;
35
+ if (d < 30) return t("time.daysAgo", { n: d });
36
+ return t("time.monthsAgo", { n: Math.floor(d / 30) });
36
37
  }
37
38
 
38
39
  function StatusDot({ ok, optional }: { ok: boolean; optional?: boolean }) {
@@ -42,6 +43,7 @@ function StatusDot({ ok, optional }: { ok: boolean; optional?: boolean }) {
42
43
  }
43
44
 
44
45
  function CompactStatus({ data }: { data: PreflightData }) {
46
+ const { t } = useI18n();
45
47
  const [open, setOpen] = useState(false);
46
48
  const gh = data.github;
47
49
  const allOk = gh.installed && gh.authenticated && data.openrouterKey;
@@ -53,7 +55,7 @@ function CompactStatus({ data }: { data: PreflightData }) {
53
55
  className="flex items-center gap-1.5 hover:opacity-70 transition-opacity"
54
56
  >
55
57
  <StatusDot ok={allOk} />
56
- <span className="text-[10px] font-mono text-muted-foreground/30">status</span>
58
+ <span className="text-[10px] font-mono text-muted-foreground/30">{t("input.status")}</span>
57
59
  <ChevronUp className={`h-2.5 w-2.5 text-muted-foreground/20 transition-transform ${open ? "" : "rotate-180"}`} />
58
60
  </button>
59
61
  {open && (
@@ -94,6 +96,7 @@ export function InputScreen({
94
96
  onSessionSelect?: (id: string) => void;
95
97
  version?: string;
96
98
  }) {
99
+ const { t } = useI18n();
97
100
  const [value, setValue] = useState("");
98
101
  const [focused, setFocused] = useState(false);
99
102
  const [preflight, setPreflight] = useState<PreflightData | null>(null);
@@ -126,9 +129,9 @@ export function InputScreen({
126
129
  </span>
127
130
  )}
128
131
  </div>
129
- <p className="text-base text-muted-foreground/50">
130
- Turn PRs into navigable stories
131
- </p>
132
+ <p className="text-base text-muted-foreground/50">
133
+ {t("input.tagline")}
134
+ </p>
132
135
  </div>
133
136
 
134
137
  <div>
@@ -157,7 +160,7 @@ export function InputScreen({
157
160
  </div>
158
161
  <div className="flex justify-center mt-2.5">
159
162
  <span className="text-[10px] text-muted-foreground/20 font-mono">
160
- ↵ Enter to analyze
163
+ {t("input.enterToAnalyze")}
161
164
  </span>
162
165
  </div>
163
166
  </form>
@@ -168,9 +171,9 @@ export function InputScreen({
168
171
 
169
172
  {recents.length > 0 && (
170
173
  <div className="space-y-3">
171
- <div className="text-[10px] font-medium text-muted-foreground/25 uppercase tracking-[0.15em] text-center">
172
- Recent
173
- </div>
174
+ <div className="text-[10px] font-medium text-muted-foreground/25 uppercase tracking-[0.15em] text-center">
175
+ {t("input.recent")}
176
+ </div>
174
177
  <div className="space-y-px">
175
178
  {recents.map((s) => (
176
179
  <button
@@ -189,7 +192,7 @@ export function InputScreen({
189
192
  <span className="font-mono">{s.repo.split("/").pop()}</span>
190
193
  <span className="font-mono">#{s.pr_number}</span>
191
194
  <span className="text-muted-foreground/15">·</span>
192
- <span>{timeAgo(s.analyzed_at)}</span>
195
+ <span>{timeAgo(s.analyzed_at, t)}</span>
193
196
  </div>
194
197
  </button>
195
198
  ))}
@@ -211,6 +214,7 @@ export function InputScreen({
211
214
  const SIONIC_HERO_BG = "https://www.sionic.ai/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fmain-intro-bg.1455295d.png&w=1920&q=75";
212
215
 
213
216
  function SponsorBanner() {
217
+ const { t } = useI18n();
214
218
  return (
215
219
  <a
216
220
  href="https://www.sionic.ai"
@@ -234,11 +238,11 @@ function SponsorBanner() {
234
238
  />
235
239
  <div className="h-3 w-px bg-white/15 shrink-0" />
236
240
  <span className="text-[10px] text-white/45 truncate">
237
- The Power of AI for Every Business
241
+ {t("input.sponsorTagline")}
238
242
  </span>
239
243
  </div>
240
244
  <div className="relative flex items-center gap-1.5 shrink-0">
241
- <span className="text-[8px] text-white/20 uppercase tracking-widest">Ad</span>
245
+ <span className="text-[8px] text-white/20 uppercase tracking-widest">{t("input.ad")}</span>
242
246
  <ExternalLink className="h-2.5 w-2.5 text-white/15 group-hover:text-white/40 transition-colors" />
243
247
  </div>
244
248
  </a>
@@ -2,18 +2,19 @@ import { useState, useEffect, useRef } from "react";
2
2
  import { CheckCircle2, Circle, Loader2 } from "lucide-react";
3
3
  import type { ProgressEvent, ProgressStage } from "../../../analyzer/progress.ts";
4
4
  import { stageIndex, allStages } from "../../../analyzer/progress.ts";
5
-
6
- const STAGE_LABELS: Record<ProgressStage, string> = {
7
- fetching: "Fetch PR data",
8
- parsing: "Parse diff",
9
- cloning: "Clone repository",
10
- checkout: "Checkout branches",
11
- exploring: "Explore codebase",
12
- analyzing: "Analyze files",
13
- grouping: "Group changes",
14
- summarizing: "Generate summary",
15
- narrating: "Write narrative",
16
- done: "Complete",
5
+ import { useI18n, type TranslationKey } from "../lib/i18n/index.ts";
6
+
7
+ const STAGE_KEYS: Record<ProgressStage, TranslationKey> = {
8
+ fetching: "loading.fetching",
9
+ parsing: "loading.parsing",
10
+ cloning: "loading.cloning",
11
+ checkout: "loading.checkout",
12
+ exploring: "loading.exploring",
13
+ analyzing: "loading.analyzing",
14
+ grouping: "loading.grouping",
15
+ summarizing: "loading.summarizing",
16
+ narrating: "loading.narrating",
17
+ done: "loading.done",
17
18
  };
18
19
 
19
20
  const MAX_LOG_LINES = 8;
@@ -29,7 +30,7 @@ interface StepInfo {
29
30
  log: string[];
30
31
  }
31
32
 
32
- function buildSteps(events: ProgressEvent[]): StepInfo[] {
33
+ function buildSteps(events: ProgressEvent[], stageLabels: Record<ProgressStage, string>): StepInfo[] {
33
34
  const stages = allStages();
34
35
  const lastByStage = new Map<ProgressStage, ProgressEvent>();
35
36
  const firstTs = new Map<ProgressStage, number>();
@@ -72,7 +73,7 @@ function buildSteps(events: ProgressEvent[]): StepInfo[] {
72
73
 
73
74
  return {
74
75
  stage,
75
- message: event?.message ?? STAGE_LABELS[stage],
76
+ message: event?.message ?? stageLabels[stage],
76
77
  done,
77
78
  active,
78
79
  durationMs,
@@ -107,7 +108,11 @@ export function LoadingTimeline({
107
108
  logEndRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest" });
108
109
  }, [events.length]);
109
110
 
110
- const steps = buildSteps(events);
111
+ const { t } = useI18n();
112
+ const stageLabels = Object.fromEntries(
113
+ Object.entries(STAGE_KEYS).map(([stage, key]) => [stage, t(key)]),
114
+ ) as Record<ProgressStage, string>;
115
+ const steps = buildSteps(events, stageLabels);
111
116
  const seconds = Math.floor(elapsed / 1000);
112
117
 
113
118
  const prInfo = events.find((e) => e.pr_title);
@@ -136,7 +141,7 @@ export function LoadingTimeline({
136
141
 
137
142
  <div className="space-y-3">
138
143
  {steps.map((step) => {
139
- const completionDetail = step.message !== STAGE_LABELS[step.stage] ? step.message : "";
144
+ const completionDetail = step.message !== stageLabels[step.stage] ? step.message : "";
140
145
  const progress = step.current !== undefined && step.total !== undefined
141
146
  ? ` (${step.current}/${step.total})`
142
147
  : "";
@@ -154,7 +159,7 @@ export function LoadingTimeline({
154
159
  <div className="min-w-0 flex-1">
155
160
  <div className="flex items-center gap-2">
156
161
  <span className={`text-base font-medium ${step.done ? "text-muted-foreground" : step.active ? "text-foreground" : "text-muted-foreground/50"}`}>
157
- {STAGE_LABELS[step.stage]}{progress}
162
+ {stageLabels[step.stage]}{progress}
158
163
  </span>
159
164
  {step.done && step.durationMs !== undefined && (
160
165
  <span className="text-sm text-muted-foreground/60">
@@ -11,6 +11,7 @@ import { SlidesPanel } from "../panels/SlidesPanel.tsx";
11
11
  import { StackPanel } from "../panels/StackPanel.tsx";
12
12
  import { ReviewModal } from "./ReviewModal.tsx";
13
13
  import { useOutdatedCheck } from "../hooks/useOutdatedCheck.ts";
14
+ import { useI18n, type TranslationKey } from "../lib/i18n/index.ts";
14
15
 
15
16
  const VALID_TABS = ["story", "discussion", "groups", "files", "stack", "slides", "cartoon"] as const;
16
17
  type TabValue = typeof VALID_TABS[number];
@@ -34,11 +35,18 @@ const RISK_DOT: Record<string, string> = {
34
35
  critical: "bg-red-600",
35
36
  };
36
37
 
37
- const STATE_STYLES: Record<string, { bg: string; text: string; label: string }> = {
38
- open: { bg: "bg-green-500/10", text: "text-green-600 dark:text-green-400", label: "Open" },
39
- merged: { bg: "bg-purple-500/10", text: "text-purple-600 dark:text-purple-400", label: "Merged" },
40
- closed: { bg: "bg-red-500/10", text: "text-red-600 dark:text-red-400", label: "Closed" },
41
- draft: { bg: "bg-neutral-500/10", text: "text-neutral-500", label: "Draft" },
38
+ const STATE_STYLE_CLASS: Record<string, { bg: string; text: string }> = {
39
+ open: { bg: "bg-green-500/10", text: "text-green-600 dark:text-green-400" },
40
+ merged: { bg: "bg-purple-500/10", text: "text-purple-600 dark:text-purple-400" },
41
+ closed: { bg: "bg-red-500/10", text: "text-red-600 dark:text-red-400" },
42
+ draft: { bg: "bg-neutral-500/10", text: "text-neutral-500" },
43
+ };
44
+
45
+ const STATE_LABEL_KEYS: Record<string, TranslationKey> = {
46
+ open: "results.open",
47
+ merged: "results.merged",
48
+ closed: "results.closed",
49
+ draft: "results.draft",
42
50
  };
43
51
 
44
52
  export function ResultsScreen({
@@ -68,6 +76,7 @@ export function ResultsScreen({
68
76
  const [tab, setTab] = useState<TabValue>(getInitialTab);
69
77
  const [reviewOpen, setReviewOpen] = useState(false);
70
78
  const outdated = useOutdatedCheck(sessionId);
79
+ const { t } = useI18n();
71
80
 
72
81
  useEffect(() => {
73
82
  onTabChange?.(tab);
@@ -135,10 +144,11 @@ export function ResultsScreen({
135
144
  {repoSlug}
136
145
  </a>
137
146
  {meta.pr_state && (() => {
138
- const s = STATE_STYLES[meta.pr_state] ?? STATE_STYLES.open!;
147
+ const s = STATE_STYLE_CLASS[meta.pr_state] ?? STATE_STYLE_CLASS.open!;
148
+ const labelKey = STATE_LABEL_KEYS[meta.pr_state] ?? STATE_LABEL_KEYS.open!;
139
149
  return (
140
150
  <span className={`text-[10px] font-medium px-1.5 py-0.5 rounded-md ${s!.bg} ${s!.text}`}>
141
- {s!.label}
151
+ {t(labelKey)}
142
152
  </span>
143
153
  );
144
154
  })()}
@@ -151,7 +161,7 @@ export function ResultsScreen({
151
161
  className="flex items-center gap-1.5 h-7 px-3 rounded-md border text-[11px] font-medium text-foreground hover:bg-accent/40 transition-colors shrink-0"
152
162
  >
153
163
  <Check className="h-3 w-3" />
154
- Review
164
+ {t("results.review")}
155
165
  <ChevronDown className="h-3 w-3 text-muted-foreground/40" />
156
166
  </button>
157
167
  )}
@@ -183,7 +193,7 @@ export function ResultsScreen({
183
193
  <span className="text-green-600 dark:text-green-400 tabular-nums">+{meta.total_additions}</span>
184
194
  <span className="text-red-600 dark:text-red-400 tabular-nums">-{meta.total_deletions}</span>
185
195
  <span className="text-muted-foreground/25">·</span>
186
- <span className="tabular-nums">{meta.total_files_changed} files</span>
196
+ <span className="tabular-nums">{t("results.nFiles", { n: meta.total_files_changed })}</span>
187
197
  </div>
188
198
  </div>
189
199
  </div>
@@ -191,7 +201,7 @@ export function ResultsScreen({
191
201
  <div className="flex items-center gap-2 mt-3 px-3 py-2 rounded-lg border border-yellow-500/20 bg-yellow-500/5">
192
202
  <AlertTriangle className="h-3.5 w-3.5 text-yellow-600 dark:text-yellow-400 shrink-0" />
193
203
  <span className="text-[11px] text-yellow-700 dark:text-yellow-300 flex-1">
194
- This PR has been updated since this analysis was created.
204
+ {t("results.prUpdated")}
195
205
  </span>
196
206
  {onReanalyze && (
197
207
  <button
@@ -200,7 +210,7 @@ export function ResultsScreen({
200
210
  className="flex items-center gap-1 text-[11px] font-medium text-yellow-700 dark:text-yellow-300 hover:text-yellow-900 dark:hover:text-yellow-100 shrink-0 transition-colors"
201
211
  >
202
212
  <RefreshCw className="h-3 w-3" />
203
- Re-analyze
213
+ {t("results.reAnalyze")}
204
214
  </button>
205
215
  )}
206
216
  </div>
@@ -218,8 +228,9 @@ export function ResultsScreen({
218
228
  </button>
219
229
  <span className={`h-1.5 w-1.5 rounded-full shrink-0 ${RISK_DOT[summary.risk_level] ?? RISK_DOT.medium}`} />
220
230
  {meta.pr_state && (() => {
221
- const s = STATE_STYLES[meta.pr_state]!;
222
- return <span className={`text-[9px] font-medium px-1 py-px rounded ${s.bg} ${s.text} shrink-0`}>{s.label}</span>;
231
+ const s = STATE_STYLE_CLASS[meta.pr_state]!;
232
+ const labelKey = STATE_LABEL_KEYS[meta.pr_state] ?? STATE_LABEL_KEYS.open!;
233
+ return <span className={`text-[9px] font-medium px-1 py-px rounded ${s.bg} ${s.text} shrink-0`}>{t(labelKey)}</span>;
223
234
  })()}
224
235
  <span className="text-sm font-medium truncate flex-1">{meta.pr_title}</span>
225
236
  <span className="text-xs text-muted-foreground/30 font-mono shrink-0">{repoSlug}</span>
@@ -229,34 +240,34 @@ export function ResultsScreen({
229
240
  <TabsList className="w-full justify-start">
230
241
  <TabsTrigger value="story">
231
242
  <BookOpen className="h-3 w-3 shrink-0" />
232
- Story
243
+ {t("results.story")}
233
244
  </TabsTrigger>
234
245
  <TabsTrigger value="discussion">
235
246
  <MessageSquare className="h-3 w-3 shrink-0" />
236
- Discussion
247
+ {t("results.discussion")}
237
248
  </TabsTrigger>
238
249
  <TabsTrigger value="groups">
239
250
  <Layers className="h-3 w-3 shrink-0" />
240
- Groups
251
+ {t("results.groups")}
241
252
  </TabsTrigger>
242
253
  <TabsTrigger value="files">
243
254
  <FolderTree className="h-3 w-3 shrink-0" />
244
- Files
255
+ {t("results.files")}
245
256
  </TabsTrigger>
246
257
  <TabsTrigger value="stack">
247
258
  <GitPullRequestArrow className="h-3 w-3 shrink-0" />
248
- Stack
259
+ {t("results.stack")}
249
260
  </TabsTrigger>
250
261
  {(!enabledPlugins || enabledPlugins.includes("slides")) && (
251
262
  <TabsTrigger value="slides">
252
263
  <Presentation className="h-3 w-3 shrink-0" />
253
- Slides
264
+ {t("results.slides")}
254
265
  </TabsTrigger>
255
266
  )}
256
267
  {(!enabledPlugins || enabledPlugins.includes("cartoon")) && (
257
268
  <TabsTrigger value="cartoon">
258
269
  <Sparkles className="h-3 w-3 shrink-0" />
259
- Comic
270
+ {t("results.comic")}
260
271
  </TabsTrigger>
261
272
  )}
262
273
  </TabsList>
@@ -3,6 +3,7 @@ import { X, Check, MessageSquare, Loader2, AlertCircle, ExternalLink } from "luc
3
3
  import { TipTapEditor, getTextWithAnchors } from "./TipTapEditor.tsx";
4
4
  import type { useEditor } from "@tiptap/react";
5
5
  import { analytics } from "../lib/analytics.ts";
6
+ import { useI18n } from "../lib/i18n/index.ts";
6
7
 
7
8
  type ReviewEvent = "APPROVE" | "REQUEST_CHANGES" | "COMMENT";
8
9
 
@@ -36,11 +37,18 @@ interface ReviewModalProps {
36
37
  }
37
38
 
38
39
  export function ReviewModal({ prUrl, onClose }: ReviewModalProps) {
40
+ const { t } = useI18n();
39
41
  const [event, setEvent] = useState<ReviewEvent>("APPROVE");
40
42
  const [submitting, setSubmitting] = useState(false);
41
43
  const [result, setResult] = useState<{ ok: boolean; html_url?: string; error?: string } | null>(null);
42
44
  const editorRef = useRef<ReturnType<typeof useEditor>>(null);
43
45
 
46
+ const eventLabels: Record<ReviewEvent, string> = {
47
+ APPROVE: t("review.approve"),
48
+ REQUEST_CHANGES: t("review.requestChanges"),
49
+ COMMENT: t("review.comment"),
50
+ };
51
+
44
52
  const handleSubmit = useCallback(async () => {
45
53
  if (submitting) return;
46
54
  setSubmitting(true);
@@ -74,7 +82,7 @@ export function ReviewModal({ prUrl, onClose }: ReviewModalProps) {
74
82
  onClick={(e) => e.stopPropagation()}
75
83
  >
76
84
  <div className="flex items-center justify-between px-4 h-11 border-b">
77
- <span className="text-sm font-medium">Submit Review</span>
85
+ <span className="text-sm font-medium">{t("review.submitReview")}</span>
78
86
  <button
79
87
  type="button"
80
88
  onClick={onClose}
@@ -89,7 +97,7 @@ export function ReviewModal({ prUrl, onClose }: ReviewModalProps) {
89
97
  <div className="space-y-4 py-2">
90
98
  <div className="flex items-center gap-2 text-green-600 dark:text-green-400">
91
99
  <Check className="h-4 w-4" />
92
- <span className="text-sm font-medium">Review submitted</span>
100
+ <span className="text-sm font-medium">{t("review.reviewSubmitted")}</span>
93
101
  </div>
94
102
  {result.html_url && (
95
103
  <a
@@ -98,8 +106,8 @@ export function ReviewModal({ prUrl, onClose }: ReviewModalProps) {
98
106
  rel="noopener noreferrer"
99
107
  className="inline-flex items-center gap-1.5 text-[11px] text-muted-foreground/60 hover:text-foreground transition-colors"
100
108
  >
101
- <ExternalLink className="h-3 w-3" />
102
- View on GitHub
109
+ <ExternalLink className="h-3 w-3" />
110
+ {t("review.viewOnGithub")}
103
111
  </a>
104
112
  )}
105
113
  <div className="flex justify-end">
@@ -108,7 +116,7 @@ export function ReviewModal({ prUrl, onClose }: ReviewModalProps) {
108
116
  onClick={onClose}
109
117
  className="text-[11px] text-muted-foreground/50 hover:text-foreground px-3 py-1.5 rounded-md hover:bg-accent/40 transition-colors"
110
118
  >
111
- Close
119
+ {t("common.close")}
112
120
  </button>
113
121
  </div>
114
122
  </div>
@@ -124,19 +132,19 @@ export function ReviewModal({ prUrl, onClose }: ReviewModalProps) {
124
132
  event === e.value ? e.activeClass : e.class
125
133
  }`}
126
134
  >
127
- {e.label}
135
+ {eventLabels[e.value]}
128
136
  </button>
129
137
  ))}
130
138
  </div>
131
139
 
132
140
  <div>
133
141
  <div className="text-[10px] font-medium text-muted-foreground/40 uppercase tracking-wider mb-2">
134
- Message {event !== "APPROVE" && <span className="text-red-500/60 normal-case">*</span>}
142
+ {t("review.message")} {event !== "APPROVE" && <span className="text-red-500/60 normal-case">*</span>}
135
143
  </div>
136
144
  <div className="rounded-lg border px-3 py-2.5 min-h-[80px] focus-within:border-foreground/15 transition-colors">
137
145
  <TipTapEditor
138
146
  editorRef={editorRef}
139
- placeholder={event === "APPROVE" ? "Optional message..." : "Describe the changes needed..."}
147
+ placeholder={event === "APPROVE" ? t("review.optionalMessage") : t("review.describeChanges")}
140
148
  autoFocus
141
149
  submitOnModEnter
142
150
  onSubmit={handleSubmit}
@@ -152,17 +160,17 @@ export function ReviewModal({ prUrl, onClose }: ReviewModalProps) {
152
160
  )}
153
161
 
154
162
  <div className="flex items-center justify-between pt-1">
155
- <span className="text-[10px] text-muted-foreground/25">
156
- {navigator.platform.includes("Mac") ? "⌘" : "Ctrl"}+Enter to submit
157
- </span>
163
+ <span className="text-[10px] text-muted-foreground/25">
164
+ {t("review.ctrlEnterToSubmit", { key: navigator.platform.includes("Mac") ? "⌘" : "Ctrl" })}
165
+ </span>
158
166
  <div className="flex gap-2">
159
167
  <button
160
168
  type="button"
161
169
  onClick={onClose}
162
170
  className="text-[11px] text-muted-foreground/50 hover:text-foreground px-3 py-1.5 rounded-md hover:bg-accent/40 transition-colors"
163
- >
164
- Cancel
165
- </button>
171
+ >
172
+ {t("common.cancel")}
173
+ </button>
166
174
  <button
167
175
  type="button"
168
176
  onClick={handleSubmit}
@@ -176,7 +184,7 @@ export function ReviewModal({ prUrl, onClose }: ReviewModalProps) {
176
184
  ) : (
177
185
  <MessageSquare className="h-3 w-3" />
178
186
  )}
179
- Submit
187
+ {t("common.submit")}
180
188
  </button>
181
189
  </div>
182
190
  </div>