ltcai 4.7.0 → 5.0.0

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.
@@ -6,6 +6,8 @@ import { Badge } from "@/components/ui/badge";
6
6
  import { Button } from "@/components/ui/button";
7
7
  import { Input } from "@/components/ui/input";
8
8
  import { cn, asArray } from "@/lib/utils";
9
+ import { LANGUAGE_LABELS, t, type Language } from "@/i18n";
10
+ import { useAppStore } from "@/store/appStore";
9
11
 
10
12
  const FLOW_COMPLETE_KEY = "lattice.productFlow.complete";
11
13
  const FLOW_USER_KEY = "lattice.productFlow.user";
@@ -43,7 +45,16 @@ export function readProductFlowComplete() {
43
45
  return false;
44
46
  }
45
47
 
48
+ function readSavedFlowUser(): { email?: string; name?: string } | null {
49
+ try {
50
+ const raw = localStorage.getItem(FLOW_USER_KEY);
51
+ return raw ? JSON.parse(raw) : null;
52
+ } catch {}
53
+ return null;
54
+ }
55
+
46
56
  export function ProductFlow({ onComplete }: { onComplete: () => void }) {
57
+ const language = useAppStore((state) => state.language);
47
58
  const [step, setStep] = React.useState<FlowStep>("login");
48
59
  const [analysis, setAnalysis] = React.useState<FlowAnalysis | null>(null);
49
60
  const [analysisError, setAnalysisError] = React.useState<string | null>(null);
@@ -70,16 +81,17 @@ export function ProductFlow({ onComplete }: { onComplete: () => void }) {
70
81
  sysinfo: sysinfo.ok ? sysinfo.data as ApiData : null,
71
82
  });
72
83
  if (!setup.ok && !recommendationsResult.ok && !models.ok) {
73
- setAnalysisError("Lattice could not finish reading this computer. You can still continue with a safe default.");
84
+ setAnalysisError(t(language, "flow.analysis.error"));
74
85
  }
75
86
  }
76
87
  void runAnalysis();
77
88
  return () => { cancelled = true; };
78
- }, [analysis, step]);
89
+ }, [analysis, language, step]);
79
90
 
80
91
  return (
81
- <div className="ritual-shell" aria-label="Awaken your Brain">
92
+ <div className="ritual-shell" aria-label={t(language, "flow.shell")}>
82
93
  <div className="ritual-container">
94
+ <LanguageChooser />
83
95
  {/* The living presence participates in the ritual at every step */}
84
96
  <div className="ritual-brain">
85
97
  <LivingBrain
@@ -133,80 +145,116 @@ export function ProductFlow({ onComplete }: { onComplete: () => void }) {
133
145
  );
134
146
  }
135
147
 
