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,5 +1,6 @@
import { describe, expect, it, vi } from "vitest";
import type { DesktopHostBridge } from "@/desktop/host";
import { i18n } from "@/i18n/i18next";
import {
createDesktopPermissions,
type DesktopPermissionEnvironment,
@@ -180,4 +181,28 @@ describe("desktop-permissions", () => {
expect(result.state).toBe("denied");
});
it("uses the active app language for local status details", async () => {
await i18n.changeLanguage("zh-CN");
try {
const permissions = createDesktopPermissions(
fakeEnvironment({
notification: { permission: "granted" },
navigator: {
permissions: {
query: vi.fn(async () => ({ state: "prompt" })),
},
mediaDevices: { getUserMedia: vi.fn() },
},
}),
);
const snapshot = await permissions.getDesktopPermissionSnapshot();
expect(snapshot.notifications.detail).toBe("系统已允许通知。");
expect(snapshot.microphone.detail).toBe("麦克风权限尚未授予。");
} finally {
await i18n.changeLanguage("en");
}
});
});
@@ -1,5 +1,6 @@
import { type DesktopHostBridge, getDesktopHost } from "@/desktop/host";
import { isNative, isWeb } from "@/constants/platform";
import { i18n } from "@/i18n/i18next";
export type DesktopPermissionKind = "notifications" | "microphone";
@@ -97,24 +98,26 @@ function mapNotificationPermissionString(permission: string): DesktopPermissionS
if (permission === "granted") {
return status({
state: "granted",
detail: "Notifications are allowed by the OS.",
detail: i18n.t("desktop.permissions.notifications.allowed"),
});
}
if (permission === "denied") {
return status({
state: "denied",
detail: "Notifications are denied in system settings.",
detail: i18n.t("desktop.permissions.notifications.denied"),
});
}
if (permission === "default") {
return status({
state: "prompt",
detail: "Notifications have not been granted yet.",
detail: i18n.t("desktop.permissions.notifications.notGranted"),
});
}
return status({
state: "unknown",
detail: `Unexpected notification permission state: ${permission}`,
detail: i18n.t("desktop.permissions.notifications.unexpectedState", {
state: permission,
}),
});
}
@@ -127,7 +130,7 @@ export function createDesktopPermissions(env: DesktopPermissionEnvironment): Des
if (!env.isWeb) {
return status({
state: "unavailable",
detail: "Desktop notification status is only available on web runtime.",
detail: i18n.t("desktop.permissions.notifications.webOnly"),
});
}
@@ -138,8 +141,8 @@ export function createDesktopPermissions(env: DesktopPermissionEnvironment): Des
return status({
state: supported ? "granted" : "unavailable",
detail: supported
? "Desktop notifications are supported."
: "Desktop notifications are not supported on this platform.",
? i18n.t("desktop.permissions.notifications.supported")
: i18n.t("desktop.permissions.notifications.unsupported"),
});
} catch {
// Fall through to web API check
@@ -153,7 +156,7 @@ export function createDesktopPermissions(env: DesktopPermissionEnvironment): Des
return status({
state: "unavailable",
detail: "Web Notification API is unavailable in this environment.",
detail: i18n.t("desktop.permissions.notifications.apiUnavailable"),
});
}
@@ -161,7 +164,7 @@ export function createDesktopPermissions(env: DesktopPermissionEnvironment): Des
if (!env.isWeb) {
return status({
state: "unavailable",
detail: "Desktop microphone status is only available on web runtime.",
detail: i18n.t("desktop.permissions.microphone.webOnly"),
});
}
@@ -169,7 +172,7 @@ export function createDesktopPermissions(env: DesktopPermissionEnvironment): Des
if (!webNavigator) {
return status({
state: "unavailable",
detail: "Navigator is unavailable in this environment.",
detail: i18n.t("desktop.permissions.microphone.navigatorUnavailable"),
});
}
@@ -180,36 +183,39 @@ export function createDesktopPermissions(env: DesktopPermissionEnvironment): Des
if (result?.state === "granted") {
return status({
state: "granted",
detail: "Microphone access is granted.",
detail: i18n.t("desktop.permissions.microphone.granted"),
});
}
if (result?.state === "denied") {
return status({
state: "denied",
detail: "Microphone access is denied in system settings.",
detail: i18n.t("desktop.permissions.microphone.denied"),
});
}
if (result?.state === "prompt") {
return status({
state: "prompt",
detail: "Microphone permission has not been granted yet.",
detail: i18n.t("desktop.permissions.microphone.notGranted"),
});
}
return status({
state: "unknown",
detail: `Unexpected microphone permission state: ${result?.state ?? "unknown"}`,
detail: i18n.t("desktop.permissions.microphone.unexpectedState", {
state: result?.state ?? "unknown",
}),
});
} catch (error) {
if (isPermissionsQueryRuntimeUnsupported(error)) {
return status({
state: "unknown",
detail:
"Microphone status API is unavailable in this runtime. Use Request to check access.",
detail: i18n.t("desktop.permissions.microphone.statusApiUnavailable"),
});
}
return status({
state: "unknown",
detail: `Failed to query microphone status: ${getErrorMessage(error)}`,
detail: i18n.t("desktop.permissions.microphone.queryFailed", {
message: getErrorMessage(error),
}),
});
}
}
@@ -217,13 +223,13 @@ export function createDesktopPermissions(env: DesktopPermissionEnvironment): Des
if (typeof webNavigator.mediaDevices?.getUserMedia !== "function") {
return status({
state: "unavailable",
detail: "Microphone capture is unavailable in this environment.",
detail: i18n.t("desktop.permissions.microphone.captureUnavailable"),
});
}
return status({
state: "unknown",
detail: "Permission status API is unavailable. Use Request to check access.",
detail: i18n.t("desktop.permissions.microphone.permissionApiUnavailable"),
});
}
@@ -231,7 +237,7 @@ export function createDesktopPermissions(env: DesktopPermissionEnvironment): Des
if (!env.isWeb) {
return status({
state: "unavailable",
detail: "Desktop notification requests are only available on web runtime.",
detail: i18n.t("desktop.permissions.notifications.requestsWebOnly"),
});
}
@@ -246,14 +252,16 @@ export function createDesktopPermissions(env: DesktopPermissionEnvironment): Des
} catch (error) {
return status({
state: "unknown",
detail: `Failed to request notification permission: ${getErrorMessage(error)}`,
detail: i18n.t("desktop.permissions.notifications.requestFailed", {
message: getErrorMessage(error),
}),
});
}
}
return status({
state: "unavailable",
detail: "Web Notification API requestPermission() is unavailable.",
detail: i18n.t("desktop.permissions.notifications.requestUnavailable"),
});
}
@@ -261,7 +269,7 @@ export function createDesktopPermissions(env: DesktopPermissionEnvironment): Des
if (!env.isWeb) {
return status({
state: "unavailable",
detail: "Desktop microphone requests are only available on web runtime.",
detail: i18n.t("desktop.permissions.microphone.requestsWebOnly"),
});
}
@@ -269,7 +277,7 @@ export function createDesktopPermissions(env: DesktopPermissionEnvironment): Des
if (!webNavigator || typeof webNavigator.mediaDevices?.getUserMedia !== "function") {
return status({
state: "unavailable",
detail: "Microphone capture API is unavailable in this environment.",
detail: i18n.t("desktop.permissions.microphone.captureApiUnavailable"),
});
}
@@ -287,18 +295,20 @@ export function createDesktopPermissions(env: DesktopPermissionEnvironment): Des
if (errorName === "NotAllowedError" || errorName === "PermissionDeniedError") {
return status({
state: "denied",
detail: "Microphone permission was denied by the user or system.",
detail: i18n.t("desktop.permissions.microphone.requestDenied"),
});
}
if (errorName === "NotFoundError" || errorName === "DevicesNotFoundError") {
return status({
state: "unavailable",
detail: "No microphone device was found.",
detail: i18n.t("desktop.permissions.microphone.noDevice"),
});
}
return status({
state: "unknown",
detail: `Failed to request microphone permission: ${getErrorMessage(error)}`,
detail: i18n.t("desktop.permissions.microphone.requestFailed", {
message: getErrorMessage(error),
}),
});
}
}
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import {
getDesktopPermissionSnapshot,
requestDesktopPermission,
@@ -20,17 +21,8 @@ export interface UseDesktopPermissionsReturn {
sendTestNotification: () => Promise<void>;
}
const EMPTY_NOTIFICATION_STATUS = {
state: "unknown" as const,
detail: "Notification status has not been checked yet.",
};
const EMPTY_MICROPHONE_STATUS = {
state: "unknown" as const,
detail: "Microphone status has not been checked yet.",
};
export function useDesktopPermissions(): UseDesktopPermissionsReturn {
const { t } = useTranslation();
const isDesktopApp = shouldShowDesktopPermissionSection();
const isMountedRef = useRef(true);
const [snapshot, setSnapshot] = useState<DesktopPermissionSnapshot | null>(null);
@@ -83,8 +75,14 @@ export function useDesktopPermissions(): UseDesktopPermissionsReturn {
setSnapshot((previous) => {
const base: DesktopPermissionSnapshot = previous ?? {
checkedAt: Date.now(),
notifications: EMPTY_NOTIFICATION_STATUS,
microphone: EMPTY_MICROPHONE_STATUS,
notifications: {
state: "unknown",
detail: t("desktop.permissions.empty.notifications"),
},
microphone: {
state: "unknown",
detail: t("desktop.permissions.empty.microphone"),
},
};
if (kind === "notifications") {
@@ -110,7 +108,7 @@ export function useDesktopPermissions(): UseDesktopPermissionsReturn {
await refreshPermissions();
}
},
[isDesktopApp, refreshPermissions],
[isDesktopApp, refreshPermissions, t],
);
const [testNotificationError, setTestNotificationError] = useState<string | null>(null);
@@ -124,22 +122,20 @@ export function useDesktopPermissions(): UseDesktopPermissionsReturn {
setTestNotificationError(null);
try {
const sent = await sendOsNotification({
title: "Paseo notification test",
body: "If you can see this, desktop notifications work.",
title: t("desktop.permissions.testNotification.title"),
body: t("desktop.permissions.testNotification.body"),
});
if (!sent) {
setTestNotificationError(
"Notification was not delivered. Check System Settings > Notifications.",
);
setTestNotificationError(t("desktop.permissions.testNotification.notDelivered"));
}
} catch {
setTestNotificationError("Failed to send notification.");
setTestNotificationError(t("desktop.permissions.testNotification.failed"));
} finally {
if (isMountedRef.current) {
setIsSendingTestNotification(false);
}
}
}, [isDesktopApp]);
}, [isDesktopApp, t]);
useEffect(() => {
if (!isDesktopApp) {