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,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" };
|
||||
|
||||
Reference in New Issue
Block a user