Complete client UI i18n migration
Adds client-side i18n support, language settings, translated UI copy, and locale parity coverage.
This commit is contained in:
@@ -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'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>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user