diff --git a/packages/views/eslint.config.mjs b/packages/views/eslint.config.mjs index cedc14383f..a64f6b5f75 100644 --- a/packages/views/eslint.config.mjs +++ b/packages/views/eslint.config.mjs @@ -85,6 +85,9 @@ const TRANSLATED_FILES = [ "layout/app-sidebar.tsx", "layout/help-launcher.tsx", "layout/workspace-loader.tsx", + "onboarding/components/step-header.tsx", + "onboarding/steps/step-welcome.tsx", + "onboarding/onboarding-flow.tsx", ]; export default [ diff --git a/packages/views/locales/en/onboarding.json b/packages/views/locales/en/onboarding.json index 0967ef424b..7f8039bf12 100644 --- a/packages/views/locales/en/onboarding.json +++ b/packages/views/locales/en/onboarding.json @@ -1 +1,22 @@ -{} +{ + "step_header": { + "step_of": "Step {{current}} of {{total}}" + }, + "welcome": { + "wordmark": "Welcome to Multica", + "headline_line1": "Your AI teammates,", + "headline_line2": "in", + "headline_emphasis": "one workspace.", + "lede": "Assign them work like you'd assign a colleague — they pick it up, update status, and comment when done.", + "lede_web": "Desktop bundles the runtime — nothing to install. Continue on web to connect your own CLI.", + "lede_desktop": "By the end, a real agent will be replying to your first issue.", + "download_desktop": "Download Desktop", + "continue_on_web": "Continue on web", + "start_exploring": "Start exploring", + "skip_existing": "I've done this before", + "illustration_caption": "Every issue, every thread, every decision — shared by your team and agents." + }, + "errors": { + "skip_failed": "Failed to finish onboarding" + } +} diff --git a/packages/views/locales/index.ts b/packages/views/locales/index.ts index 079ad4dc5c..573d22147a 100644 --- a/packages/views/locales/index.ts +++ b/packages/views/locales/index.ts @@ -3,6 +3,7 @@ import enCommon from "./en/common.json"; import enAuth from "./en/auth.json"; import enSettings from "./en/settings.json"; import enEditor from "./en/editor.json"; +import enOnboarding from "./en/onboarding.json"; import enInvite from "./en/invite.json"; import enLabels from "./en/labels.json"; import enMembers from "./en/members.json"; @@ -21,6 +22,7 @@ import zhHansCommon from "./zh-Hans/common.json"; import zhHansAuth from "./zh-Hans/auth.json"; import zhHansSettings from "./zh-Hans/settings.json"; import zhHansEditor from "./zh-Hans/editor.json"; +import zhHansOnboarding from "./zh-Hans/onboarding.json"; import zhHansInvite from "./zh-Hans/invite.json"; import zhHansLabels from "./zh-Hans/labels.json"; import zhHansMembers from "./zh-Hans/members.json"; @@ -45,6 +47,7 @@ export const RESOURCES: Record = { auth: enAuth, settings: enSettings, editor: enEditor, + onboarding: enOnboarding, invite: enInvite, labels: enLabels, members: enMembers, @@ -65,6 +68,7 @@ export const RESOURCES: Record = { auth: zhHansAuth, settings: zhHansSettings, editor: zhHansEditor, + onboarding: zhHansOnboarding, invite: zhHansInvite, labels: zhHansLabels, members: zhHansMembers, diff --git a/packages/views/locales/zh-Hans/onboarding.json b/packages/views/locales/zh-Hans/onboarding.json index 0967ef424b..b0d5efe2e1 100644 --- a/packages/views/locales/zh-Hans/onboarding.json +++ b/packages/views/locales/zh-Hans/onboarding.json @@ -1 +1,22 @@ -{} +{ + "step_header": { + "step_of": "第 {{current}} 步 / 共 {{total}} 步" + }, + "welcome": { + "wordmark": "欢迎使用 Multica", + "headline_line1": "你的 AI 队友,", + "headline_line2": "在", + "headline_emphasis": "同一个工作区。", + "lede": "像分配给同事一样把任务交给它们——它们会接手、推进状态、完成后留下评论。", + "lede_web": "桌面端自带运行时,无需安装。在 web 端继续可以连接你自己的 CLI。", + "lede_desktop": "完成本流程后,真实的智能体会回复你创建的第一个 issue。", + "download_desktop": "下载桌面端", + "continue_on_web": "在 web 端继续", + "start_exploring": "开始探索", + "skip_existing": "我之前做过了", + "illustration_caption": "每个 issue、每条对话、每个决策——团队和智能体共享同一份上下文。" + }, + "errors": { + "skip_failed": "完成上手引导失败" + } +} diff --git a/packages/views/onboarding/components/step-header.test.tsx b/packages/views/onboarding/components/step-header.test.tsx index 859222550c..7a0076ab79 100644 --- a/packages/views/onboarding/components/step-header.test.tsx +++ b/packages/views/onboarding/components/step-header.test.tsx @@ -1,8 +1,28 @@ +import type { ReactNode } from "react"; import { describe, expect, it } from "vitest"; -import { render, screen } from "@testing-library/react"; +import { render as rtlRender, screen, type RenderOptions } from "@testing-library/react"; import { ONBOARDING_STEP_ORDER } from "@multica/core/onboarding"; +import { I18nProvider } from "@multica/core/i18n/react"; +import enCommon from "../../locales/en/common.json"; +import enOnboarding from "../../locales/en/onboarding.json"; import { StepHeader } from "./step-header"; +const TEST_RESOURCES = { + en: { common: enCommon, onboarding: enOnboarding }, +}; + +function I18nWrapper({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} + +function render(ui: React.ReactElement, options?: RenderOptions) { + return rtlRender(ui, { wrapper: I18nWrapper, ...options }); +} + describe("StepHeader", () => { it("renders one dot per step in ONBOARDING_STEP_ORDER", () => { const { container } = render(); diff --git a/packages/views/onboarding/components/step-header.tsx b/packages/views/onboarding/components/step-header.tsx index 7959efb591..2e97948ef5 100644 --- a/packages/views/onboarding/components/step-header.tsx +++ b/packages/views/onboarding/components/step-header.tsx @@ -5,6 +5,7 @@ import { type OnboardingStep, } from "@multica/core/onboarding"; import { cn } from "@multica/ui/lib/utils"; +import { useT } from "../../i18n"; /** * Horizontal step indicator shown at the top of every onboarding step @@ -30,6 +31,7 @@ import { cn } from "@multica/ui/lib/utils"; * mapping from local UI step to the canonical `OnboardingStep`. */ export function StepHeader({ currentStep }: { currentStep: OnboardingStep }) { + const { t } = useT("onboarding"); const total = ONBOARDING_STEP_ORDER.length; const currentIndex = ONBOARDING_STEP_ORDER.indexOf(currentStep); // Defensive: unknown step → render a disabled-looking header rather @@ -43,7 +45,7 @@ export function StepHeader({ currentStep }: { currentStep: OnboardingStep }) { aria-valuemin={1} aria-valuemax={total} aria-valuenow={safeIndex + 1} - aria-label={`Step ${safeIndex + 1} of ${total}`} + aria-label={t(($) => $.step_header.step_of, { current: safeIndex + 1, total })} className="flex w-full items-center justify-between py-2" >
@@ -65,7 +67,7 @@ export function StepHeader({ currentStep }: { currentStep: OnboardingStep }) { })}
- Step {safeIndex + 1} of {total} + {t(($) => $.step_header.step_of, { current: safeIndex + 1, total })} ); diff --git a/packages/views/onboarding/onboarding-flow.tsx b/packages/views/onboarding/onboarding-flow.tsx index 2369b31132..460a79d104 100644 --- a/packages/views/onboarding/onboarding-flow.tsx +++ b/packages/views/onboarding/onboarding-flow.tsx @@ -24,6 +24,7 @@ import { StepRuntimeConnect } from "./steps/step-runtime-connect"; import { StepPlatformFork } from "./steps/step-platform-fork"; import { StepAgent } from "./steps/step-agent"; import { StepFirstIssue } from "./steps/step-first-issue"; +import { useT } from "../i18n"; const EMPTY_QUESTIONNAIRE: QuestionnaireAnswers = { team_size: null, @@ -55,6 +56,7 @@ export function OnboardingFlow({ onComplete: (workspace?: Workspace) => void; runtimeInstructions?: React.ReactNode; }) { + const { t } = useT("onboarding"); const user = useAuthStore((s) => s.user); if (!user) { throw new Error("OnboardingFlow requires an authenticated user"); @@ -114,7 +116,7 @@ export function OnboardingFlow({ await completeOnboarding("skip_existing"); } catch (err) { toast.error( - err instanceof Error ? err.message : "Failed to finish onboarding", + err instanceof Error ? err.message : t(($) => $.errors.skip_failed), ); return; } diff --git a/packages/views/onboarding/steps/step-welcome.tsx b/packages/views/onboarding/steps/step-welcome.tsx index d47013c55b..d986f98a87 100644 --- a/packages/views/onboarding/steps/step-welcome.tsx +++ b/packages/views/onboarding/steps/step-welcome.tsx @@ -11,6 +11,7 @@ import { STATUS_CONFIG } from "@multica/core/issues/config"; import type { IssueStatus } from "@multica/core/types"; import { StatusIcon } from "../../issues/components/status-icon"; import { ProviderLogo } from "../../runtimes/components/provider-logo"; +import { useT } from "../../i18n"; /** * Step 0 — the one-shot product intro shown on every onboarding @@ -45,6 +46,7 @@ export function StepWelcome({ onSkip?: () => void | Promise; isWeb?: boolean; }) { + const { t } = useT("onboarding"); // Tracks which button is mid-flight so we can show a per-button // spinner and disable both while one is in progress. const [pending, setPending] = useState<"next" | "skip" | null>(null); @@ -79,30 +81,25 @@ export function StepWelcome({
- Welcome to Multica + {t(($) => $.welcome.wordmark)}

- Your AI teammates, + {t(($) => $.welcome.headline_line1)}
- in one workspace. + {t(($) => $.welcome.headline_line2)}{" "} + {t(($) => $.welcome.headline_emphasis)}

- Assign them work like you'd assign a colleague — they - pick it up, update status, and comment when done. + {t(($) => $.welcome.lede)}

- {isWeb ? ( - <> - Desktop bundles the runtime — nothing to install. - Continue on web to connect your own CLI. - - ) : ( - "By the end, a real agent will be replying to your first issue." - )} + {isWeb + ? t(($) => $.welcome.lede_web) + : t(($) => $.welcome.lede_desktop)}

@@ -124,7 +121,7 @@ export function StepWelcome({ className={buttonVariants({ size: "lg" })} > - Download Desktop + {t(($) => $.welcome.download_desktop)} @@ -148,7 +145,7 @@ export function StepWelcome({ {pending === "next" && ( )} - Start exploring + {t(($) => $.welcome.start_exploring)} )} @@ -162,7 +159,7 @@ export function StepWelcome({ {pending === "skip" && ( )} - I've done this before + {t(($) => $.welcome.skip_existing)} )} @@ -180,8 +177,7 @@ export function StepWelcome({

- Every issue, every thread, every decision — shared by your team and - agents. + {t(($) => $.welcome.illustration_caption)}