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
@@ -1,6 +1,7 @@
1
1
  import { useState, useEffect, useCallback, useRef } from "react";
2
2
  import { X, Check, Loader2, Search, ChevronDown } from "lucide-react";
3
3
  import { analytics, getConsent, setConsent } from "../lib/analytics.ts";
4
+ import { useI18n } from "../lib/i18n/index.ts";
4
5
 
5
6
  interface ConfigData {
6
7
  model: string;
@@ -13,6 +14,7 @@ interface ConfigData {
13
14
  has_github_token: boolean;
14
15
  enabled_plugins: string[];
15
16
  available_plugins: Array<{ id: string; name: string }>;
17
+ custom_prompt: string;
16
18
  defaults: {
17
19
  model: string;
18
20
  language: string;
@@ -45,6 +47,7 @@ const LANGUAGES = [
45
47
  ];
46
48
 
47
49
  export function SettingsPanel({ onClose, onFeaturesChange }: { onClose: () => void; onFeaturesChange?: () => void }) {
50
+ const { t, locale, setLocale } = useI18n();
48
51
  const [config, setConfig] = useState<ConfigData | null>(null);
49
52
  const [saving, setSaving] = useState(false);
50
53
  const [saved, setSaved] = useState(false);
@@ -100,7 +103,7 @@ export function SettingsPanel({ onClose, onFeaturesChange }: { onClose: () => vo
100
103
  return (
101
104
  <div>
102
105
  <div className="flex items-center justify-between mb-6">
103
- <h2 className="text-sm font-semibold">Settings</h2>
106
+ <h2 className="text-sm font-semibold">{t("settings.title")}</h2>
104
107
  <div className="flex items-center gap-2">
105
108
  {saving && <Loader2 className="h-3 w-3 animate-spin text-muted-foreground/40" />}
106
109
  {saved && <Check className="h-3 w-3 text-green-500" />}
@@ -115,8 +118,8 @@ export function SettingsPanel({ onClose, onFeaturesChange }: { onClose: () => vo
115
118
  </div>
116
119
 
117
120
  <div className="space-y-6">
118
- <Section title="Authentication">
119
- <Row label="OpenRouter API Key">
121
+ <Section title={t("settings.authentication")}>
122
+ <Row label={t("settings.openrouterApiKey")}>
120
123
  {showApiKeyField ? (
121
124
  <div className="flex gap-1.5">
122
125
  <input
@@ -148,37 +151,37 @@ export function SettingsPanel({ onClose, onFeaturesChange }: { onClose: () => vo
148
151
  }}
149
152
  className="h-7 px-2.5 rounded-md bg-foreground text-background text-[11px] font-medium disabled:opacity-20 hover:opacity-80 transition-opacity"
150
153
  >
151
- Save
154
+ {t("common.save")}
152
155
  </button>
153
156
  </div>
154
157
  ) : (
155
158
  <div className="flex items-center gap-2">
156
159
  <span className={`h-1.5 w-1.5 rounded-full ${config.has_api_key ? "bg-green-500" : "bg-red-500"}`} />
157
160
  <span className="text-[11px] text-muted-foreground/50">
158
- {config.has_api_key ? "Configured" : "Not set"}
161
+ {config.has_api_key ? t("settings.configured") : t("settings.notSet")}
159
162
  </span>
160
163
  <button
161
164
  type="button"
162
165
  onClick={() => setShowApiKeyField(true)}
163
166
  className="text-[11px] text-muted-foreground/40 hover:text-foreground transition-colors"
164
167
  >
165
- {config.has_api_key ? "Change" : "Set"}
168
+ {config.has_api_key ? t("settings.change") : t("settings.set")}
166
169
  </button>
167
170
  </div>
168
171
  )}
169
172
  </Row>
170
- <Row label="GitHub Token">
173
+ <Row label={t("settings.githubToken")}>
171
174
  <div className="flex items-center gap-2">
172
175
  <span className={`h-1.5 w-1.5 rounded-full ${config.has_github_token ? "bg-green-500" : "bg-red-500"}`} />
173
176
  <span className="text-[11px] text-muted-foreground/50">
174
- {config.has_github_token ? "gh CLI" : "Not detected"}
177
+ {config.has_github_token ? t("settings.ghCli") : t("settings.notDetected")}
175
178
  </span>
176
179
  </div>
177
180
  </Row>
178
181
  </Section>
179
182
 
180
- <Section title="Model">
181
- <Row label="LLM">
183
+ <Section title={t("settings.model")}>
184
+ <Row label={t("settings.llm")}>
182
185
  {config.has_api_key ? (
183
186
  <ModelSelect
184
187
  value={config.model}
@@ -186,10 +189,10 @@ export function SettingsPanel({ onClose, onFeaturesChange }: { onClose: () => vo
186
189
  onChange={(id: string) => save({ model: id })}
187
190
  />
188
191
  ) : (
189
- <span className="text-[11px] text-muted-foreground/40">Set API key first</span>
192
+ <span className="text-[11px] text-muted-foreground/40">{t("settings.setApiKeyFirst")}</span>
190
193
  )}
191
194
  </Row>
192
- <Row label="Agent">
195
+ <Row label={t("settings.agent")}>
193
196
  <div className="flex gap-px rounded-md border p-0.5">
194
197
  {AGENTS.map((a) => (
195
198
  <button
@@ -207,36 +210,66 @@ export function SettingsPanel({ onClose, onFeaturesChange }: { onClose: () => vo
207
210
  ))}
208
211
  </div>
209
212
  </Row>
210
- <Row label="Language">
213
+ <Row label={t("settings.language")}>
211
214
  <select
212
215
  value={config.language}
213
216
  onChange={(e) => save({ language: e.target.value })}
214
217
  className="h-7 rounded-md border bg-background px-2 text-[11px] focus:outline-none focus:border-foreground/20 cursor-pointer"
215
218
  >
216
219
  {LANGUAGES.map((l) => (
217
- <option key={l} value={l}>{l === "auto" ? "Auto-detect" : l}</option>
220
+ <option key={l} value={l}>{l === "auto" ? t("settings.autoDetect") : l}</option>
218
221
  ))}
219
222
  </select>
220
223
  </Row>
224
+ <Row label={t("settings.uiLanguage")}>
225
+ <div className="flex gap-px rounded-md border p-0.5">
226
+ {(["en", "ko"] as const).map((l) => (
227
+ <button
228
+ key={l}
229
+ type="button"
230
+ onClick={() => setLocale(l)}
231
+ className={`px-2.5 py-1 rounded text-[11px] transition-colors ${
232
+ locale === l
233
+ ? "bg-accent text-foreground font-medium"
234
+ : "text-muted-foreground/50 hover:text-foreground"
235
+ }`}
236
+ >
237
+ {l === "en" ? "English" : "한국어"}
238
+ </button>
239
+ ))}
240
+ </div>
241
+ </Row>
221
242
  </Section>
222
243
 
223
- <Section title="Limits">
224
- <Row label="Max files">
244
+ <Section title={t("settings.limits")}>
245
+ <Row label={t("settings.maxFiles")}>
225
246
  <NumberInput value={config.max_files} onChange={(v) => save({ max_files: v })} />
226
247
  </Row>
227
- <Row label="Timeout">
248
+ <Row label={t("settings.timeout")}>
228
249
  <div className="flex items-center gap-1.5">
229
250
  <NumberInput value={config.timeout} onChange={(v) => save({ timeout: v })} />
230
- <span className="text-[10px] text-muted-foreground/30">sec</span>
251
+ <span className="text-[10px] text-muted-foreground/30">{t("settings.seconds")}</span>
231
252
  </div>
232
253
  </Row>
233
- <Row label="Concurrency">
254
+ <Row label={t("settings.concurrency")}>
234
255
  <NumberInput value={config.concurrency} onChange={(v) => save({ concurrency: v })} />
235
256
  </Row>
236
257
  </Section>
237
258
 
259
+ <Section title={t("settings.customPrompt")}>
260
+ <div className="space-y-2">
261
+ <p className="text-[10px] text-muted-foreground/40 leading-relaxed">
262
+ {t("settings.customPromptDesc")}
263
+ </p>
264
+ <CustomPromptInput
265
+ value={config.custom_prompt}
266
+ onSave={(v) => save({ custom_prompt: v })}
267
+ />
268
+ </div>
269
+ </Section>
270
+
238
271
  {config.available_plugins.length > 0 && (
239
- <Section title="Plugins">
272
+ <Section title={t("settings.plugins")}>
240
273
  <div className="space-y-1">
241
274
  {config.available_plugins.map((p) => {
242
275
  const enabled = config.enabled_plugins.includes(p.id);
@@ -268,7 +301,7 @@ export function SettingsPanel({ onClose, onFeaturesChange }: { onClose: () => vo
268
301
  </div>
269
302
  </Section>
270
303
  )}
271
- <Section title="Privacy">
304
+ <Section title={t("settings.privacy")}>
272
305
  <AnalyticsToggle />
273
306
  </Section>
274
307
  </div>
@@ -276,7 +309,48 @@ export function SettingsPanel({ onClose, onFeaturesChange }: { onClose: () => vo
276
309
  );
277
310
  }
278
311
 
312
+ function CustomPromptInput({ value, onSave }: { value: string; onSave: (v: string) => void }) {
313
+ const { t } = useI18n();
314
+ const [local, setLocal] = useState(value);
315
+ const [focused, setFocused] = useState(false);
316
+
317
+ useEffect(() => { setLocal(value); }, [value]);
318
+
319
+ const dirty = local !== value;
320
+
321
+ function handleSave() {
322
+ if (dirty) onSave(local);
323
+ }
324
+
325
+ return (
326
+ <div className="space-y-1.5">
327
+ <textarea
328
+ value={local}
329
+ onChange={(e) => setLocal(e.target.value)}
330
+ onFocus={() => setFocused(true)}
331
+ onBlur={() => { setFocused(false); handleSave(); }}
332
+ onKeyDown={(e) => {
333
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
334
+ e.preventDefault();
335
+ handleSave();
336
+ (e.target as HTMLTextAreaElement).blur();
337
+ }
338
+ }}
339
+ placeholder={t("settings.customPromptPlaceholder")}
340
+ rows={3}
341
+ className="w-full rounded-md border bg-background px-2.5 py-2 text-[11px] leading-relaxed placeholder:text-muted-foreground/30 focus:outline-none focus:border-foreground/20 resize-y min-h-[60px]"
342
+ />
343
+ {focused && dirty && (
344
+ <p className="text-[10px] text-muted-foreground/30">
345
+ {t("settings.cmdEnterToSave")}
346
+ </p>
347
+ )}
348
+ </div>
349
+ );
350
+ }
351
+
279
352
  function AnalyticsToggle() {
353
+ const { t } = useI18n();
280
354
  const [consent, setLocal] = useState(() => getConsent());
281
355
  const enabled = consent === "granted";
282
356
 
@@ -293,10 +367,10 @@ function AnalyticsToggle() {
293
367
  };
294
368
 
295
369
  return (
296
- <Row label="Usage Analytics">
370
+ <Row label={t("settings.usageAnalytics")}>
297
371
  <div className="flex items-center gap-2">
298
372
  <span className="text-[11px] text-muted-foreground/50">
299
- {enabled ? "Enabled" : "Disabled"}
373
+ {enabled ? t("common.enabled") : t("common.disabled")}
300
374
  </span>
301
375
  <button
302
376
  type="button"
@@ -315,6 +389,7 @@ function AnalyticsToggle() {
315
389
  }
316
390
 
317
391
  function ModelSelect({ value, models: allModels, onChange }: { value: string; models: ModelInfo[]; onChange: (id: string) => void }) {
392
+ const { t } = useI18n();
318
393
  const [open, setOpen] = useState(false);
319
394
  const [search, setSearch] = useState("");
320
395
  const ref = useRef<HTMLDivElement>(null);
@@ -363,14 +438,14 @@ function ModelSelect({ value, models: allModels, onChange }: { value: string; mo
363
438
  type="text"
364
439
  value={search}
365
440
  onChange={(e) => setSearch(e.target.value)}
366
- placeholder="Search models..."
441
+ placeholder={t("settings.searchModels")}
367
442
  className="flex-1 bg-transparent text-[11px] focus:outline-none placeholder:text-muted-foreground/30"
368
443
  />
369
444
  </div>
370
445
  </div>
371
446
  <div className="max-h-[280px] overflow-y-auto p-1">
372
447
  {models.length === 0 && (
373
- <div className="px-2 py-3 text-center text-[11px] text-muted-foreground/40">No models found</div>
448
+ <div className="px-2 py-3 text-center text-[11px] text-muted-foreground/40">{t("settings.noModelsFound")}</div>
374
449
  )}
375
450
  {models.slice(0, 80).map((m, i) => {
376
451
  const isSelected = m.id === value;
@@ -0,0 +1,76 @@
1
+ import { createContext, useContext, useState, useCallback, useMemo, type ReactNode } from "react";
2
+ import { en, type TranslationKeys } from "./en.ts";
3
+ import { ko } from "./ko.ts";
4
+
5
+ export type Locale = "en" | "ko";
6
+
7
+ const LOCALES: Record<Locale, TranslationKeys> = { en, ko: ko as unknown as TranslationKeys };
8
+ const STORAGE_KEY = "newpr-locale";
9
+
10
+ function detectLocale(): Locale {
11
+ try {
12
+ const stored = localStorage.getItem(STORAGE_KEY);
13
+ if (stored === "en" || stored === "ko") return stored;
14
+ } catch {}
15
+ const nav = navigator.language.toLowerCase();
16
+ if (nav.startsWith("ko")) return "ko";
17
+ return "en";
18
+ }
19
+
20
+ type FlatKeys<T, Prefix extends string = ""> = T extends Record<string, unknown>
21
+ ? { [K in keyof T & string]: T[K] extends string ? `${Prefix}${K}` : FlatKeys<T[K], `${Prefix}${K}.`> }[keyof T & string]
22
+ : never;
23
+
24
+ export type TranslationKey = FlatKeys<TranslationKeys>;
25
+
26
+ function getNestedValue(obj: unknown, path: string): string {
27
+ let current = obj;
28
+ for (const key of path.split(".")) {
29
+ if (current == null || typeof current !== "object") return path;
30
+ current = (current as Record<string, unknown>)[key];
31
+ }
32
+ return typeof current === "string" ? current : path;
33
+ }
34
+
35
+ function interpolate(template: string, params?: Record<string, string | number>): string {
36
+ if (!params) return template;
37
+ return template.replace(/\{(\w+)\}/g, (_, key: string) => {
38
+ const val = params[key];
39
+ return val !== undefined ? String(val) : `{${key}}`;
40
+ });
41
+ }
42
+
43
+ interface I18nContextValue {
44
+ locale: Locale;
45
+ setLocale: (locale: Locale) => void;
46
+ t: (key: TranslationKey, params?: Record<string, string | number>) => string;
47
+ }
48
+
49
+ const I18nContext = createContext<I18nContextValue | null>(null);
50
+
51
+ export function I18nProvider({ children }: { children: ReactNode }) {
52
+ const [locale, setLocaleState] = useState<Locale>(detectLocale);
53
+
54
+ const setLocale = useCallback((next: Locale) => {
55
+ setLocaleState(next);
56
+ try { localStorage.setItem(STORAGE_KEY, next); } catch {}
57
+ }, []);
58
+
59
+ const t = useCallback(
60
+ (key: TranslationKey, params?: Record<string, string | number>) => {
61
+ const raw = getNestedValue(LOCALES[locale], key);
62
+ return interpolate(raw, params);
63
+ },
64
+ [locale],
65
+ );
66
+
67
+ const value = useMemo(() => ({ locale, setLocale, t }), [locale, setLocale, t]);
68
+
69
+ return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;
70
+ }
71
+
72
+ export function useI18n(): I18nContextValue {
73
+ const ctx = useContext(I18nContext);
74
+ if (!ctx) throw new Error("useI18n must be used within I18nProvider");
75
+ return ctx;
76
+ }
@@ -0,0 +1,276 @@
1
+ export const en = {
2
+ common: {
3
+ close: "Close",
4
+ cancel: "Cancel",
5
+ save: "Save",
6
+ retry: "Retry",
7
+ tryAgain: "Try again",
8
+ back: "Back",
9
+ submit: "Submit",
10
+ download: "Download",
11
+ regenerate: "Regenerate",
12
+ generate: "Generate",
13
+ enabled: "Enabled",
14
+ disabled: "Disabled",
15
+ refresh: "Refresh",
16
+ retrying: "Retrying...",
17
+ },
18
+
19
+ time: {
20
+ justNow: "just now",
21
+ minutes: "{n}m",
22
+ minutesAgo: "{n}m ago",
23
+ hours: "{n}h",
24
+ hoursAgo: "{n}h ago",
25
+ days: "{n}d",
26
+ daysAgo: "{n}d ago",
27
+ monthsAgo: "{n}mo ago",
28
+ },
29
+
30
+ appShell: {
31
+ noAnalysesYet: "No analyses yet",
32
+ newAnalysis: "New analysis",
33
+ restarting: "Restarting...",
34
+ versionAvailable: "v{version} available",
35
+ updating: "Updating...",
36
+ updateAndRestart: "Update & restart",
37
+ chatResponding: "Chat responding...",
38
+ switchToMode: "Switch to {mode} mode",
39
+ settings: "Settings",
40
+ },
41
+
42
+ input: {
43
+ tagline: "Turn PRs into navigable stories",
44
+ enterToAnalyze: "↵ Enter to analyze",
45
+ status: "status",
46
+ recent: "Recent",
47
+ placeholder: "https://github.com/owner/repo/pull/123",
48
+ sponsorTagline: "The Power of AI for Every Business",
49
+ ad: "Ad",
50
+ },
51
+
52
+ settings: {
53
+ title: "Settings",
54
+ authentication: "Authentication",
55
+ openrouterApiKey: "OpenRouter API Key",
56
+ githubToken: "GitHub Token",
57
+ configured: "Configured",
58
+ notSet: "Not set",
59
+ notDetected: "Not detected",
60
+ change: "Change",
61
+ set: "Set",
62
+ ghCli: "gh CLI",
63
+ model: "Model",
64
+ llm: "LLM",
65
+ setApiKeyFirst: "Set API key first",
66
+ agent: "Agent",
67
+ language: "Language",
68
+ autoDetect: "Auto-detect",
69
+ limits: "Limits",
70
+ maxFiles: "Max files",
71
+ timeout: "Timeout",
72
+ seconds: "sec",
73
+ concurrency: "Concurrency",
74
+ customPrompt: "Custom Prompt",
75
+ customPromptDesc: "Additional instructions for PR analysis. Applied to all analysis stages.",
76
+ customPromptPlaceholder: "e.g. Focus on security vulnerabilities and performance issues. Ignore style changes.",
77
+ cmdEnterToSave: "Press Cmd+Enter to save, or click outside",
78
+ plugins: "Plugins",
79
+ privacy: "Privacy",
80
+ usageAnalytics: "Usage Analytics",
81
+ searchModels: "Search models...",
82
+ noModelsFound: "No models found",
83
+ uiLanguage: "UI Language",
84
+ interface: "Interface",
85
+ },
86
+
87
+ review: {
88
+ submitReview: "Submit Review",
89
+ approve: "Approve",
90
+ requestChanges: "Request changes",
91
+ comment: "Comment",
92
+ approveDesc: "Submit approval for this PR",
93
+ requestChangesDesc: "Submit feedback that must be addressed",
94
+ commentDesc: "Submit general feedback",
95
+ reviewSubmitted: "Review submitted",
96
+ viewOnGithub: "View on GitHub",
97
+ message: "Message",
98
+ optionalMessage: "Optional message...",
99
+ describeChanges: "Describe the changes needed...",
100
+ ctrlEnterToSubmit: "{key}+Enter to submit",
101
+ },
102
+
103
+ loading: {
104
+ fetching: "Fetch PR data",
105
+ parsing: "Parse diff",
106
+ cloning: "Clone repository",
107
+ checkout: "Checkout branches",
108
+ exploring: "Explore codebase",
109
+ analyzing: "Analyze files",
110
+ grouping: "Group changes",
111
+ summarizing: "Generate summary",
112
+ narrating: "Write narrative",
113
+ done: "Complete",
114
+ },
115
+
116
+ error: {
117
+ rateLimitTitle: "Rate limit reached",
118
+ rateLimitHint: "The API rate limit has been exceeded. Wait a moment before retrying.",
119
+ timeoutTitle: "Request timed out",
120
+ timeoutHint: "The analysis took too long. This can happen with very large PRs.",
121
+ networkTitle: "Connection failed",
122
+ networkHint: "Could not reach the server. Check your network connection.",
123
+ authTitle: "Authentication error",
124
+ authHint: "Your GitHub token may be expired or invalid. Run `newpr auth` to reconfigure.",
125
+ notFoundTitle: "PR not found",
126
+ notFoundHint: "The pull request could not be found. Check the URL and make sure you have access.",
127
+ apiKeyTitle: "API key error",
128
+ apiKeyHint: "Your OpenRouter API key may be missing or invalid. Set OPENROUTER_API_KEY in your environment.",
129
+ defaultTitle: "Analysis failed",
130
+ defaultHint: "Something went wrong during the analysis.",
131
+ unknownError: "An unknown error occurred",
132
+ },
133
+
134
+ chat: {
135
+ title: "Chat",
136
+ askAnything: "Ask anything about this PR",
137
+ refAndCmds: "@ to reference files · / for commands",
138
+ askAboutPr: "Ask about this PR...",
139
+ atToRef: "@ to reference · / for commands",
140
+ enterToSend: "Enter to send",
141
+ previousAnalysis: "Previous analysis",
142
+ messagesCompacted: "{count} messages compacted",
143
+ conversationCompacted: "Conversation compacted",
144
+ thinking: "Thinking…",
145
+ done: "Done",
146
+ undoLabel: "/undo",
147
+ undoDesc: "Remove last exchange",
148
+ },
149
+
150
+ results: {
151
+ open: "Open",
152
+ merged: "Merged",
153
+ closed: "Closed",
154
+ draft: "Draft",
155
+ story: "Story",
156
+ discussion: "Discussion",
157
+ groups: "Groups",
158
+ files: "Files",
159
+ stack: "Stack",
160
+ slides: "Slides",
161
+ comic: "Comic",
162
+ review: "Review",
163
+ nFiles: "{n} files",
164
+ prUpdated: "This PR has been updated since this analysis was created.",
165
+ reAnalyze: "Re-analyze",
166
+ },
167
+
168
+ detail: {
169
+ keyChanges: "Key Changes",
170
+ risk: "Risk",
171
+ dependencies: "Dependencies",
172
+ nFiles: "{n} files",
173
+ loadingDiff: "Loading diff",
174
+ },
175
+
176
+ story: {
177
+ scope: "Scope",
178
+ impact: "Impact",
179
+ walkthrough: "Walkthrough",
180
+ },
181
+
182
+ discussion: {
183
+ noSession: "No session available",
184
+ loadingDiscussion: "Loading discussion",
185
+ description: "Description",
186
+ comments: "Comments",
187
+ noContent: "No description or comments",
188
+ },
189
+
190
+ groups: {
191
+ nGroups: "{n} groups",
192
+ },
193
+
194
+ files: {
195
+ nFiles: "{n} files",
196
+ tree: "Tree",
197
+ groups: "Groups",
198
+ changes: "Changes",
199
+ ungrouped: "Ungrouped",
200
+ },
201
+
202
+ cartoon: {
203
+ title: "Comic Strip",
204
+ description: "Generate a 4-panel comic strip that visualizes the key changes in this PR. Powered by Gemini.",
205
+ generating: "Generating comic...",
206
+ takesTime: "This may take 10-30 seconds",
207
+ failed: "Generation failed",
208
+ altText: "PR 4-panel comic",
209
+ },
210
+
211
+ slides: {
212
+ title: "Slide Deck",
213
+ description: "Generate a presentation that explains this PR to your team. The number of slides is automatically determined based on PR complexity.",
214
+ generateSlides: "Generate Slides",
215
+ failed: "Generation failed",
216
+ starting: "Starting...",
217
+ resuming: "Resuming failed slides...",
218
+ takesMinutes: "This may take a few minutes",
219
+ slidesFailed: "{n} slide(s) failed to generate",
220
+ retryFailed: "Retry failed",
221
+ slidesReady: "Slides ready",
222
+ nSlidesGenerated: "{n} slides generated",
223
+ },
224
+
225
+ stack: {
226
+ title: "PR Stacking",
227
+ subtitle: "Split into focused, reviewable PRs",
228
+ description: "Automatically split this PR into a stack of smaller draft PRs based on the analysis groups. Each group becomes its own PR with proper dependency ordering.",
229
+ maxPrs: "Max PRs",
230
+ startStacking: "Start Stacking",
231
+ partition: "Partition",
232
+ partitionDesc: "Assigning files to groups",
233
+ plan: "Plan",
234
+ planDesc: "Building stack plan",
235
+ execute: "Execute",
236
+ executeDesc: "Creating commits",
237
+ publish: "Publish",
238
+ publishDesc: "Pushing branches and creating draft PRs",
239
+ ready: "Ready",
240
+ published: "Published",
241
+ publishingInfo: "Uploading stack branches and opening draft PRs. Please wait until publish results appear below.",
242
+ publishAsDraft: "Publish as Draft PRs",
243
+ nPrs: "{n} PRs",
244
+ nFilesTotal: "{n} files total",
245
+ descriptionPreview: "Description Preview",
246
+ preparingPreview: "Preparing preview bodies...",
247
+ draftPublishResults: "Draft Publish Results",
248
+ nOfNCreated: "{created}/{total} created",
249
+ closeAll: "Close all",
250
+ closeDeleteBranches: "Close + delete branches",
251
+ confirmCloseAll: "Close all stacked draft PRs?",
252
+ confirmCloseDelete: "Close all stacked draft PRs and delete stack branches?",
253
+ noDraftPrUrls: "No draft PR URLs were returned.",
254
+ branchesPushedNotCreated: "Some branches were pushed but PR creation did not complete.",
255
+ envVars: "Environment Variables",
256
+ envVarsDesc: "Set env vars for quality gate scripts (e.g. NPM_TOKEN, CI tokens)",
257
+ addVariable: "Add variable",
258
+ treeEquivalenceVerified: "Tree equivalence verified",
259
+ verificationFailed: "Verification failed: {errors}",
260
+ qualityGate: "Quality gate",
261
+ qualityGateSkipped: "Quality gate skipped: {reason}",
262
+ qualityGateAllPassed: "all {n} groups passed",
263
+ qualityGatePartial: "{passed}/{total} groups passed",
264
+ groupsWithWarnings: "{n} group(s) with warnings (non-blocking)",
265
+ },
266
+
267
+ feasibility: {
268
+ feasible: "Feasible",
269
+ notFeasible: "Not feasible — dependency cycle",
270
+ unassignedFiles: "{n} unassigned file(s)",
271
+ },
272
+ } as const;
273
+
274
+ type DeepStringify<T> = { [K in keyof T]: T[K] extends string ? string : DeepStringify<T[K]> };
275
+ export type Translations = DeepStringify<typeof en>;
276
+ export type TranslationKeys = typeof en;
@@ -0,0 +1,3 @@
1
+ export { I18nProvider, useI18n } from "./context.tsx";
2
+ export type { Locale, TranslationKey } from "./context.tsx";
3
+ export type { Translations, TranslationKeys } from "./en.ts";