148
+ function LanguageChooser() {
149
+ const language = useAppStore((state) => state.language);
150
+ const setLanguage = useAppStore((state) => state.setLanguage);
151
+
152
+ return (
153
+ <div className="language-switcher ritual-language" aria-label={t(language, "language.label")}>
154
+ {(["ko", "en"] as Language[]).map((item) => (
155
+ <button
156
+ key={item}
157
+ type="button"
158
+ className={language === item ? "is-active" : ""}
159
+ onClick={() => setLanguage(item)}
160
+ aria-pressed={language === item}
161
+ >
162
+ {LANGUAGE_LABELS[item]}
163
+ </button>
164
+ ))}
165
+ </div>
166
+ );
167
+ }
168
+
136
169
  function LoginScreen({ onSuccess }: { onSuccess: () => void }) {
170
+ const language = useAppStore((state) => state.language);
137
171
  const [email, setEmail] = React.useState(() => {
138
- try {
139
- const saved = localStorage.getItem(FLOW_USER_KEY);
140
- return saved ? JSON.parse(saved).email || "you@local" : "you@local";
141
- } catch {
142
- return "you@local";
143
- }
172
+ return readSavedFlowUser()?.email || "you@local";
144
173
  });
145
174
  const [password, setPassword] = React.useState("");
146
- const [name, setName] = React.useState("You");
175
+ const [name, setName] = React.useState(() => readSavedFlowUser()?.name || "You");
147
176
  const [busy, setBusy] = React.useState(false);
148
177
  const [error, setError] = React.useState<string | null>(null);
149
178
 
150
179
  async function submit(event: React.FormEvent) {
151
180
  event.preventDefault();
181
+ const cleanEmail = email.trim();
182
+ const cleanPassword = password.trim();
183
+ const cleanName = name.trim() || cleanEmail.split("@")[0] || "You";
184
+ if (!cleanEmail || !cleanPassword) {
185
+ setError(t(language, "flow.login.missing"));
186
+ return;
187
+ }
152
188
  setBusy(true);
153
189
  setError(null);
154
- const safePassword = password || "Lattice123";
155
- let result = await latticeApi.login(email, safePassword);
156
- if (!result.ok) {
157
- const registered = await latticeApi.register({
158
- email,
159
- password: safePassword,
160
- name: name || email.split("@")[0] || "You",
161
- nickname: name || "You",
162
- });
163
- if (registered.ok) result = await latticeApi.login(email, safePassword);
164
- }
190
+ const savedUser = readSavedFlowUser();
191
+ let result = await latticeApi.login(cleanEmail, cleanPassword);
165
192
  if (!result.ok) {
166
193
  const profile = await latticeApi.profile();
167
- if (!profile.ok) {
194
+ if (profile.ok && (!savedUser?.email || savedUser.email === cleanEmail)) {
195
+ try { localStorage.setItem(FLOW_USER_KEY, JSON.stringify({ email: cleanEmail, name: cleanName })); } catch {}
196
+ setBusy(false);
197
+ onSuccess();
198
+ return;
199
+ }
200
+ if (savedUser?.email && savedUser.email !== cleanEmail) {
168
201
  setBusy(false);
169
- setError("We could not open your local profile. Check your password or create the first local account.");
202
+ setError(t(language, "flow.login.otherEmail"));
170
203
  return;
171
204
  }
205
+ if (savedUser?.email === cleanEmail) {
206
+ setBusy(false);
207
+ setError(t(language, "flow.login.wrongPassword"));
208
+ return;
209
+ }
210
+ const registered = await latticeApi.register({
211
+ email: cleanEmail,
212
+ password: cleanPassword,
213
+ name: cleanName,
214
+ nickname: cleanName,
215
+ });
216
+ if (registered.ok) result = await latticeApi.login(cleanEmail, cleanPassword);
217
+ }
218
+ if (!result.ok) {
219
+ setBusy(false);
220
+ setError(t(language, "flow.login.unavailable"));
221
+ return;
172
222
  }
173
- try { localStorage.setItem(FLOW_USER_KEY, JSON.stringify({ email, name })); } catch {}
223
+ try { localStorage.setItem(FLOW_USER_KEY, JSON.stringify({ email: cleanEmail, name: cleanName })); } catch {}
174
224
  setBusy(false);
175
225
  onSuccess();
176
226
  }
177
227
 
178
228
  return (
179
229
  <div>
180
- <div className="ritual-title">Welcome to your mind.</div>
181
- <div className="ritual-subtitle">
182
- Models will change. Your knowledge should not. Lattice keeps your documents, conversations, decisions, and context together as a private Brain you own.
183
- </div>
230
+ <div className="ritual-title">{t(language, "flow.login.title")}</div>
231
+ <div className="ritual-subtitle">{t(language, "flow.login.body")}</div>
184
232
 
185
233
  <ProductPromise />
186
234
 
187
235
  <form onSubmit={submit} className="ritual-card" style={{ maxWidth: 420, margin: "0 auto" }}>
188
236
  <div style={{ display: "grid", gap: "0.85rem" }}>
189
237
  <div>
190
- <div style={{ fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "1px", color: "hsl(var(--fg-muted))", marginBottom: 4 }}>Your name</div>
238
+ <div style={{ fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "1px", color: "hsl(var(--fg-muted))", marginBottom: 4 }}>{t(language, "flow.name")}</div>
191
239
  <Input value={name} onChange={(e) => setName(e.target.value)} placeholder="You" />
192
240
  </div>
193
241
  <div>
194
- <div style={{ fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "1px", color: "hsl(var(--fg-muted))", marginBottom: 4 }}>Email (local only)</div>
242
+ <div style={{ fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "1px", color: "hsl(var(--fg-muted))", marginBottom: 4 }}>{t(language, "flow.email")}</div>
195
243
  <Input value={email} onChange={(e) => setEmail(e.target.value)} type="email" placeholder="you@local" />
196
244
  </div>
197
245
  <div>
198
- <div style={{ fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "1px", color: "hsl(var(--fg-muted))", marginBottom: 4 }}>Password</div>
199
- <Input value={password} onChange={(e) => setPassword(e.target.value)} type="password" placeholder="Create a strong local password" />
246
+ <div style={{ fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "1px", color: "hsl(var(--fg-muted))", marginBottom: 4 }}>{t(language, "flow.password")}</div>
247
+ <Input value={password} onChange={(e) => setPassword(e.target.value)} type="password" placeholder={t(language, "flow.password.placeholder")} />
200
248
  </div>
201
249
  </div>
202
250
 
203
251
  {error && <div style={{ marginTop: "0.85rem", padding: "0.6rem 0.85rem", background: "hsl(var(--destructive)/0.12)", border: "1px solid hsl(var(--destructive)/0.4)", borderRadius: 10, fontSize: "0.9rem" }}>{error}</div>}
204
252
 
205
- <Button type="submit" disabled={busy || !email.trim()} style={{ width: "100%", marginTop: "1rem" }}>
206
- {busy ? "Opening the Brain..." : "Open my Brain"}
253
+ <Button type="submit" disabled={busy || !email.trim() || !password.trim()} style={{ width: "100%", marginTop: "1rem" }}>
254
+ {busy ? t(language, "flow.login.busy") : t(language, "flow.login.submit")}
207
255
  </Button>
208
256
  <div style={{ fontSize: "0.75rem", color: "hsl(var(--fg-muted))", marginTop: "0.6rem" }}>
209
- Local profile first. Model choice comes later.
257
+ {t(language, "flow.login.note")}
210
258
  </div>
211
259
  </form>
212
260
  </div>
@@ -214,19 +262,20 @@ function LoginScreen({ onSuccess }: { onSuccess: () => void }) {
214
262
  }
215
263
 
216
264
  function ProductPromise() {
265
+ const language = useAppStore((state) => state.language);
217
266
  return (
218
267
  <div className="ritual-promise" aria-label="Lattice AI product promise">
219
268
  <div>
220
- <span>Durable knowledge</span>
221
- <strong>Your work becomes long-lived context.</strong>
269
+ <span>{t(language, "flow.promise.memory.k")}</span>
270
+ <strong>{t(language, "flow.promise.memory.v")}</strong>
222
271
  </div>
223
272
  <div>
224
- <span>Replaceable models</span>
225
- <strong>The model is a voice, not the asset.</strong>
273
+ <span>{t(language, "flow.promise.model.k")}</span>
274
+ <strong>{t(language, "flow.promise.model.v")}</strong>
226
275
  </div>
227
276
  <div>
228
- <span>User ownership</span>
229
- <strong>Back up, restore, and move your Brain.</strong>
277
+ <span>{t(language, "flow.promise.ownership.k")}</span>
278
+ <strong>{t(language, "flow.promise.ownership.v")}</strong>
230
279
  </div>
231
280
  </div>
232
281
  );
@@ -241,13 +290,12 @@ function AnalysisScreen({
241
290
  error: string | null;
242
291
  onContinue: () => void;
243
292
  }) {
293
+ const language = useAppStore((state) => state.language);
244
294
  const detected = buildDetectedFacts(analysis);
245
295
  return (
246
296
  <div>
247
- <div className="ritual-title">Understanding your home.</div>
248
- <div className="ritual-subtitle">
249
- Lattice is checking what this computer can support so your Brain can run locally instead of turning your memories into a cloud dependency.
250
- </div>
297
+ <div className="ritual-title">{t(language, "flow.analysis.title")}</div>
298
+ <div className="ritual-subtitle">{t(language, "flow.analysis.body")}</div>
251
299
 
252
300
  <div className="ritual-fact-grid">
253
301
  {detected.map((item, idx) => (
@@ -263,9 +311,9 @@ function AnalysisScreen({
263
311
  <div style={{ display: "flex", alignItems: "center", gap: "0.6rem" }}>
264
312
  <Sparkles style={{ color: "hsl(var(--brain-core))" }} />
265
313
  <div>
266
- <div style={{ fontWeight: 620 }}>{analysis ? recommendedSummary(analysis) : "Preparing the best fit..."}</div>
314
+ <div style={{ fontWeight: 620 }}>{analysis ? recommendedSummary(analysis, language) : t(language, "flow.analysis.finding")}</div>
267
315
  <div style={{ fontSize: "0.9rem", color: "hsl(var(--fg-muted))" }}>
268
- {analysis ? "A short, personal list of minds is ready for you to choose from." : "Reading your machine. This is gentle."}
316
+ {analysis ? t(language, "flow.analysis.ready") : t(language, "flow.analysis.wait")}
269
317
  </div>
270
318
  </div>
271
319
  </div>
@@ -275,7 +323,7 @@ function AnalysisScreen({
275
323
 
276
324
  <div style={{ marginTop: "1.25rem" }}>
277
325
  <Button onClick={onContinue} disabled={!analysis && !error} style={{ minWidth: 260 }}>
278
- See how your Brain can think
326
+ {t(language, "flow.analysis.continue")}
279
327
  </Button>
280
328
  </div>
281
329
  </div>
@@ -291,15 +339,19 @@ function RecommendationScreen({
291
339
  onBack: () => void;
292
340
  onSelect: (model: RecommendedModel) => void;
293
341
  }) {
342
+ const language = useAppStore((state) => state.language);
294
343
  const items = recommendations.length ? recommendations : [fallbackModel()];
295
344
  return (
296
345
  <div>
297
- <div className="ritual-title">How shall your mind think today?</div>
298
- <div className="ritual-subtitle">
299
- The model is the current voice of your Brain. You can replace it later; your knowledge stays.
300
- </div>
346
+ <div className="ritual-title">{t(language, "flow.recommend.title")}</div>
347
+ <div className="ritual-subtitle">{t(language, "flow.recommend.body")}</div>
301
348
 
302
349
  <div style={{ maxWidth: 560, margin: "0 auto" }}>
350
+ {items[0]?.supported ? (
351
+ <Button onClick={() => onSelect(items[0])} style={{ width: "100%", marginBottom: "0.85rem" }}>
352
+ {t(language, "flow.recommend.primary")}
353
+ </Button>
354
+ ) : null}
303
355
  {items.slice(0, 3).map((model, index) => (
304
356
  <button
305
357
  key={`${model.role}-${model.id}`}
@@ -308,17 +360,17 @@ function RecommendationScreen({
308
360
  disabled={!model.supported}
309
361
  style={{ width: "100%" }}
310
362
  >
311
- <div className="role">{rankLabel(model.role, index)}</div>
363
+ <div className="role">{rankLabel(model.role, index, language)}</div>
312
364
  <div className="name">{model.shortName}</div>
313
365
  <div className="reason">{model.reason} · {model.size || "ready"}</div>
314
- {!model.supported && <div style={{ color: "hsl(var(--destructive))", marginTop: 6, fontSize: "0.85rem" }}>Needs attention on this machine</div>}
366
+ {!model.supported && <div style={{ color: "hsl(var(--destructive))", marginTop: 6, fontSize: "0.85rem" }}>{t(language, "flow.recommend.unsupported")}</div>}
315
367
  </button>
316
368
  ))}
317
369
  </div>
318
370
 
319
371
  <div style={{ marginTop: "1.1rem", display: "flex", justifyContent: "center", gap: "1rem", alignItems: "center" }}>
320
- <Button variant="ghost" onClick={onBack}>Back</Button>
321
- <div style={{ fontSize: "0.82rem", color: "hsl(var(--fg-muted))" }}>Your choice becomes the current voice of your Brain.</div>
372
+ <Button variant="ghost" onClick={onBack}>{t(language, "flow.recommend.back")}</Button>
373
+ <div style={{ fontSize: "0.82rem", color: "hsl(var(--fg-muted))" }}>{t(language, "flow.recommend.hint")}</div>
322
374
  </div>
323
375
  </div>
324
376
  );
@@ -333,10 +385,11 @@ function InstallScreen({
333
385
  onBack: () => void;
334
386
  onComplete: () => void;
335
387
  }) {
388
+ const language = useAppStore((state) => state.language);
336
389
  const [busy, setBusy] = React.useState(false);
337
390
  const [stage, setStage] = React.useState<InstallStage>("idle");
338
391
  const [percent, setPercent] = React.useState(0);
339
- const [message, setMessage] = React.useState("Your Brain is waiting for this mind.");
392
+ const [message, setMessage] = React.useState(t(language, "flow.install.wait"));
340
393
  const [error, setError] = React.useState<string | null>(null);
341
394
 
342
395
  async function start() {
@@ -344,7 +397,7 @@ function InstallScreen({
344
397
  setError(null);
345
398
  setStage("install");
346
399
  setPercent(8);
347
- setMessage("Preparing the Brain.");
400
+ setMessage(t(language, "flow.install.prepare"));
348
401
  const result = await latticeApi.streamModelPrepare(
349
402
  { model: model.loadId, engine: model.engine || "local_mlx", allow_download: true },
350
403
  {
@@ -352,12 +405,12 @@ function InstallScreen({
352
405
  const nextStage = friendlyInstallStage(String(event.stage || ""));
353
406
  setStage(nextStage);
354
407
  setPercent(Number(event.percent || percentForStage(nextStage)));
355
- setMessage(friendlyInstallMessage(event, nextStage));
408
+ setMessage(friendlyInstallMessage(event, nextStage, language));
356
409
  },
357
410
  onDone: () => {
358
411
  setStage("done");
359
412
  setPercent(100);
360
- setMessage("Your Brain is ready.");
413
+ setMessage(t(language, "flow.install.done"));
361
414
  },
362
415
  onError: (event) => {
363
416
  setStage("error");
@@ -369,7 +422,7 @@ function InstallScreen({
369
422
  if (result.ok) {
370
423
  setStage("done");
371
424
  setPercent(100);
372
- setMessage("Your Brain is ready.");
425
+ setMessage(t(language, "flow.install.done"));
373
426
  window.setTimeout(onComplete, 700);
374
427
  } else {
375
428
  setStage("error");
@@ -385,10 +438,10 @@ function InstallScreen({
385
438
 
386
439
  return (
387
440
  <div>
388
- <div className="ritual-title">Bring this mind home.</div>
441
+ <div className="ritual-title">{t(language, "flow.install.title")}</div>
389
442
  <div className="ritual-subtitle">
390
443
  <strong>{model.shortName}</strong> — {model.reason}.<br />
391
- This gives your Brain a local voice. Download, validation, and loading happen only with your consent.
444
+ {t(language, "flow.install.body")}
392
445
  </div>
393
446
 
394
447
  {/* Living Brain reacts to the ceremony of installation */}
@@ -405,7 +458,7 @@ function InstallScreen({
405
458
  {(["install", "download", "validate", "load"] as const).map((item) => (
406
459
  <div key={item} className={`ritual-stage ${installStepState(stage, item)}`}>
407
460
  <CheckCircle2 style={{ width: 15, height: 15 }} />
408
- <span>{installLabel(item)}</span>
461
+ <span>{installLabel(item, language)}</span>
409
462
  </div>
410
463
  ))}
411
464
  </div>
@@ -416,31 +469,34 @@ function InstallScreen({
416
469
  </div>
417
470
 
418
471
  <div className="ritual-status">{message}</div>
472
+ <div className="ritual-card" style={{ margin: "0.8rem auto 0", maxWidth: 540, fontSize: "0.86rem", color: "hsl(var(--fg-muted))" }}>
473
+ {t(language, "flow.install.note")}
474
+ </div>
419
475
 
420
476
  {error && (
421
477
  <div className="ritual-card" style={{ borderColor: "hsl(var(--destructive)/0.45)", background: "hsl(var(--destructive)/0.07)", marginBottom: "1rem" }}>
422
478
  {error}
423
- <div style={{ marginTop: "0.5rem", fontSize: "0.85rem" }}>You can go back and choose a different mind, or try again.</div>
479
+ <div style={{ marginTop: "0.5rem", fontSize: "0.85rem" }}>{t(language, "flow.install.retry")}</div>
424
480
  </div>
425
481
  )}
426
482
 
427
483
  <div style={{ display: "flex", gap: "0.75rem", justifyContent: "center", marginTop: "1rem" }}>
428
- <Button variant="ghost" onClick={onBack} disabled={busy}>Choose differently</Button>
484
+ <Button variant="ghost" onClick={onBack} disabled={busy}>{t(language, "flow.install.back")}</Button>
429
485
 
430
486
  {stage !== "done" ? (
431
487
  <Button
432
488
  onClick={start}
433
489
  disabled={busy || !model.supported}
434
490
  >
435
- {busy ? "Waking the mind..." : "Yes — make this my Brain"}
491
+ {busy ? t(language, "flow.install.busy") : t(language, "flow.install.start")}
436
492
  </Button>
437
493
  ) : (
438
- <Button onClick={onComplete}>Enter your Brain</Button>
494
+ <Button onClick={onComplete}>{t(language, "flow.install.enter")}</Button>
439
495
  )}
440
496
  </div>
441
497
 
442
498
  <div style={{ fontSize: "0.72rem", color: "hsl(var(--fg-muted))", marginTop: "0.9rem" }}>
443
- Explicit consent only. All work happens locally on your machine.
499
+ {t(language, "flow.install.local")}
444
500
  </div>
445
501
  </div>
446
502
  );
@@ -572,18 +628,21 @@ function fallbackModel(): RecommendedModel {
572
628
  };
573
629
  }
574
630
 
575
- function rankLabel(role: RecommendedModel["role"], index: number) {
576
- if (role === "best") return "Best Experience";
577
- if (role === "faster") return "Faster";
578
- if (role === "advanced") return "Advanced";
631
+ function rankLabel(role: RecommendedModel["role"], index: number, language: Language) {
632
+ if (role === "best") return language === "ko" ? "추천" : "Best Experience";
633
+ if (role === "faster") return language === "ko" ? "빠른 선택" : "Faster";
634
+ if (role === "advanced") return language === "ko" ? "고급 선택" : "Advanced";
579
635
  return `Choice ${index + 1}`;
580
636
  }
581
637
 
582
- function recommendedSummary(analysis: FlowAnalysis) {
638
+ function recommendedSummary(analysis: FlowAnalysis, language: Language) {
583
639
  const recs = asRecord(analysis.recommendations?.recommendations);
584
640
  const topPick = asRecord(recs.top_pick);
585
- if (topPick.name || topPick.id) return `${friendlyModelName(String(topPick.name || topPick.id))} looks like the best fit.`;
586
- return "A private local Brain is recommended for this computer.";
641
+ if (topPick.name || topPick.id) {
642
+ const model = friendlyModelName(String(topPick.name || topPick.id));
643
+ return language === "ko" ? `${model}이 이 컴퓨터에 가장 잘 맞습니다.` : `${model} looks like the best fit.`;
644
+ }
645
+ return language === "ko" ? "이 컴퓨터에는 개인 로컬 Brain을 추천합니다." : "A private local Brain is recommended for this computer.";
587
646
  }
588
647
 
589
648
  function friendlyModelName(value: string) {
@@ -625,20 +684,26 @@ function percentForStage(stage: InstallStage) {
625
684
  return 8;
626
685
  }
627
686
 
628
- function friendlyInstallMessage(event: ApiData, stage: InstallStage) {
687
+ function friendlyInstallMessage(event: ApiData, stage: InstallStage, language: Language) {
629
688
  const fallback = {
630
- install: "Preparing the Brain.",
631
- download: "Getting the model files.",
632
- validate: "Checking that the Brain can answer.",
633
- load: "Loading the Brain.",
634
- done: "Your Brain is ready.",
635
- idle: "Ready when you are.",
636
- error: "Something needs attention.",
689
+ install: t(language, "flow.install.prepare"),
690
+ download: language === "ko" ? "모델 파일을 받는 중입니다." : "Getting the model files.",
691
+ validate: language === "ko" ? "Brain이 응답할 수 있는지 확인 중입니다." : "Checking that the Brain can answer.",
692
+ load: language === "ko" ? "Brain을 불러오는 중입니다." : "Loading the Brain.",
693
+ done: t(language, "flow.install.done"),
694
+ idle: t(language, "flow.install.wait"),
695
+ error: language === "ko" ? "확인이 필요한 일이 있습니다." : "Something needs attention.",
637
696
  }[stage];
638
697
  return cleanConsumerText(String(event.user_message || event.message || fallback));
639
698
  }
640
699
 
641
- function installLabel(stage: "install" | "download" | "validate" | "load") {
700
+ function installLabel(stage: "install" | "download" | "validate" | "load", language: Language) {
701
+ if (language === "ko") {
702
+ if (stage === "install") return "준비";
703
+ if (stage === "download") return "다운로드";
704
+ if (stage === "validate") return "확인";
705
+ return "로드";
706
+ }
642
707
  if (stage === "install") return "Install";
643
708
  if (stage === "download") return "Download";
644
709
  if (stage === "validate") return "Validate";