Complete client UI i18n migration

Adds client-side i18n support, language settings, translated UI copy, and locale parity coverage.
This commit is contained in:
chyendongnhanh338
2026-06-11 14:15:35 +08:00
committed by GitHub
parent 41cf070fd8
commit 08ebe5e7e1
199 changed files with 16426 additions and 1821 deletions
@@ -1,4 +1,5 @@
import { describe, expect, it } from "vitest";
import { i18n } from "@/i18n/i18next";
import {
createDesktopAppUpdater,
formatStatusText,
@@ -292,4 +293,32 @@ describe("formatStatusText", () => {
}),
).toBe("Restart now");
});
it("uses the active app language for local status wrappers", async () => {
await i18n.changeLanguage("zh-CN");
try {
expect(
formatStatusText({
status: "checking",
availableUpdate: null,
installMessage: null,
lastCheckedAt: null,
formatVersion,
formatLastCheckedAt,
}),
).toBe("正在检查 app 更新...");
expect(
formatStatusText({
status: "available",
availableUpdate: buildFakeCheckResult({ latestVersion: "1.2.3" }),
installMessage: null,
lastCheckedAt: null,
formatVersion,
formatLastCheckedAt,
}),
).toBe("更新已就绪:v1.2.3");
} finally {
await i18n.changeLanguage("en");
}
});
});
@@ -4,6 +4,7 @@ import type {
DesktopAppUpdateInstallResult,
DesktopReleaseChannel,
} from "@/desktop/updates/desktop-updates";
import { i18n } from "@/i18n/i18next";
export type DesktopAppUpdateStatus =
| "idle"
@@ -119,40 +120,44 @@ export function formatStatusText(input: {
} = input;
if (status === "checking") {
return "Checking for app updates...";
return i18n.t("desktop.updates.status.checking");
}
if (status === "installing") {
return "Installing app update...";
return i18n.t("desktop.updates.status.installing");
}
if (status === "up-to-date") {
if (lastCheckedAt != null) {
return `Up to date. Last checked at ${formatLastCheckedAt(lastCheckedAt)}.`;
return i18n.t("desktop.updates.status.upToDateWithLastChecked", {
time: formatLastCheckedAt(lastCheckedAt),
});
}
return "Up to date.";
return i18n.t("desktop.updates.status.upToDate");
}
if (status === "pending") {
return "We'll let you know when the update is ready.";
return i18n.t("desktop.updates.status.pending");
}
if (status === "available") {
if (availableUpdate?.latestVersion) {
return `Update ready: ${formatVersion(availableUpdate.latestVersion)}`;
return i18n.t("desktop.updates.status.availableWithVersion", {
version: formatVersion(availableUpdate.latestVersion),
});
}
return "An app update is ready to install.";
return i18n.t("desktop.updates.status.available");
}
if (status === "installed") {
return installMessage ?? "App update installed. Restart required.";
return installMessage ?? i18n.t("desktop.updates.status.installed");
}
if (status === "error") {
return "Failed to update app.";
return i18n.t("desktop.updates.status.failed");
}
return "Update status has not been checked yet.";
return i18n.t("desktop.updates.status.idle");
}
export function createDesktopAppUpdater(deps: DesktopAppUpdaterDeps): DesktopAppUpdater {
@@ -264,7 +269,7 @@ export function createDesktopAppUpdater(deps: DesktopAppUpdaterDeps): DesktopApp
const message = getErrorMessage(error);
deps.reportInstallError?.({
error,
message: "Unable to install the desktop app update.",
message: i18n.t("desktop.updates.installError"),
logLabel: "[DesktopUpdater] Failed to install app update",
});
commit({
@@ -1,6 +1,7 @@
import { isElectronRuntime } from "@/desktop/host";
import { invokeDesktopCommand } from "@/desktop/electron/invoke";
import { isWeb } from "@/constants/platform";
import { i18n } from "@/i18n/i18next";
export interface DesktopAppUpdateCheckResult {
hasUpdate: boolean;
@@ -136,7 +137,7 @@ export async function installDesktopAppUpdate({
return {
installed: result.installed === true,
version: toStringOrNull(result.version),
message: toStringOrNull(result.message) ?? "Update completed.",
message: toStringOrNull(result.message) ?? i18n.t("desktop.updates.status.installed"),
};
}
@@ -1,4 +1,5 @@
import { describe, expect, it } from "vitest";
import { i18n } from "@/i18n/i18next";
import {
resolveUpdateCalloutDescriptor,
type ResolveUpdateCalloutInput,
@@ -104,4 +105,19 @@ describe("resolveUpdateCalloutDescriptor", () => {
?.dismissalKey,
).toBe("desktop-update:available:1.2.4");
});
it("uses the active app language for local callout chrome", async () => {
await i18n.changeLanguage("zh-CN");
try {
const descriptor = resolveUpdateCalloutDescriptor(input());
expect(descriptor?.title).toBe("有可用更新");
expect(descriptor?.actions).toEqual([
{ role: "changelog", label: "更新内容" },
{ role: "install", label: "安装并重启", variant: "primary", disabled: false },
]);
} finally {
await i18n.changeLanguage("en");
}
});
});
@@ -1,4 +1,5 @@
import type { DesktopAppUpdateStatus } from "@/desktop/updates/use-desktop-app-updater";
import { i18n } from "@/i18n/i18next";
export type UpdateCalloutBody =
| { kind: "available"; versionLabel: string | null }
@@ -57,23 +58,30 @@ export function resolveUpdateCalloutDescriptor(
let title: string;
let body: UpdateCalloutBody;
if (isInstalling) {
title = "Installing update";
title = i18n.t("desktop.updates.callout.installingTitle");
body = { kind: "installing" };
} else if (isError) {
title = "Update failed";
body = { kind: "error", message: input.errorMessage ?? "Something went wrong." };
title = i18n.t("desktop.updates.callout.failedTitle");
body = {
kind: "error",
message: input.errorMessage ?? i18n.t("desktop.updates.callout.genericError"),
};
} else {
title = "Update available";
title = i18n.t("desktop.updates.callout.availableTitle");
body = { kind: "available", versionLabel: formatVersionLabel(latestVersion) };
}
const actions: UpdateCalloutActionDescriptor[] = [{ role: "changelog", label: "What's new" }];
const actions: UpdateCalloutActionDescriptor[] = [
{ role: "changelog", label: i18n.t("desktop.updates.callout.whatsNew") },
];
if (isError) {
actions.push({ role: "retry", label: "Retry", variant: "primary" });
actions.push({ role: "retry", label: i18n.t("common.actions.retry"), variant: "primary" });
} else {
actions.push({
role: "install",
label: isInstalling ? "Installing..." : "Install & restart",
label: isInstalling
? i18n.t("desktop.updates.callout.installingAction")
: i18n.t("desktop.updates.callout.installAndRestart"),
variant: "primary",
disabled: isInstalling,
});
@@ -1,4 +1,5 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { SidebarCalloutDescriptionText } from "@/components/sidebar-callout";
import { getIsElectronMac } from "@/constants/platform";
import { useSidebarCallouts } from "@/contexts/sidebar-callout-context";
@@ -12,20 +13,19 @@ import { openExternalUrl } from "@/utils/open-external-url";
const FALLBACK_DOWNLOAD_URL = "https://paseo.sh/download";
function RosettaCalloutDescription() {
function RosettaCalloutDescription({ t }: { t: ReturnType<typeof useTranslation>["t"] }) {
return (
<>
<SidebarCalloutDescriptionText>
You&apos;re running the Intel build of Paseo under Rosetta on Apple Silicon.
</SidebarCalloutDescriptionText>
<SidebarCalloutDescriptionText>
This causes high CPU usage. Download the Apple Silicon build to fix it.
{t("desktop.rosetta.runningIntel")}
</SidebarCalloutDescriptionText>
<SidebarCalloutDescriptionText>{t("desktop.rosetta.highCpu")}</SidebarCalloutDescriptionText>
</>
);
}
export function RosettaCalloutSource() {
const { t } = useTranslation();
const callouts = useSidebarCallouts();
const [runtimeInfo, setRuntimeInfo] = useState<DesktopRuntimeInfo | null>(null);
const isElectronMac = getIsElectronMac();
@@ -67,20 +67,20 @@ export function RosettaCalloutSource() {
return callouts.show({
id: "desktop-rosetta-warning",
priority: 300,
title: "Download the Apple Silicon build",
description: <RosettaCalloutDescription />,
title: t("desktop.rosetta.title"),
description: <RosettaCalloutDescription t={t} />,
variant: "error",
dismissible: false,
actions: [
{
label: "Download",
label: t("desktop.rosetta.download"),
onPress: openDownload,
variant: "primary",
},
],
testID: "rosetta-callout",
});
}, [callouts, isElectronMac, openDownload, runtimeInfo]);
}, [callouts, isElectronMac, openDownload, runtimeInfo, t]);
return null;
}
@@ -1,5 +1,6 @@
import { Gift } from "lucide-react-native";
import { type ReactNode, useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import { useUnistyles } from "react-native-unistyles";
import {
type SidebarCalloutAction,
@@ -18,10 +19,10 @@ import { openExternalUrl } from "@/utils/open-external-url";
const CHECK_INTERVAL_MS = 30 * 60 * 1000;
const CHANGELOG_URL = "https://paseo.sh/changelog";
function renderBody(body: UpdateCalloutBody): ReactNode {
if (body.kind === "installing") return "Installing and restarting...";
function renderBody(body: UpdateCalloutBody, t: ReturnType<typeof useTranslation>["t"]): ReactNode {
if (body.kind === "installing") return t("desktop.updates.callout.installingDescription");
if (body.kind === "error") return body.message;
return <UpdateAvailableDescription versionLabel={body.versionLabel ?? undefined} />;
return <UpdateAvailableDescription versionLabel={body.versionLabel ?? undefined} t={t} />;
}
function materializeActions(
@@ -37,6 +38,7 @@ function materializeActions(
}
export function UpdateCalloutSource() {
const { t } = useTranslation();
const callouts = useSidebarCallouts();
const { theme } = useUnistyles();
const {
@@ -90,7 +92,7 @@ export function UpdateCalloutSource() {
dismissalKey: descriptor.dismissalKey,
priority: descriptor.priority,
title: descriptor.title,
description: renderBody(descriptor.body),
description: renderBody(descriptor.body, t),
icon: descriptor.showGiftIcon ? (
<Gift size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
) : undefined,
@@ -114,21 +116,28 @@ export function UpdateCalloutSource() {
status,
theme.colors.foregroundMuted,
theme.iconSize.sm,
t,
]);
return null;
}
function UpdateAvailableDescription({ versionLabel }: { versionLabel?: string }) {
function UpdateAvailableDescription({
versionLabel,
t,
}: {
versionLabel?: string;
t: ReturnType<typeof useTranslation>["t"];
}) {
return (
<>
<SidebarCalloutDescriptionText>
{versionLabel
? `${versionLabel} is ready to install.`
: "A new version is ready to install."}
? t("desktop.updates.callout.versionReady", { version: versionLabel })
: t("desktop.updates.callout.newVersionReady")}
</SidebarCalloutDescriptionText>
<SidebarCalloutDescriptionText>
Upgrading the app will stop running agents and close terminal sessions.
{t("desktop.updates.callout.restartWarning")}
</SidebarCalloutDescriptionText>
</>
);