From 5b6b70a679fc4de4942cf20c4b450280429ebd7f Mon Sep 17 00:00:00 2001 From: gracefully91 Date: Fri, 24 Apr 2026 13:08:45 +0900 Subject: [PATCH 1/4] feat: add korean localization toggle --- src/app/globals.css | 27 ++ src/app/layout.tsx | 15 +- src/components/AgentModelsWidget.tsx | 13 +- src/components/AgentTerminalsGrid.tsx | 15 +- src/components/BatchProgressPanel.tsx | 22 +- src/components/ControlBar.tsx | 27 +- src/components/DiscordBridgeWidget.tsx | 8 +- src/components/GitHubPanel.tsx | 40 ++- src/components/HomeDashboard.tsx | 43 +-- src/components/HomeEmptyState.tsx | 30 +- src/components/HowToWorkModal.tsx | 89 ++++-- src/components/LocaleProvider.tsx | 50 ++++ src/components/LoopGuardWidget.tsx | 8 +- src/components/OperatorFeaturesPanel.tsx | 8 +- src/components/ProjectDashboard.tsx | 20 +- src/components/ProjectHistoryWidget.tsx | 8 +- src/components/ScheduledTriggerWidget.tsx | 16 +- src/components/SettingsPage.tsx | 323 +++++++++++++++++----- src/components/SetupWizard.tsx | 172 +++++++----- src/components/TelegramBridgeWidget.tsx | 8 +- src/lib/locale.ts | 19 ++ 21 files changed, 697 insertions(+), 264 deletions(-) create mode 100644 src/components/LocaleProvider.tsx create mode 100644 src/lib/locale.ts diff --git a/src/app/globals.css b/src/app/globals.css index 7f4a519..4d68ec1 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -89,3 +89,30 @@ h1, h2, h3, h4, h5, h6 { .animate-name-shimmer { animation: qw-name-shimmer 1.6s ease-in-out infinite; } + +/* Korean help text should wrap by word, not by individual syllable. + Keep long URLs/tokens from overflowing narrow tooltips and modals. */ +.ko-help { + word-break: keep-all; + overflow-wrap: normal; + line-break: strict; + text-wrap: pretty; +} + +.ko-help p, +.ko-help li, +.ko-help div, +.ko-help span, +.ko-help b, +.ko-help strong { + word-break: inherit; + overflow-wrap: inherit; + line-break: inherit; +} + +.ko-help code, +.ko-help pre, +.ko-help .break-anywhere { + word-break: normal; + overflow-wrap: anywhere; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 9e2d391..a071db1 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,6 +4,7 @@ import "./globals.css"; import Sidebar from "@/components/Sidebar"; import TopHeader from "@/components/TopHeader"; import GlobalNotificationListener from "@/components/GlobalNotificationListener"; +import { LocaleProvider } from "@/components/LocaleProvider"; const geistMono = Geist_Mono({ variable: "--font-geist-mono", @@ -23,12 +24,14 @@ export default function RootLayout({ return ( - - -
- -
{children}
-
+ + + +
+ +
{children}
+
+
); diff --git a/src/components/AgentModelsWidget.tsx b/src/components/AgentModelsWidget.tsx index 4ebd483..18336e8 100644 --- a/src/components/AgentModelsWidget.tsx +++ b/src/components/AgentModelsWidget.tsx @@ -21,6 +21,7 @@ import { useCallback, useEffect, useState } from "react"; import InfoTooltip from "./InfoTooltip"; +import { useLocale } from "@/components/LocaleProvider"; interface AgentRow { agent_id: string; @@ -68,6 +69,7 @@ function optionsForBackend(backend: string) { // state. Mounted only when the modal is open so we don't run the // fetch until the operator actually wants to configure something. function AgentModelsModal({ projectId, onClose }: { projectId: string; onClose: () => void }) { + const { locale } = useLocale(); const [rows, setRows] = useState(null); const [error, setError] = useState(null); const [busy, setBusy] = useState(null); @@ -172,7 +174,7 @@ function AgentModelsModal({ projectId, onClose }: { projectId: string; onClose:
-

Agent Models

+

{locale === "ko" ? "에이전트 모델" : "Agent Models"}

{error && err: {error}}
@@ -257,6 +259,7 @@ function AgentModelsModal({ projectId, onClose }: { projectId: string; onClose: // component init; we don't poll, since backends rarely change // outside the modal flow. export default function AgentModelsButton({ projectId }: AgentModelsWidgetProps) { + const { locale } = useLocale(); const [open, setOpen] = useState(false); const [summary, setSummary] = useState<{ id: string; backend: string }[] | null>(null); @@ -278,9 +281,11 @@ export default function AgentModelsButton({ projectId }: AgentModelsWidgetProps)
- Agent Models + {locale === "ko" ? "에이전트 모델" : "Agent Models"} - Agent Models — configure which LLM model and reasoning effort each agent uses. Changes require an agent restart to take effect. + {locale === "ko" + ? <>에이전트 모델 - 각 에이전트가 어떤 LLM 모델과 추론 수준을 사용할지 설정합니다. 변경 사항은 에이전트를 재시작해야 적용됩니다. + : <>Agent Models — configure which LLM model and reasoning effort each agent uses. Changes require an agent restart to take effect.}
{summary && summary.length > 0 && ( diff --git a/src/components/AgentTerminalsGrid.tsx b/src/components/AgentTerminalsGrid.tsx index 06965bf..62f71b1 100644 --- a/src/components/AgentTerminalsGrid.tsx +++ b/src/components/AgentTerminalsGrid.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import TerminalGrid from "./TerminalGrid"; +import { useLocale } from "@/components/LocaleProvider"; // #208: the top-right quadrant must show all four agents // (Head, RE1, RE2, Dev) as a 2x2 grid. TerminalGrid's @@ -41,13 +42,14 @@ interface AgentTerminalsGridProps { * the terminals and their messages are lost to the other agents. */ export default function AgentTerminalsGrid({ projectId, agentStates, onStatusChange }: AgentTerminalsGridProps) { + const { locale } = useLocale(); const [tipOpen, setTipOpen] = useState(false); return (
- Agent Terminals + {locale === "ko" ? "에이전트 터미널" : "Agent Terminals"}
{tipOpen && (
- These show what each agent is doing in their CLI session.{" "} - Do not type here directly — use the AgentChattr chat - above instead. Agents won't see messages typed in their - terminals. + {locale === "ko" + ? <>각 에이전트가 CLI 세션에서 무엇을 하고 있는지 보여주는 읽기 전용 터미널입니다. 여기에 직접 입력하지 마세요. 위의 AgentChattr 채팅을 사용해야 에이전트가 메시지를 볼 수 있습니다. + : <>These show what each agent is doing in their CLI session. Do not type here directly — use the AgentChattr chat above instead. Agents won't see messages typed in their terminals.}
)}
diff --git a/src/components/BatchProgressPanel.tsx b/src/components/BatchProgressPanel.tsx index 61b550e..b2bba8a 100644 --- a/src/components/BatchProgressPanel.tsx +++ b/src/components/BatchProgressPanel.tsx @@ -2,6 +2,7 @@ import { useEffect, useState, useCallback } from "react"; import InfoTooltip from "./InfoTooltip"; +import { useLocale } from "@/components/LocaleProvider"; interface BatchProgressItem { issue_number: number; @@ -50,6 +51,7 @@ function ProgressBar({ percent }: { percent: number }) { * GitHub panel. */ export default function BatchProgressPanel({ projectId }: BatchProgressPanelProps) { + const { locale } = useLocale(); const [data, setData] = useState(null); const load = useCallback(() => { @@ -68,7 +70,7 @@ export default function BatchProgressPanel({ projectId }: BatchProgressPanelProp if (!data) { return (
- Loading batch progress… + {locale === "ko" ? "배치 진행 상황 로딩 중..." : "Loading batch progress…"}
); } @@ -79,11 +81,11 @@ export default function BatchProgressPanel({ projectId }: BatchProgressPanelProp
- Current Batch: (none) + {locale === "ko" ? "현재 배치: (없음)" : "Current Batch: (none)"}
- No active batch. Ask Head to start one via the chat. + {locale === "ko" ? "활성 배치가 없습니다. 채팅에서 Head에게 시작을 요청하세요." : "No active batch. Ask Head to start one via the chat."}
); @@ -95,12 +97,12 @@ export default function BatchProgressPanel({ projectId }: BatchProgressPanelProp
- Current Batch: Batch {data.batch_number ?? "—"} + {locale === "ko" ? `현재 배치: ${data.batch_number ?? "—"}번` : `Current Batch: Batch ${data.batch_number ?? "—"}`} - ✅ COMPLETE + {locale === "ko" ? "✅ 완료" : "✅ COMPLETE"}
- All {data.items.length} items merged. Waiting for the next batch. + {locale === "ko" ? `${data.items.length}개 항목 모두 병합됨. 다음 배치를 기다리는 중.` : `All ${data.items.length} items merged. Waiting for the next batch.`}
); @@ -110,11 +112,13 @@ export default function BatchProgressPanel({ projectId }: BatchProgressPanelProp
- Current Batch: Batch {data.batch_number ?? "—"} + {locale === "ko" ? `현재 배치: ${data.batch_number ?? "—"}번` : `Current Batch: Batch ${data.batch_number ?? "—"}`} - ({data.items.length} items) + {locale === "ko" ? `(${data.items.length}개 항목)` : `(${data.items.length} items)`} - Current Batch — progress tracker for the active batch. Polls GitHub to resolve each issue's status (queued → in review → approved → merged). + {locale === "ko" + ? <>현재 배치 - 활성 배치 진행 상황 추적기입니다. GitHub를 조회해 각 이슈 상태를 대기 → 검토 중 → 승인 → 병합 순으로 추적합니다. + : <>Current Batch — progress tracker for the active batch. Polls GitHub to resolve each issue's status (queued → in review → approved → merged).}
diff --git a/src/components/ControlBar.tsx b/src/components/ControlBar.tsx index b7305aa..eb430b9 100644 --- a/src/components/ControlBar.tsx +++ b/src/components/ControlBar.tsx @@ -11,10 +11,12 @@ import { getNotificationBackgroundOnly, setNotificationBackgroundOnly, } from "../lib/notificationSound"; +import { useLocale } from "@/components/LocaleProvider"; // ─── Server Controls ───────────────────────────────────────────────────────── function ServerSection({ projectId }: { projectId: string }) { + const { locale } = useLocale(); const [loading, setLoading] = useState(null); const [feedback, setFeedback] = useState(null); const [confirmStop, setConfirmStop] = useState(false); @@ -139,7 +141,7 @@ function ServerSection({ projectId }: { projectId: string }) { return (
- Server + {locale === "ko" ? "서버" : "Server"}
{feedback && ( @@ -205,6 +207,7 @@ const AWAKE_AUTO_POLL_MS = 30_000; const AWAKE_AUTO_DEFAULT_HOURS = 8; function SystemSection({ projectId }: { projectId: string }) { + const { locale } = useLocale(); const [active, setActive] = useState(false); const [remaining, setRemaining] = useState(null); const [platform, setPlatform] = useState(""); @@ -452,10 +455,10 @@ function SystemSection({ projectId }: { projectId: string }) { {showKeepAwakeSubsection && (
- Keep Mac Awake + {locale === "ko" ? "Mac 절전 방지" : "Keep Mac Awake"} @@ -475,7 +478,9 @@ function SystemSection({ projectId }: { projectId: string }) {
{showKeepAwakeHelp && (
- Keep Mac Awake runs macOS caffeinate to stop the screen, disk, and system idle timers from sleeping your Mac during an overnight run. Make sure the laptop is plugged in — caffeinate blocks sleep but not battery drain. + {locale === "ko" + ? <>Mac 절전 방지는 macOS의 caffeinate를 실행해 야간 작업 중 Mac이 절전 상태로 들어가는 것을 막습니다. 충전기를 연결해 두세요. caffeinate는 절전은 막지만 배터리 소모를 막아주지는 않습니다. + : <>Keep Mac Awake runs macOS caffeinate to stop the screen, disk, and system idle timers from sleeping your Mac during an overnight run. Make sure the laptop is plugged in — caffeinate blocks sleep but not battery drain.}
)} {awakeAutoStatus && ( @@ -523,17 +528,19 @@ function SystemSection({ projectId }: { projectId: string }) { is now its own subsection with an always-visible descriptor. */}
- Notification Sound + {locale === "ko" ? "알림음" : "Notification Sound"}
{showSoundHelp && (
- Notification Sound plays a brief chime when an agent posts a new message (not your own sends, not system events). Sound choice picks one of the bundled chimes. Background-only mode suppresses the chime while the tab is focused — ding only when you're looking elsewhere. All prefs persist in localStorage. + {locale === "ko" + ? <>알림음은 에이전트가 새 메시지를 보낼 때 짧은 알림음을 재생합니다. 내 메시지나 시스템 이벤트에는 울리지 않습니다. 사운드 선택으로 내장 알림음 중 하나를 고를 수 있고, 백그라운드 전용 모드는 탭이 포커스된 동안에는 알림음을 막습니다. 모든 설정은 localStorage에 저장됩니다. + : <>Notification Sound plays a brief chime when an agent posts a new message (not your own sends, not system events). Sound choice picks one of the bundled chimes. Background-only mode suppresses the chime while the tab is focused — ding only when you're looking elsewhere. All prefs persist in localStorage.}
)}
diff --git a/src/components/DiscordBridgeWidget.tsx b/src/components/DiscordBridgeWidget.tsx index 76c29af..cce5306 100644 --- a/src/components/DiscordBridgeWidget.tsx +++ b/src/components/DiscordBridgeWidget.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import InfoTooltip from "./InfoTooltip"; import DiscordSetupModal from "./DiscordSetupModal"; +import { useLocale } from "@/components/LocaleProvider"; interface BatchState { complete: boolean; @@ -42,6 +43,7 @@ async function callDiscord(action: string, body: Record) { * scratch. */ export default function DiscordBridgeWidget({ projectId }: DiscordBridgeWidgetProps) { + const { locale } = useLocale(); const [status, setStatus] = useState(null); const [busy, setBusy] = useState(false); const [actionError, setActionError] = useState(null); @@ -236,9 +238,11 @@ export default function DiscordBridgeWidget({ projectId }: DiscordBridgeWidgetPr
- Discord Bridge + {locale === "ko" ? "디스코드 브릿지" : "Discord Bridge"} - Discord Bridge forwards AgentChattr messages to a Discord channel so you can monitor from Discord. Bidirectional — replies from Discord appear in chat. + {locale === "ko" + ? <>디스코드 브릿지 - AgentChattr 메시지를 디스코드 채널로 전달해서 디스코드에서 모니터링할 수 있게 합니다. 양방향이며 디스코드에서 보낸 답장도 채팅에 나타납니다. + : <>Discord Bridge forwards AgentChattr messages to a Discord channel so you can monitor from Discord. Bidirectional — replies from Discord appear in chat.}
diff --git a/src/components/GitHubPanel.tsx b/src/components/GitHubPanel.tsx index b183e81..4f42e9f 100644 --- a/src/components/GitHubPanel.tsx +++ b/src/components/GitHubPanel.tsx @@ -4,6 +4,7 @@ import { useState, useEffect, useCallback } from "react"; import InfoTooltip from "./InfoTooltip"; import OvernightQueueModal from "./OvernightQueueModal"; import BatchProgressPanel from "./BatchProgressPanel"; +import { useLocale } from "@/components/LocaleProvider"; interface Issue { number: number; @@ -96,6 +97,7 @@ interface RateLimitInfo { } export default function GitHubPanel({ projectId }: GitHubPanelProps) { + const { locale } = useLocale(); const [issues, setIssues] = useState([]); const [prs, setPrs] = useState([]); // #411 / quadwork#281: recently closed issues + merged PRs. @@ -181,8 +183,12 @@ export default function GitHubPanel({ projectId }: GitHubPanelProps) { : "bg-[#ffcc00]/20 text-[#ffcc00]" }`}> {rateLimit.critical - ? `GitHub API rate limited — showing cached data. Resets in ${rateLimit.resetInMinutes}m` - : `GitHub API: ${rateLimit.remaining}/${rateLimit.limit} remaining. Resets in ${rateLimit.resetInMinutes}m` + ? (locale === "ko" + ? `GitHub API 제한에 걸렸습니다 - 캐시된 데이터를 표시합니다. ${rateLimit.resetInMinutes}분 후 초기화됩니다` + : `GitHub API rate limited — showing cached data. Resets in ${rateLimit.resetInMinutes}m`) + : (locale === "ko" + ? `GitHub API 남음: ${rateLimit.remaining}/${rateLimit.limit}. ${rateLimit.resetInMinutes}분 후 초기화` + : `GitHub API: ${rateLimit.remaining}/${rateLimit.limit} remaining. Resets in ${rateLimit.resetInMinutes}m`) }
)} @@ -192,15 +198,17 @@ export default function GitHubPanel({ projectId }: GitHubPanelProps) {
- Issues ({issues.length}) + {locale === "ko" ? `이슈 (${issues.length})` : `Issues (${issues.length})`} - Issues — open issues on the project's GitHub repo. Click any item to open it on GitHub. + {locale === "ko" + ? <>이슈 - 이 프로젝트 GitHub 저장소의 열린 이슈입니다. 항목을 클릭하면 GitHub에서 열립니다. + : <>Issues — open issues on the project's GitHub repo. Click any item to open it on GitHub.}
{issues.length === 0 && ( -
No issues
+
{locale === "ko" ? "이슈 없음" : "No issues"}
)} {issues.map((issue) => ( - Recently closed + {locale === "ko" ? "최근 종료됨" : "Recently closed"}
{closedIssues.length === 0 && ( -
None yet
+
{locale === "ko" ? "아직 없음" : "None yet"}
)} {closedIssues.map((issue) => (
- Pull Requests ({prs.length}) + {locale === "ko" ? `풀 리퀘스트 (${prs.length})` : `Pull Requests (${prs.length})`} - Pull Requests — open PRs awaiting review or merge. Click to open on GitHub. + {locale === "ko" + ? <>풀 리퀘스트 - 검토 또는 병합을 기다리는 열린 PR입니다. 클릭하면 GitHub에서 열립니다. + : <>Pull Requests — open PRs awaiting review or merge. Click to open on GitHub.}
{prs.length === 0 && ( -
No PRs
+
{locale === "ko" ? "PR 없음" : "No PRs"}
)} {prs.map((pr) => { const reviews = pr.reviews || []; @@ -313,10 +323,10 @@ export default function GitHubPanel({ projectId }: GitHubPanelProps) { {/* #411 / quadwork#281: Recently merged PRs — last 5, muted style with a ✓ to distinguish from open. */}
- Recently merged + {locale === "ko" ? "최근 병합됨" : "Recently merged"}
{mergedPrs.length === 0 && ( -
None yet
+
{locale === "ko" ? "아직 없음" : "None yet"}
)} {mergedPrs.map((pr) => (
OVERNIGHT-QUEUE.md - Overnight Queue — the task queue file Head reads to pick the next ticket. Click Edit to modify batch contents and ordering. + {locale === "ko" + ? <>야간 큐 - Head가 다음 티켓을 고를 때 읽는 작업 큐 파일입니다. 편집을 눌러 배치 내용과 순서를 수정할 수 있습니다. + : <>Overnight Queue — the task queue file Head reads to pick the next ticket. Click Edit to modify batch contents and ordering.}
diff --git a/src/components/HomeDashboard.tsx b/src/components/HomeDashboard.tsx index 6c60333..ef8f9a5 100644 --- a/src/components/HomeDashboard.tsx +++ b/src/components/HomeDashboard.tsx @@ -3,6 +3,7 @@ import { useState, useEffect } from "react"; import Link from "next/link"; import HomeEmptyState from "./HomeEmptyState"; +import { useLocale } from "@/components/LocaleProvider"; interface Project { id: string; @@ -21,18 +22,19 @@ interface ActivityEvent { projectName: string; } -function timeAgo(iso: string): string { +function timeAgo(iso: string, locale: "en" | "ko"): string { const diff = Date.now() - new Date(iso).getTime(); const mins = Math.floor(diff / 60000); - if (mins < 1) return "just now"; - if (mins < 60) return `${mins}m ago`; + if (mins < 1) return locale === "ko" ? "방금 전" : "just now"; + if (mins < 60) return locale === "ko" ? `${mins}분 전` : `${mins}m ago`; const hours = Math.floor(mins / 60); - if (hours < 24) return `${hours}h ago`; + if (hours < 24) return locale === "ko" ? `${hours}시간 전` : `${hours}h ago`; const days = Math.floor(hours / 24); - return `${days}d ago`; + return locale === "ko" ? `${days}일 전` : `${days}d ago`; } export default function HomeDashboard() { + const { locale } = useLocale(); const [projects, setProjects] = useState([]); const [activity, setActivity] = useState([]); // #229: track whether /api/projects has resolved (success OR @@ -77,15 +79,19 @@ export default function HomeDashboard() { )} {projectsState === "error" && (
- Could not load projects from /api/projects. The dashboard may be out of date — check the server logs and reload. + {locale === "ko" + ? "/api/projects 에서 프로젝트를 불러오지 못했습니다. 대시보드 상태가 오래되었을 수 있습니다. 서버 로그를 확인하고 새로고침해 주세요." + : "Could not load projects from /api/projects. The dashboard may be out of date — check the server logs and reload."}
)} {/* Header */}
-

Projects

+

{locale === "ko" ? "프로젝트" : "Projects"}

- {projects.length} configured project{projects.length !== 1 ? "s" : ""} + {locale === "ko" + ? `${projects.length}개 프로젝트 설정됨` + : `${projects.length} configured project${projects.length !== 1 ? "s" : ""}`}

@@ -110,13 +116,13 @@ export default function HomeDashboard() {
- open → + {locale === "ko" ? "열기 →" : "open →"}
- agents + {locale === "ko" ? "에이전트" : "agents"} {project.agentCount}
@@ -124,14 +130,14 @@ export default function HomeDashboard() { {project.openPrs}
- repo + {locale === "ko" ? "저장소" : "repo"} {project.repo}
{project.lastActivity && (
- last activity: {timeAgo(project.lastActivity)} + {locale === "ko" ? "최근 활동: " : "last activity: "}{timeAgo(project.lastActivity, locale)}
)} @@ -142,31 +148,30 @@ export default function HomeDashboard() { href="/setup" className="border border-dashed border-border p-4 flex items-center justify-center text-text-muted hover:text-text hover:border-text-muted transition-colors min-h-[88px]" > - + New Project + {locale === "ko" ? "+ 새 프로젝트" : "+ New Project"}
{/* #507: subtle Discord community link */}
- Want to talk with the creator?{" "} + {locale === "ko" ? "제작자와 이야기하고 싶다면 " : "Want to talk with the creator? "} - Join Hunt Town - {" "} - and find @project7. + {locale === "ko" ? "Hunt Town" : "Join Hunt Town"} + {locale === "ko" ? " 에 들어와서 @project7 을 찾아보세요." : " and find @project7."}
{/* Right column: activity feed — scrolls independently on desktop */}
-

Recent Activity

+

{locale === "ko" ? "최근 활동" : "Recent Activity"}

{activity.length === 0 && ( -
No recent activity
+
{locale === "ko" ? "최근 활동이 없습니다" : "No recent activity"}
)} {activity.map((item, i) => (
{/* #446: QuadWork symbol replaces the generic agent-team icon */} -

{headline}

-

{subtext}

+

{headline}

+

{subtext}

{hasProjects ? ( - ← look at the left sidebar + + {locale === "ko" ? "← 왼쪽 사이드바를 보세요" : "← look at the left sidebar"} + ) : ( - Add Your First Project → + {locale === "ko" ? "첫 프로젝트 추가 →" : "Add Your First Project →"} )}
diff --git a/src/components/HowToWorkModal.tsx b/src/components/HowToWorkModal.tsx index 7a66d5d..29e3e6f 100644 --- a/src/components/HowToWorkModal.tsx +++ b/src/components/HowToWorkModal.tsx @@ -1,34 +1,62 @@ "use client"; import { useEffect } from "react"; +import { useLocale } from "@/components/LocaleProvider"; interface HowToWorkModalProps { open: boolean; onClose: () => void; } -const STEPS: { title: string; body: string }[] = [ - { - title: "You assign a task in the chat", - body: "Tell @head what to build. Be as specific or as vague as you like.", - }, - { - title: "Head creates a GitHub issue", - body: "Head opens an issue, adds it to the queue, and waits for your trigger.", - }, - { - title: "Dev writes the code", - body: "Dev clones a branch, implements the change, and opens a pull request.", - }, - { - title: "Reviewers check the work", - body: "RE1 and RE2 each review the PR independently. Both must approve before the PR is mergeable.", - }, - { - title: "Head merges and continues", - body: "Head merges the approved PR and assigns the next ticket from the queue. The cycle continues all night while you sleep.", - }, -]; +function getSteps(locale: "en" | "ko"): { title: string; body: string }[] { + if (locale === "ko") { + return [ + { + title: "채팅에서 작업을 지시합니다", + body: "@head 에게 무엇을 만들지 말해 주세요. 아주 구체적으로 써도 되고, 느슨하게 지시해도 됩니다.", + }, + { + title: "Head가 GitHub 이슈를 만듭니다", + body: "Head가 이슈를 열고, 큐에 추가한 뒤, 당신의 트리거를 기다립니다.", + }, + { + title: "Dev가 코드를 작성합니다", + body: "Dev가 브랜치를 만들고, 변경 사항을 구현한 뒤, 풀 리퀘스트를 엽니다.", + }, + { + title: "리뷰어가 작업을 검토합니다", + body: "RE1과 RE2가 각각 독립적으로 PR을 리뷰합니다. 둘 다 승인해야 PR이 병합 가능 상태가 됩니다.", + }, + { + title: "Head가 병합하고 계속 진행합니다", + body: "Head가 승인된 PR을 병합하고, 큐에서 다음 티켓을 할당합니다. 당신이 자는 동안에도 이 사이클은 밤새 계속됩니다.", + }, + ]; + } + + return [ + { + title: "You assign a task in the chat", + body: "Tell @head what to build. Be as specific or as vague as you like.", + }, + { + title: "Head creates a GitHub issue", + body: "Head opens an issue, adds it to the queue, and waits for your trigger.", + }, + { + title: "Dev writes the code", + body: "Dev clones a branch, implements the change, and opens a pull request.", + }, + { + title: "Reviewers check the work", + body: "RE1 and RE2 each review the PR independently. Both must approve before the PR is mergeable.", + }, + { + title: "Head merges and continues", + body: "Head merges the approved PR and assigns the next ticket from the queue. The cycle continues all night while you sleep.", + }, + ]; +} /** * "How to Work" modal (#229). @@ -38,6 +66,9 @@ const STEPS: { title: string; body: string }[] = [ * Closes on Escape, backdrop click, or the X button. */ export default function HowToWorkModal({ open, onClose }: HowToWorkModalProps) { + const { locale } = useLocale(); + const steps = getSteps(locale); + useEffect(() => { if (!open) return; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; @@ -56,13 +87,13 @@ export default function HowToWorkModal({ open, onClose }: HowToWorkModalProps) { aria-labelledby="how-to-work-title" >
e.stopPropagation()} > -

How QuadWork builds your code

+

+ {locale === "ko" ? "QuadWork가 코드를 만드는 방식" : "How QuadWork builds your code"} +

- Five steps from your one-line request to a merged pull request. + {locale === "ko" + ? "한 줄 요청에서 병합된 풀 리퀘스트까지 가는 5단계입니다." + : "Five steps from your one-line request to a merged pull request."}

    {/* Vertical accent line connecting the step circles. */} - {STEPS.map((step, i) => ( + {steps.map((step, i) => (
  1. void; +} + +const LocaleContext = createContext(null); + +export function LocaleProvider({ + children, +}: { + children: React.ReactNode; +}) { + const [hydrated, setHydrated] = useState(false); + const [locale, setLocaleState] = useState("en"); + + useEffect(() => { + setLocaleState(detectBrowserLocale()); + setHydrated(true); + }, []); + + useEffect(() => { + document.documentElement.lang = locale; + try { + window.localStorage.setItem(LOCALE_STORAGE_KEY, locale); + } catch {} + try { + document.cookie = `${LOCALE_COOKIE_KEY}=${locale}; path=/; max-age=31536000; samesite=lax`; + } catch {} + }, [locale]); + + const value = useMemo(() => ({ + hydrated, + locale, + setLocale: (next) => setLocaleState(normalizeLocale(next)), + }), [hydrated, locale]); + + return {children}; +} + +export function useLocale() { + const value = useContext(LocaleContext); + if (!value) throw new Error("useLocale must be used within LocaleProvider"); + return value; +} diff --git a/src/components/LoopGuardWidget.tsx b/src/components/LoopGuardWidget.tsx index 881a03a..496f9d4 100644 --- a/src/components/LoopGuardWidget.tsx +++ b/src/components/LoopGuardWidget.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import InfoTooltip from "./InfoTooltip"; +import { useLocale } from "@/components/LocaleProvider"; interface LoopGuardWidgetProps { projectId: string; @@ -18,6 +19,7 @@ interface LoopGuardWidgetProps { * via update_settings ws event so the change is immediate. */ export default function LoopGuardWidget({ projectId }: LoopGuardWidgetProps) { + const { locale } = useLocale(); const [value, setValue] = useState(30); const [draft, setDraft] = useState("30"); const [saving, setSaving] = useState(false); @@ -122,9 +124,11 @@ export default function LoopGuardWidget({ projectId }: LoopGuardWidgetProps) { return (
    - Loop Guard + {locale === "ko" ? "루프 가드" : "Loop Guard"} - Loop Guard pauses agent-to-agent message chains after this many hops with no human reply. Higher values let agents work longer overnight; lower values add safety against runaway loops. AgentChattr accepts 4–50; QuadWork defaults to 30 (about 5–6 full PR cycles). Posting any chat message yourself resets the counter immediately. + {locale === "ko" + ? <>루프 가드 - 사람의 응답 없이 에이전트끼리 메시지를 주고받는 횟수가 이 값에 도달하면 체인을 멈춥니다. 값을 높이면 야간 작업을 더 길게 돌릴 수 있고, 낮추면 runaway loop에 대한 안전성이 높아집니다. AgentChattr 허용 범위는 4-50이며 QuadWork 기본값은 30입니다. 직접 채팅을 한 번 보내면 카운터는 즉시 초기화됩니다. + : <>Loop Guard pauses agent-to-agent message chains after this many hops with no human reply. Higher values let agents work longer overnight; lower values add safety against runaway loops. AgentChattr accepts 4–50; QuadWork defaults to 30 (about 5–6 full PR cycles). Posting any chat message yourself resets the counter immediately.}
    diff --git a/src/components/OperatorFeaturesPanel.tsx b/src/components/OperatorFeaturesPanel.tsx index 4d74bc3..0e13b63 100644 --- a/src/components/OperatorFeaturesPanel.tsx +++ b/src/components/OperatorFeaturesPanel.tsx @@ -8,6 +8,7 @@ import DiscordBridgeWidget from "./DiscordBridgeWidget"; import LoopGuardWidget from "./LoopGuardWidget"; import ProjectHistoryWidget from "./ProjectHistoryWidget"; import AgentModelsWidget from "./AgentModelsWidget"; +import { useLocale } from "@/components/LocaleProvider"; /** * Bottom-right quadrant of the project dashboard (#208). @@ -30,11 +31,14 @@ import AgentModelsWidget from "./AgentModelsWidget"; * clips in cramped split-view / mobile. */ export default function OperatorFeaturesPanel({ projectId }: { projectId: string }) { + const { locale } = useLocale(); return (
    - - Operator Features — tools for running autonomous overnight batches. Includes the Scheduled Trigger, Telegram Bridge, Loop Guard, Project History, and Agent Models. + {locale === "ko" + ? <>운영자 기능 - 야간 자율 배치를 운영할 때 쓰는 도구 모음입니다. Scheduled Trigger, Telegram Bridge, Discord Bridge, Loop Guard, Project History, Agent Models가 포함됩니다. + : <>Operator Features — tools for running autonomous overnight batches. Includes the Scheduled Trigger, Telegram Bridge, Loop Guard, Project History, and Agent Models.} } />
    diff --git a/src/components/ProjectDashboard.tsx b/src/components/ProjectDashboard.tsx index c92df58..0ef64de 100644 --- a/src/components/ProjectDashboard.tsx +++ b/src/components/ProjectDashboard.tsx @@ -8,6 +8,7 @@ import GitHubPanel from "./GitHubPanel"; import ControlBar from "./ControlBar"; import AgentTerminalsGrid from "./AgentTerminalsGrid"; import OperatorFeaturesPanel from "./OperatorFeaturesPanel"; +import { useLocale } from "@/components/LocaleProvider"; const MIN_SIZE = 150; // px const DIVIDER = 4; // px @@ -19,6 +20,7 @@ interface ProjectDashboardProps { } export default function ProjectDashboard({ projectId }: ProjectDashboardProps) { + const { locale } = useLocale(); const containerRef = useRef(null); const [colRatio, setColRatio] = useState(0.5); const [rowRatio, setRowRatio] = useState(0.5); @@ -71,16 +73,20 @@ export default function ProjectDashboard({ projectId }: ProjectDashboardProps) { - ), [filterSystem, toggleFilter]); + ), [filterSystem, locale, toggleFilter]); // Poll agent states useEffect(() => { @@ -172,7 +178,9 @@ export default function ProjectDashboard({ projectId }: ProjectDashboardProps) {
    - Primary Chat — live chat between you and the 4 AI agents. Messages you type here trigger agent actions. Use @mentions to address specific agents. + {locale === "ko" + ? <>메인 채팅 - 당신과 4개의 AI 에이전트가 실시간으로 대화하는 공간입니다. 여기 입력한 메시지가 에이전트 동작을 시작시킵니다. 특정 에이전트를 부를 때는 @멘션을 사용하세요. + : <>Primary Chat — live chat between you and the 4 AI agents. Messages you type here trigger agent actions. Use @mentions to address specific agents.} }> {filterToggle} @@ -221,7 +229,9 @@ export default function ProjectDashboard({ projectId }: ProjectDashboardProps) {
    - GitHub — open issues and pull requests on this project's repo. Click any item to open it on GitHub. The batch progress panel tracks the active batch's lifecycle from queued to merged. + {locale === "ko" + ? <>GitHub - 이 프로젝트 저장소의 열린 이슈와 PR을 보여줍니다. 항목을 클릭하면 GitHub에서 바로 열립니다. 아래 배치 진행 패널은 현재 배치가 대기에서 병합까지 어떻게 진행되는지 추적합니다. + : <>GitHub — open issues and pull requests on this project's repo. Click any item to open it on GitHub. The batch progress panel tracks the active batch's lifecycle from queued to merged.} } />
    diff --git a/src/components/ProjectHistoryWidget.tsx b/src/components/ProjectHistoryWidget.tsx index 6c3e650..0fea72d 100644 --- a/src/components/ProjectHistoryWidget.tsx +++ b/src/components/ProjectHistoryWidget.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react"; import InfoTooltip from "./InfoTooltip"; +import { useLocale } from "@/components/LocaleProvider"; interface ProjectHistoryWidgetProps { projectId: string; @@ -37,6 +38,7 @@ const MAX_BYTES = 10 * 1024 * 1024; * project, and renders a small progress / result block. */ export default function ProjectHistoryWidget({ projectId }: ProjectHistoryWidgetProps) { + const { locale } = useLocale(); const fileRef = useRef(null); const [busy, setBusy] = useState<"export" | "import" | "restore" | null>(null); const [error, setError] = useState(null); @@ -264,9 +266,11 @@ export default function ProjectHistoryWidget({ projectId }: ProjectHistoryWidget return (
    - Project History + {locale === "ko" ? "프로젝트 히스토리" : "Project History"} - Project History — export or import the full AgentChattr chat history for this project. Useful for backup, migration, or resuming after a fresh install. + {locale === "ko" + ? <>프로젝트 히스토리 - 이 프로젝트의 전체 AgentChattr 채팅 기록을 내보내거나 가져옵니다. 백업, 마이그레이션, 재설치 후 복구에 유용합니다. + : <>Project History — export or import the full AgentChattr chat history for this project. Useful for backup, migration, or resuming after a fresh install.}
    diff --git a/src/components/ScheduledTriggerWidget.tsx b/src/components/ScheduledTriggerWidget.tsx index cb7167e..c27247f 100644 --- a/src/components/ScheduledTriggerWidget.tsx +++ b/src/components/ScheduledTriggerWidget.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import InfoTooltip from "./InfoTooltip"; +import { useLocale } from "@/components/LocaleProvider"; interface ScheduledTriggerWidgetProps { projectId: string; @@ -86,6 +87,7 @@ function formatCountdown(ms: number): string { * project picks up the last-used message + running status. */ export default function ScheduledTriggerWidget({ projectId }: ScheduledTriggerWidgetProps) { + const { locale } = useLocale(); const [trigger, setTrigger] = useState(null); const [message, setMessage] = useState(""); const [intervalMin, setIntervalMin] = useState(15); @@ -398,10 +400,14 @@ export default function ScheduledTriggerWidget({ projectId }: ScheduledTriggerWi
    - Scheduled Trigger{running ? (autoTriggered ? " (auto)" : " (running)") : ""} + {locale === "ko" + ? `예약 트리거${running ? (autoTriggered ? " (자동)" : " (실행 중)") : ""}` + : `Scheduled Trigger${running ? (autoTriggered ? " (auto)" : " (running)") : ""}`} - Scheduled Trigger sends a periodic message to all agents on a timer. Use this to keep the autonomous workflow running overnight. First message fires after the configured interval, not immediately. + {locale === "ko" + ? <>예약 트리거 - 타이머에 따라 모든 에이전트에게 주기적으로 메시지를 보냅니다. 야간 자율 워크플로우를 계속 돌릴 때 사용하세요. 첫 메시지는 즉시가 아니라 설정한 간격 후에 전송됩니다. + : <>Scheduled Trigger sends a periodic message to all agents on a timer. Use this to keep the autonomous workflow running overnight. First message fires after the configured interval, not immediately.}
    @@ -410,14 +416,16 @@ export default function ScheduledTriggerWidget({ projectId }: ScheduledTriggerWi
    diff --git a/src/components/SettingsPage.tsx b/src/components/SettingsPage.tsx index 0bb825b..f0dab52 100644 --- a/src/components/SettingsPage.tsx +++ b/src/components/SettingsPage.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useCallback, useRef } from "react"; import { useSearchParams } from "next/navigation"; +import { useLocale } from "@/components/LocaleProvider"; interface AgentConfig { display_name: string; @@ -53,6 +54,163 @@ const BACKENDS: { value: string; label: string }[] = [ ]; const MODELS = ["opus", "sonnet", "haiku"]; +const COPY = { + en: { + loading: "Loading...", + title: "Settings", + save: "Save", + saving: "Saving...", + saved: "Saved", + operatorIdentity: "Operator Identity", + yourNameInChat: "Your name in chat", + language: "Language", + operatorHelp: + "Shows next to your messages in the AgentChattr chat panel. Defaults to user if blank. Allowed: 1-32 letters, digits, dash, underscore (matches AgentChattr name rules; other characters are stripped server-side). Reserved agent names like head, dev, re1, re2, and system are rejected and fall back to user.", + global: "Global", + dashboardPort: "QuadWork Dashboard Port", + agentChattrUrlGlobal: "AgentChattr URL (global override)", + globalHelp: + "The dashboard binds to the QuadWork port. The AgentChattr URL is the v1 fallback; new projects use a per-project AgentChattr clone and ignore this field.", + defaults: "Defaults", + defaultAgentCli: "Default agent CLI", + reviewerGithubUser: "Reviewer GitHub user", + reviewerGithubToken: "Reviewer GitHub token", + configured: "Configured", + notConfigured: "Not configured", + pasteNewToken: "Paste new token", + defaultsHelp: + "The default CLI seeds new project agents. The reviewer GitHub user/token are used by RE1/RE2 to post PR review comments without your personal token. The token is written to ~/.quadwork/reviewer-token (mode 0600) and is never returned by the API.", + system: "System", + keepAwake: "Keep Awake", + on: "on", + off: "off", + stop: "Stop", + start: "Start", + keepAwakeHelp: + "Prevents this machine from sleeping while agents are running. Machine-level (not per-project) - uses caffeinate on macOS.", + cleanup: "Cleanup", + cleanupIntro: + "Each project now has its own AgentChattr clone at ~/.quadwork/{id}/agentchattr (~77 MB). After all projects are migrated, the legacy global install can be removed:", + cleanupSingle: "To remove a single project's clone and config entry:", + cleanupHelp: + "Both commands prompt for confirmation. Worktrees and source repos are never touched. See npx quadwork --help or the README's Disk Usage section for details.", + activeProjects: "Active Projects", + projectName: "Project Name", + githubRepo: "GitHub Repo", + workingDirectory: "Working Directory", + agents: "Agents", + onlyInstalledPrefix: "Only", + onlyInstalledSuffix: "is installed.", + installOther: "Install", + forMoreBackendOptions: "for more backend options:", + name: "Name", + command: "Command", + model: "Model", + cwd: "CWD", + agentsMd: "AGENTS.md", + owner: "Owner", + reviewer: "Reviewer", + builder: "Builder", + edit: "edit", + oneCliInstalled: "Only one CLI installed - install the other for more options", + agentsMdPlaceholder: "# AGENTS.md seed content for this agent...", + agentChattr: "AgentChattr", + agentChattrUrl: "AgentChattr URL", + sessionToken: "Session Token", + optional: "(optional)", + mcpHttpPort: "MCP HTTP Port", + mcpSsePort: "MCP SSE Port", + restoreProject: "Restore Project", + archive: "Archive", + remove: "Remove", + removeQuestion: "Remove?", + confirm: "Confirm", + cancel: "Cancel", + addProject: "+ Add Project", + archived: "Archived", + restore: "Restore", + confirmRemove: "Confirm Remove", + newProject: "New Project", + }, + ko: { + loading: "로딩 중...", + title: "설정", + save: "저장", + saving: "저장 중...", + saved: "저장됨", + operatorIdentity: "운영자 정보", + yourNameInChat: "채팅에서의 이름", + language: "언어", + operatorHelp: + "AgentChattr 채팅 패널에서 내 메시지 옆에 표시됩니다. 비워두면 기본값은 user입니다. 허용: 1-32자의 영문, 숫자, 하이픈, 언더스코어(AgentChattr 이름 규칙과 동일). 다른 문자는 서버에서 제거됩니다. head, dev, re1, re2, system 같은 예약 이름은 거부되고 user로 대체됩니다.", + global: "전역", + dashboardPort: "QuadWork 대시보드 포트", + agentChattrUrlGlobal: "AgentChattr URL (전역 오버라이드)", + globalHelp: + "대시보드는 QuadWork 포트에 바인딩됩니다. AgentChattr URL은 v1 호환용 기본값이며, 새 프로젝트는 프로젝트별 AgentChattr 클론을 사용하므로 이 필드는 무시됩니다.", + defaults: "기본값", + defaultAgentCli: "기본 에이전트 CLI", + reviewerGithubUser: "리뷰어 GitHub 사용자", + reviewerGithubToken: "리뷰어 GitHub 토큰", + configured: "설정됨", + notConfigured: "미설정", + pasteNewToken: "새 토큰 붙여넣기", + defaultsHelp: + "기본 CLI는 새 프로젝트 에이전트의 초기값으로 사용됩니다. 리뷰어 GitHub 사용자/토큰은 개인 토큰 없이 RE1/RE2가 PR 리뷰 댓글을 남길 때 사용됩니다. 토큰은 ~/.quadwork/reviewer-token (권한 0600)에 저장되며 API로는 반환되지 않습니다.", + system: "시스템", + keepAwake: "절전 방지", + on: "켜짐", + off: "꺼짐", + stop: "중지", + start: "시작", + keepAwakeHelp: + "에이전트가 실행되는 동안 이 기기가 잠들지 않도록 합니다. 기기 전체 설정이며(프로젝트별 아님) macOS에서는 caffeinate를 사용합니다.", + cleanup: "정리", + cleanupIntro: + "각 프로젝트는 이제 ~/.quadwork/{id}/agentchattr (~77 MB)에 자체 AgentChattr 클론을 가집니다. 모든 프로젝트 마이그레이션이 끝나면 예전 전역 설치는 제거할 수 있습니다:", + cleanupSingle: "특정 프로젝트의 클론과 설정 항목만 제거하려면:", + cleanupHelp: + "두 명령 모두 확인 절차가 있습니다. 워크트리와 소스 저장소는 건드리지 않습니다. 자세한 내용은 npx quadwork --help 또는 README의 Disk Usage 섹션을 참고하세요.", + activeProjects: "활성 프로젝트", + projectName: "프로젝트 이름", + githubRepo: "GitHub 저장소", + workingDirectory: "작업 디렉터리", + agents: "에이전트", + onlyInstalledPrefix: "", + onlyInstalledSuffix: "만 설치되어 있습니다.", + installOther: "", + forMoreBackendOptions: "다른 CLI를 설치하면 선택지가 늘어납니다:", + name: "이름", + command: "명령어", + model: "모델", + cwd: "작업 디렉터리", + agentsMd: "AGENTS.md", + owner: "소유자", + reviewer: "검토자", + builder: "개발자", + edit: "편집", + oneCliInstalled: "CLI 하나만 설치됨 - 더 많은 옵션을 위해 다른 CLI를 설치하세요", + agentsMdPlaceholder: "# 이 에이전트의 AGENTS.md 초기 내용...", + agentChattr: "AgentChattr", + agentChattrUrl: "AgentChattr URL", + sessionToken: "세션 토큰", + optional: "(선택)", + mcpHttpPort: "MCP HTTP 포트", + mcpSsePort: "MCP SSE 포트", + restoreProject: "프로젝트 복원", + archive: "보관", + remove: "제거", + removeQuestion: "제거할까요?", + confirm: "확인", + cancel: "취소", + addProject: "+ 프로젝트 추가", + archived: "보관됨", + restore: "복원", + confirmRemove: "제거 확인", + newProject: "새 프로젝트", + }, +} as const; + function Input({ label, value, onChange, onBlur, type = "text", placeholder }: { label: string; value: string; @@ -99,6 +257,8 @@ function Select({ label, value, onChange, options }: { } export default function SettingsPage() { + const { locale, setLocale } = useLocale(); + const t = COPY[locale]; const searchParams = useSearchParams(); const [config, setConfig] = useState(null); const [saving, setSaving] = useState(false); @@ -299,7 +459,7 @@ export default function SettingsPage() { } const newProject: ProjectConfig = { id, - name: "New Project", + name: t.newProject, repo: "owner/repo", working_dir: "", agents, @@ -389,18 +549,18 @@ export default function SettingsPage() { setConfirmDelete(null); }; - if (!config) return
    Loading...
    ; + if (!config) return
    {t.loading}
    ; return (
    -

    Settings

    +

    {t.title}

    @@ -408,28 +568,48 @@ export default function SettingsPage() { dashboard chat messages. Server-side validated to AC's registry name rules (1–32 alnum + dash + underscore). */}
    -

    Operator Identity

    -
    +

    {t.operatorIdentity}

    +
    updateGlobal("operator_name" as keyof Config, v)} placeholder="user" /> +
    + +
    + {(["en", "ko"] as const).map((code) => { + const active = locale === code; + return ( + + ); + })} +
    +

    - Shows next to your messages in the AgentChattr chat panel. Defaults to user if blank. - Allowed: 1–32 letters, digits, dash, underscore (matches AgentChattr's name rules; other characters are stripped server-side). - Reserved agent names like head, dev, re1, re2, and system are rejected and fall back to user. + {t.operatorHelp}

    {/* Global Settings (#212: full-width grid, every section visible) */}
    -

    Global

    +

    {t.global}

    setPortDraft(v)} onBlur={() => { @@ -441,24 +621,23 @@ export default function SettingsPage() { type="number" /> updateGlobal("agentchattr_url", v)} placeholder="http://127.0.0.1:8300" />

    - The dashboard binds to the QuadWork port. The AgentChattr URL is the v1 fallback; - new projects use a per-project AgentChattr clone (master #181) and ignore this field. + {t.globalHelp}

    {/* Defaults — default agent CLI + reviewer credentials (#212) */}
    -

    Defaults

    +

    {t.defaults}

    updateGlobal("reviewer_github_user" as keyof Config, v)} placeholder="reviewer-bot" />
    - +
    - {reviewerTokenExists === null ? "…" : reviewerTokenExists ? "Configured" : "Not configured"} + {reviewerTokenExists === null ? "…" : reviewerTokenExists ? t.configured : t.notConfigured}
    @@ -485,7 +664,7 @@ export default function SettingsPage() { type="password" value={reviewerTokenInput} onChange={(e) => setReviewerTokenInput(e.target.value)} - placeholder="Paste new token" + placeholder={t.pasteNewToken} className="flex-1 bg-transparent border border-border px-2 py-1 text-[11px] text-text outline-none focus:border-accent font-mono" />

    - The default CLI seeds new project agents. The reviewer GitHub user/token are - used by RE1/RE2 to post PR review comments without your personal - token. The token is written to{" "} - ~/.quadwork/reviewer-token{" "} - (mode 0600) and is never returned by the API. + {t.defaultsHelp}

    {/* System — Keep Awake (#212) */}
    -

    System

    +

    {t.system}

    - Keep Awake — {keepAwakeActive ? "on" : "off"} + {t.keepAwake} - {keepAwakeActive ? t.on : t.off} - Prevents this machine from sleeping while agents are running. Machine-level - (not per-project) — uses caffeinate on macOS. + {t.keepAwakeHelp}
    {/* Cleanup commands (#212 / #189) */}
    -

    Cleanup

    +

    {t.cleanup}

    - Each project now has its own AgentChattr clone at + {t.cleanupIntro.split("~/.quadwork/{id}/agentchattr")[0]} {" "}~/.quadwork/{id}/agentchattr - {" "}(~77 MB). After all projects are migrated, the legacy global install can be removed: + {t.cleanupIntro.includes("~/.quadwork/{id}/agentchattr") + ? t.cleanupIntro.split("~/.quadwork/{id}/agentchattr")[1] + : ""}

    npx quadwork cleanup --legacy
    -

    To remove a single project's clone and config entry:

    +

    {t.cleanupSingle}

    npx quadwork cleanup --project <id>
    -

    - Both commands prompt for confirmation. Worktrees and source repos are never touched. - See npx quadwork --help or the README's Disk Usage section for details. -

    +

    {t.cleanupHelp}

    @@ -550,7 +723,7 @@ export default function SettingsPage() { {/* Per-project settings */}
    -

    Active Projects

    +

    {t.activeProjects}

    {config.projects.filter((p) => !p.archived).map((project) => { const idx = config.projects.indexOf(project); @@ -567,18 +740,18 @@ export default function SettingsPage() { {/* Basic project info */}
    renameProject(idx, v)} /> updateProject(idx, { repo: v })} placeholder="owner/repo" /> updateProject(idx, { working_dir: v })} placeholder="/path/to/project" @@ -587,14 +760,18 @@ export default function SettingsPage() { {/* Agents table */}
    -

    Agents

    +

    {t.agents}

    {cliStatus && (cliStatus.claude ? !cliStatus.codex : cliStatus.codex) && (
    - {cliStatus.claude ? "Only Claude Code" : "Only Codex CLI"} is installed. + {locale === "ko" + ? `${cliStatus.claude ? "Claude Code" : "Codex CLI"}${t.onlyInstalledSuffix}` + : `${t.onlyInstalledPrefix} ${cliStatus.claude ? "Claude Code" : "Codex CLI"} ${t.onlyInstalledSuffix}`} - Install {cliStatus.claude ? "Codex" : "Claude Code"} for more backend options: + {locale === "ko" + ? `${cliStatus.claude ? "Codex" : "Claude Code"} ${t.forMoreBackendOptions}` + : `${t.installOther} ${cliStatus.claude ? "Codex" : "Claude Code"} ${t.forMoreBackendOptions}`} {cliStatus.claude ? "npm install -g codex" : "npm install -g @anthropic-ai/claude-code"} @@ -603,11 +780,11 @@ export default function SettingsPage() { )}
    - Name - Command - Model - CWD - AGENTS.md + {t.name} + {t.command} + {t.model} + {t.cwd} + {t.agentsMd}
    {Object.entries(project.agents || {}).map(([agentId, agent]) => (
    @@ -619,7 +796,7 @@ export default function SettingsPage() { className="bg-transparent text-[11px] text-text font-semibold outline-none border border-border px-1 py-0.5 focus:border-accent" /> - {agentId === "head" ? "Owner" : agentId.startsWith("reviewer") ? "Reviewer" : "Builder"} + {agentId === "head" ? t.owner : agentId.startsWith("reviewer") ? t.reviewer : t.builder}