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,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) {
|
||||
|
||||
Reference in New Issue
Block a user