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,6 +1,7 @@
import React, { type ReactElement, useCallback, useMemo, useState } from "react";
import { ActivityIndicator, Alert, Text, View } from "react-native";
import * as Clipboard from "expo-clipboard";
import { useTranslation } from "react-i18next";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { settingsStyles } from "@/styles/settings";
import { SettingsSection } from "@/screens/settings/settings-section";
@@ -41,6 +42,7 @@ function useKeepRunningAfterQuitToggle(args: {
}
function useDaemonCliStatusModal() {
const { t } = useTranslation();
const [cliStatusOutput, setCliStatusOutput] = useState<string | null>(null);
const [isCliStatusModalOpen, setIsCliStatusModalOpen] = useState(false);
const [isLoadingCliStatus, setIsLoadingCliStatus] = useState(false);
@@ -52,12 +54,12 @@ function useDaemonCliStatusModal() {
setIsCliStatusModalOpen(true);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setCliStatusOutput(`Failed to fetch daemon status: ${message}`);
setCliStatusOutput(t("desktop.daemon.fullStatus.fetchFailed", { message }));
setIsCliStatusModalOpen(true);
} finally {
setIsLoadingCliStatus(false);
}
}, []);
}, [t]);
const handleCopyCliStatus = useCallback(() => {
if (!cliStatusOutput) {
@@ -65,13 +67,13 @@ function useDaemonCliStatusModal() {
}
void Clipboard.setStringAsync(cliStatusOutput)
.then(() => {
Alert.alert("Copied", "Status copied to clipboard.");
Alert.alert(t("common.states.copied"), t("desktop.daemon.fullStatus.copied"));
return;
})
.catch((error) => {
console.error("[Settings] Failed to copy daemon status", error);
});
}, [cliStatusOutput]);
}, [cliStatusOutput, t]);
const handleCloseCliStatusModal = useCallback(() => setIsCliStatusModalOpen(false), []);
@@ -86,6 +88,7 @@ function useDaemonCliStatusModal() {
}
function useDaemonLogsModal(daemonLogs: { logPath?: string } | null) {
const { t } = useTranslation();
const [isLogsModalOpen, setIsLogsModalOpen] = useState(false);
const handleCopyLogPath = useCallback(() => {
@@ -96,14 +99,14 @@ function useDaemonLogsModal(daemonLogs: { logPath?: string } | null) {
void Clipboard.setStringAsync(logPath)
.then(() => {
Alert.alert("Copied", "Log path copied.");
Alert.alert(t("common.states.copied"), t("desktop.daemon.logs.copied"));
return;
})
.catch((error) => {
console.error("[Settings] Failed to copy log path", error);
Alert.alert("Error", "Unable to copy log path.");
Alert.alert(t("common.errors.error"), t("desktop.daemon.logs.copyFailed"));
});
}, [daemonLogs?.logPath]);
}, [daemonLogs?.logPath, t]);
const handleOpenLogs = useCallback(() => {
if (!daemonLogs) {
@@ -124,18 +127,23 @@ interface DaemonLogsModalProps {
}
function DaemonLogsModal({ visible, onClose, daemonLogs }: DaemonLogsModalProps) {
const { t } = useTranslation();
const header = useMemo<SheetHeader>(() => ({ title: t("desktop.daemon.logs.modalTitle") }), [t]);
return (
<AdaptiveModalSheet
visible={visible}
onClose={onClose}
header={DAEMON_LOGS_HEADER}
header={header}
testID="managed-daemon-logs-dialog"
snapPoints={LOGS_MODAL_SNAP_POINTS}
>
<View style={styles.modalBody}>
<Text style={settingsStyles.rowHint}>{daemonLogs?.logPath ?? "Log path unavailable"}</Text>
<Text style={settingsStyles.rowHint}>
{daemonLogs?.logPath ?? t("desktop.daemon.logs.unavailable")}
</Text>
<Text style={styles.logOutput} selectable dataSet={CODE_SURFACE_DATASET}>
{daemonLogs?.contents?.length ? daemonLogs.contents : "(log file is empty)"}
{daemonLogs?.contents?.length ? daemonLogs.contents : t("desktop.daemon.logs.empty")}
</Text>
</View>
</AdaptiveModalSheet>
@@ -155,11 +163,17 @@ function DaemonCliStatusModal({
cliStatusOutput,
onCopy,
}: DaemonCliStatusModalProps) {
const { t } = useTranslation();
const header = useMemo<SheetHeader>(
() => ({ title: t("desktop.daemon.fullStatus.modalTitle") }),
[t],
);
return (
<AdaptiveModalSheet
visible={visible}
onClose={onClose}
header={DAEMON_STATUS_HEADER}
header={header}
testID="daemon-cli-status-dialog"
snapPoints={CLI_STATUS_MODAL_SNAP_POINTS}
>
@@ -169,10 +183,10 @@ function DaemonCliStatusModal({
</Text>
<View style={styles.modalActions}>
<Button variant="outline" size="sm" onPress={onClose}>
Close
{t("common.actions.close")}
</Button>
<Button size="sm" onPress={onCopy}>
Copy
{t("common.actions.copy")}
</Button>
</View>
</View>
@@ -200,6 +214,7 @@ interface DaemonInfoCardProps {
}
function DaemonInfoCard(props: DaemonInfoCardProps) {
const { t } = useTranslation();
const {
daemonStatusStateText,
daemonStatusDetailText,
@@ -223,8 +238,8 @@ function DaemonInfoCard(props: DaemonInfoCardProps) {
<View style={settingsStyles.card}>
<View style={settingsStyles.row}>
<View style={settingsStyles.rowContent}>
<Text style={settingsStyles.rowTitle}>Status</Text>
<Text style={settingsStyles.rowHint}>Only the built-in desktop daemon is shown here</Text>
<Text style={settingsStyles.rowTitle}>{t("desktop.daemon.status.title")}</Text>
<Text style={settingsStyles.rowHint}>{t("desktop.daemon.status.builtInOnly")}</Text>
</View>
<View style={styles.statusValueGroup}>
<Text style={styles.valueText}>{daemonStatusStateText}</Text>
@@ -233,39 +248,39 @@ function DaemonInfoCard(props: DaemonInfoCardProps) {
</View>
<View style={ROW_WITH_BORDER_STYLE}>
<View style={settingsStyles.rowContent}>
<Text style={settingsStyles.rowTitle}>Manage built-in daemon</Text>
<Text style={settingsStyles.rowHint}>Let Paseo start and stop the built-in daemon</Text>
<Text style={settingsStyles.rowTitle}>{t("desktop.daemon.management.title")}</Text>
<Text style={settingsStyles.rowHint}>{t("desktop.daemon.management.hint")}</Text>
</View>
<Switch
value={!isDaemonManagementPaused}
onValueChange={handleToggleDaemonManagement}
disabled={isUpdatingDaemonManagement}
accessibilityLabel="Manage built-in daemon"
accessibilityLabel={t("desktop.daemon.management.title")}
/>
</View>
<View style={ROW_WITH_BORDER_STYLE}>
<View style={settingsStyles.rowContent}>
<Text style={settingsStyles.rowTitle}>Keep daemon running after quit</Text>
<Text style={settingsStyles.rowHint}>Daemon keeps running when you quit Paseo</Text>
<Text style={settingsStyles.rowTitle}>{t("desktop.daemon.keepRunning.title")}</Text>
<Text style={settingsStyles.rowHint}>{t("desktop.daemon.keepRunning.hint")}</Text>
</View>
<Switch
value={keepRunningAfterQuit}
onValueChange={handleToggleKeepRunningAfterQuit}
disabled={isUpdatingKeepRunningAfterQuit}
accessibilityLabel="Keep daemon running after quit"
accessibilityLabel={t("desktop.daemon.keepRunning.title")}
/>
</View>
<View style={ROW_WITH_BORDER_STYLE}>
<View style={settingsStyles.rowContent}>
<Text style={settingsStyles.rowTitle}>Log file</Text>
<Text style={settingsStyles.rowTitle}>{t("desktop.daemon.logs.title")}</Text>
<Text style={settingsStyles.rowHint}>
{daemonLogs?.logPath ?? "Log path unavailable"}
{daemonLogs?.logPath ?? t("desktop.daemon.logs.unavailable")}
</Text>
</View>
<View style={styles.actionGroup}>
{daemonLogs?.logPath ? (
<Button variant="outline" size="sm" leftIcon={copyIcon} onPress={handleCopyLogPath}>
Copy path
{t("desktop.daemon.logs.copyPath")}
</Button>
) : null}
<Button
@@ -275,16 +290,14 @@ function DaemonInfoCard(props: DaemonInfoCardProps) {
onPress={handleOpenLogs}
disabled={!daemonLogs}
>
Open logs
{t("desktop.daemon.logs.open")}
</Button>
</View>
</View>
<View style={ROW_WITH_BORDER_STYLE}>
<View style={settingsStyles.rowContent}>
<Text style={settingsStyles.rowTitle}>Full status</Text>
<Text style={settingsStyles.rowHint}>
Runs `paseo daemon status` and shows the output
</Text>
<Text style={settingsStyles.rowTitle}>{t("desktop.daemon.fullStatus.title")}</Text>
<Text style={settingsStyles.rowHint}>{t("desktop.daemon.fullStatus.hint")}</Text>
</View>
<Button
variant="outline"
@@ -293,7 +306,7 @@ function DaemonInfoCard(props: DaemonInfoCardProps) {
onPress={handleRunCliStatus}
disabled={isLoadingCliStatus}
>
{isLoadingCliStatus ? "Loading..." : "View status"}
{isLoadingCliStatus ? t("common.states.loading") : t("desktop.daemon.fullStatus.view")}
</Button>
</View>
</View>
@@ -301,6 +314,7 @@ function DaemonInfoCard(props: DaemonInfoCardProps) {
}
export function LocalDaemonSection() {
const { t } = useTranslation();
const { theme } = useUnistyles();
const showSection = shouldUseDesktopDaemon();
const appVersion = resolveAppVersion();
@@ -318,8 +332,13 @@ export function LocalDaemonSection() {
const daemonVersionMismatch = isVersionMismatch(appVersion, daemonVersion);
const daemonStatusStateText =
statusError ?? (daemonStatus?.status === "running" ? daemonStatus.status : "not running");
const daemonStatusDetailText = `PID ${daemonStatus?.pid ? daemonStatus.pid : "—"}`;
statusError ??
(daemonStatus?.status === "running"
? t("desktop.daemon.status.running")
: t("desktop.daemon.status.notRunning"));
const daemonStatusDetailText = t("desktop.daemon.status.pid", {
pid: daemonStatus?.pid ? daemonStatus.pid : "—",
});
const isDaemonManagementPaused = !daemonSettings.manageBuiltInDaemon;
const { isUpdating: isUpdatingDaemonManagement, toggle: handleToggleDaemonManagement } =
@@ -382,12 +401,12 @@ export function LocalDaemonSection() {
textStyle={settingsStyles.sectionHeaderLinkText}
style={settingsStyles.sectionHeaderLink}
onPress={handleOpenAdvancedSettings}
accessibilityLabel="Open advanced daemon settings"
accessibilityLabel={t("desktop.daemon.openAdvancedSettings")}
>
Advanced settings
{t("desktop.daemon.advancedSettings")}
</Button>
),
[advancedSettingsIcon, handleOpenAdvancedSettings],
[advancedSettingsIcon, handleOpenAdvancedSettings, t],
);
if (!showSection) {
@@ -396,7 +415,7 @@ export function LocalDaemonSection() {
return (
<SettingsSection
title="Daemon"
title={t("desktop.daemon.title")}
trailing={advancedSettingsButton}
testID="host-page-daemon-lifecycle-card"
>
@@ -427,11 +446,7 @@ export function LocalDaemonSection() {
{daemonVersionMismatch ? (
<View style={styles.warningCard}>
<Text style={styles.warningText}>
{
"App and daemon versions don't match. Update both to the same version for the best experience."
}
</Text>
<Text style={styles.warningText}>{t("desktop.daemon.versionMismatch")}</Text>
</View>
) : null}
</>
@@ -513,5 +528,3 @@ const LOADING_CARD_STYLE = [settingsStyles.card, styles.loadingCard];
const ROW_WITH_BORDER_STYLE = [settingsStyles.row, settingsStyles.rowBorder];
const LOGS_MODAL_SNAP_POINTS = ["70%", "92%"];
const CLI_STATUS_MODAL_SNAP_POINTS = ["60%", "85%"];
const DAEMON_LOGS_HEADER: SheetHeader = { title: "Daemon logs" };
const DAEMON_STATUS_HEADER: SheetHeader = { title: "Daemon status" };