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.
- package/package.json +1 -1
- package/src/analyzer/pipeline.ts +1 -0
- package/src/config/index.ts +1 -0
- package/src/config/store.ts +1 -0
- package/src/llm/prompts.ts +10 -4
- package/src/types/config.ts +1 -0
- package/src/web/client/components/AppShell.tsx +29 -22
- package/src/web/client/components/ChatSection.tsx +18 -12
- package/src/web/client/components/DetailPane.tsx +10 -6
- package/src/web/client/components/ErrorScreen.tsx +15 -41
- package/src/web/client/components/FeasibilityAlert.tsx +5 -3
- package/src/web/client/components/InputScreen.tsx +21 -17
- package/src/web/client/components/LoadingTimeline.tsx +22 -17
- package/src/web/client/components/ResultsScreen.tsx +31 -20
- package/src/web/client/components/ReviewModal.tsx +23 -15
- package/src/web/client/components/SettingsPanel.tsx +100 -25
- package/src/web/client/lib/i18n/context.tsx +76 -0
- package/src/web/client/lib/i18n/en.ts +276 -0
- package/src/web/client/lib/i18n/index.ts +3 -0
- package/src/web/client/lib/i18n/ko.ts +274 -0
- package/src/web/client/main.tsx +4 -1
- package/src/web/client/panels/CartoonPanel.tsx +12 -10
- package/src/web/client/panels/DiscussionPanel.tsx +14 -12
- package/src/web/client/panels/FilesPanel.tsx +14 -9
- package/src/web/client/panels/GroupsPanel.tsx +3 -1
- package/src/web/client/panels/SlidesPanel.tsx +17 -15
- package/src/web/client/panels/StackPanel.tsx +50 -44
- package/src/web/client/panels/StoryPanel.tsx +5 -3
- package/src/web/server/routes.ts +27 -21
- 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">
|
|
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="
|
|
119
|
-
<Row label="
|
|
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
|
-
|
|
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 ? "
|
|
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 ? "
|
|
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="
|
|
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 ? "
|
|
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="
|
|
181
|
-
<Row label="
|
|
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">
|
|
192
|
+
<span className="text-[11px] text-muted-foreground/40">{t("settings.setApiKeyFirst")}</span>
|
|
190
193
|
)}
|
|
191
194
|
</Row>
|
|
192
|
-
<Row label="
|
|
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="
|
|
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" ? "
|
|
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="
|
|
224
|
-
<Row label="
|
|
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="
|
|
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">
|
|
251
|
+
<span className="text-[10px] text-muted-foreground/30">{t("settings.seconds")}</span>
|
|
231
252
|
</div>
|
|
232
253
|
</Row>
|
|
233
|
-
<Row label="
|
|
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
|
-
|
|
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="
|
|
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="
|
|
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 ? "
|
|
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="
|
|
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">
|
|
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;
|