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
+205 -88
View File
@@ -21,6 +21,8 @@ import { useRouter } from "expo-router";
import { useFocusEffect } from "@react-navigation/native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { useTranslation } from "react-i18next";
import type { TFunction } from "i18next";
import { Buffer } from "buffer";
import {
ArrowLeft,
@@ -92,6 +94,7 @@ import { resolveAppVersion } from "@/utils/app-version";
import { settingsStyles } from "@/styles/settings";
import { THINKING_TONE_NATIVE_PCM_BASE64 } from "@/utils/thinking-tone.native-pcm";
import { useVoiceAudioEngineOptional } from "@/contexts/voice-context";
import { LANGUAGE_OPTIONS, type AppLanguage } from "@/i18n/locales";
import {
HostConnectionsPage,
HostAgentsPage,
@@ -127,34 +130,44 @@ export type SettingsView =
interface SidebarSectionItem {
id: SettingsSectionSlug;
label: string;
labelKey: string;
icon: ComponentType<{ size: number; color: string }>;
desktopOnly?: boolean;
}
const SIDEBAR_SECTION_ITEMS: SidebarSectionItem[] = [
{ id: "general", label: "General", icon: Settings },
{ id: "daemon", label: "Daemon", icon: Server, desktopOnly: true },
{ id: "appearance", label: "Appearance", icon: Palette },
{ id: "shortcuts", label: "Shortcuts", icon: Keyboard, desktopOnly: true },
{ id: "integrations", label: "Integrations", icon: Puzzle, desktopOnly: true },
{ id: "permissions", label: "Permissions", icon: Shield, desktopOnly: true },
{ id: "diagnostics", label: "Diagnostics", icon: Stethoscope },
{ id: "about", label: "About", icon: Info },
{ id: "general", labelKey: "settings.sections.general", icon: Settings },
{ id: "daemon", labelKey: "settings.sections.daemon", icon: Server, desktopOnly: true },
{ id: "appearance", labelKey: "settings.sections.appearance", icon: Palette },
{ id: "shortcuts", labelKey: "settings.sections.shortcuts", icon: Keyboard, desktopOnly: true },
{
id: "integrations",
labelKey: "settings.sections.integrations",
icon: Puzzle,
desktopOnly: true,
},
{
id: "permissions",
labelKey: "settings.sections.permissions",
icon: Shield,
desktopOnly: true,
},
{ id: "diagnostics", labelKey: "settings.sections.diagnostics", icon: Stethoscope },
{ id: "about", labelKey: "settings.sections.about", icon: Info },
];
interface HostSectionItem {
id: HostSectionSlug;
label: string;
labelKey: string;
icon: ComponentType<{ size: number; color: string }>;
}
const HOST_SECTION_ITEMS: HostSectionItem[] = [
{ id: "connections", label: "Connections", icon: Network },
{ id: "agents", label: "Agents", icon: Bot },
{ id: "workspaces", label: "Workspaces", icon: FolderGit2 },
{ id: "providers", label: "Providers", icon: Boxes },
{ id: "host", label: "Host", icon: Server },
{ id: "connections", labelKey: "settings.hostSections.connections", icon: Network },
{ id: "agents", labelKey: "settings.hostSections.agents", icon: Bot },
{ id: "workspaces", labelKey: "settings.hostSections.workspaces", icon: FolderGit2 },
{ id: "providers", labelKey: "settings.hostSections.providers", icon: Boxes },
{ id: "host", labelKey: "settings.hostSections.host", icon: Server },
];
function renderHostSettingsContent(
@@ -197,21 +210,21 @@ function selectedSidebarItemStyle({ hovered }: PressableStateCallbackType & { ho
const ROW_WITH_BORDER_STYLE = [settingsStyles.row, settingsStyles.rowBorder];
const SEND_BEHAVIOR_OPTIONS = [
{ value: "interrupt" as const, label: "Interrupt" },
{ value: "queue" as const, label: "Queue" },
];
function getSendBehaviorOptions(t: TFunction) {
return [
{ value: "interrupt" as const, label: t("settings.general.defaultSend.options.interrupt") },
{ value: "queue" as const, label: t("settings.general.defaultSend.options.queue") },
];
}
const RELEASE_CHANNEL_OPTIONS = [
{ value: "stable" as const, label: "Stable" },
{ value: "beta" as const, label: "Beta" },
];
const SERVICE_URL_BEHAVIOR_LABELS: Record<ServiceUrlBehavior, string> = {
ask: "Ask",
"in-app": "In Paseo",
external: "External browser",
};
function getServiceUrlBehaviorLabel(t: TFunction, value: ServiceUrlBehavior): string {
const labels: Record<ServiceUrlBehavior, string> = {
ask: t("settings.general.serviceUrls.options.ask"),
"in-app": t("settings.general.serviceUrls.options.inApp"),
external: t("settings.general.serviceUrls.options.external"),
};
return labels[value];
}
const SERVICE_URL_BEHAVIOR_VALUES: ServiceUrlBehavior[] = ["ask", "in-app", "external"];
@@ -224,17 +237,20 @@ interface GeneralSectionProps {
isDesktopApp: boolean;
handleSendBehaviorChange: (behavior: SendBehavior) => void;
handleServiceUrlBehaviorChange: (behavior: ServiceUrlBehavior) => void;
handleLanguageChange: (language: AppLanguage) => void;
handleTerminalScrollbackLinesChange: (lines: number) => void;
}
interface ServiceUrlBehaviorMenuItemProps {
value: ServiceUrlBehavior;
label: string;
selected: boolean;
onChange: (value: ServiceUrlBehavior) => void;
}
function ServiceUrlBehaviorMenuItem({
value,
label,
selected,
onChange,
}: ServiceUrlBehaviorMenuItemProps) {
@@ -243,7 +259,28 @@ function ServiceUrlBehaviorMenuItem({
}, [onChange, value]);
return (
<DropdownMenuItem selected={selected} onSelect={handleSelect}>
{SERVICE_URL_BEHAVIOR_LABELS[value]}
{label}
</DropdownMenuItem>
);
}
interface LanguageMenuItemProps {
value: AppLanguage;
selected: boolean;
onChange: (value: AppLanguage) => void;
}
function LanguageMenuItem({ value, selected, onChange }: LanguageMenuItemProps) {
const { t } = useTranslation();
const handleSelect = useCallback(() => {
onChange(value);
}, [onChange, value]);
const option = LANGUAGE_OPTIONS.find((entry) => entry.value === value);
const label = option ? t(option.labelKey) : value;
return (
<DropdownMenuItem selected={selected} onSelect={handleSelect}>
{label}
</DropdownMenuItem>
);
}
@@ -253,10 +290,19 @@ function GeneralSection({
isDesktopApp,
handleSendBehaviorChange,
handleServiceUrlBehaviorChange,
handleLanguageChange,
handleTerminalScrollbackLinesChange,
}: GeneralSectionProps) {
const { theme } = useUnistyles();
const { t } = useTranslation();
const iconColor = theme.colors.foregroundMuted;
const sendBehaviorOptions = useMemo(() => getSendBehaviorOptions(t), [t]);
const selectedLanguageOption = LANGUAGE_OPTIONS.find(
(option) => option.value === settings.language,
);
const selectedLanguageLabel = selectedLanguageOption
? t(selectedLanguageOption.labelKey)
: settings.language;
const [terminalScrollbackValue, setTerminalScrollbackValue] = useState(
String(settings.terminalScrollbackLines),
);
@@ -283,32 +329,60 @@ function GeneralSection({
}, [settings.terminalScrollbackLines]);
return (
<SettingsSection title="General">
<SettingsSection title={t("settings.general.title")}>
<View style={settingsStyles.card}>
<View style={settingsStyles.row}>
<View style={settingsStyles.rowContent}>
<Text style={settingsStyles.rowTitle}>Default send</Text>
<Text style={settingsStyles.rowTitle}>{t("settings.general.defaultSend.label")}</Text>
<Text style={settingsStyles.rowHint}>
What happens when you press Enter while the agent is running
{t("settings.general.defaultSend.description")}
</Text>
</View>
<SegmentedControl
size="sm"
value={settings.sendBehavior}
onValueChange={handleSendBehaviorChange}
options={SEND_BEHAVIOR_OPTIONS}
options={sendBehaviorOptions}
/>
</View>
<View style={ROW_WITH_BORDER_STYLE}>
<View style={settingsStyles.rowContent}>
<Text style={settingsStyles.rowTitle}>{t("settings.general.language.label")}</Text>
<Text style={settingsStyles.rowHint}>{t("settings.general.language.description")}</Text>
</View>
<DropdownMenu>
<DropdownMenuTrigger
accessibilityRole="button"
accessibilityLabel={selectedLanguageLabel}
style={themeTriggerStyle}
>
<Text style={styles.themeTriggerText}>{selectedLanguageLabel}</Text>
<ChevronDown size={theme.iconSize.sm} color={iconColor} />
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" width={220}>
{LANGUAGE_OPTIONS.map((option) => (
<LanguageMenuItem
key={option.value}
value={option.value}
selected={settings.language === option.value}
onChange={handleLanguageChange}
/>
))}
</DropdownMenuContent>
</DropdownMenu>
</View>
{isDesktopApp ? (
<View style={ROW_WITH_BORDER_STYLE}>
<View style={settingsStyles.rowContent}>
<Text style={settingsStyles.rowTitle}>Service URLs</Text>
<Text style={settingsStyles.rowHint}>Where to open URLs from running scripts</Text>
<Text style={settingsStyles.rowTitle}>{t("settings.general.serviceUrls.label")}</Text>
<Text style={settingsStyles.rowHint}>
{t("settings.general.serviceUrls.description")}
</Text>
</View>
<DropdownMenu>
<DropdownMenuTrigger style={themeTriggerStyle}>
<Text style={styles.themeTriggerText}>
{SERVICE_URL_BEHAVIOR_LABELS[settings.serviceUrlBehavior]}
{getServiceUrlBehaviorLabel(t, settings.serviceUrlBehavior)}
</Text>
<ChevronDown size={theme.iconSize.sm} color={iconColor} />
</DropdownMenuTrigger>
@@ -317,6 +391,7 @@ function GeneralSection({
<ServiceUrlBehaviorMenuItem
key={value}
value={value}
label={getServiceUrlBehaviorLabel(t, value)}
selected={settings.serviceUrlBehavior === value}
onChange={handleServiceUrlBehaviorChange}
/>
@@ -327,8 +402,12 @@ function GeneralSection({
) : null}
<View style={ROW_WITH_BORDER_STYLE}>
<View style={settingsStyles.rowContent}>
<Text style={settingsStyles.rowTitle}>Terminal scrollback</Text>
<Text style={settingsStyles.rowHint}>Lines kept in the built-in terminal buffer</Text>
<Text style={settingsStyles.rowTitle}>
{t("settings.general.terminalScrollback.label")}
</Text>
<Text style={settingsStyles.rowHint}>
{t("settings.general.terminalScrollback.description")}
</Text>
</View>
<TextInput
value={terminalScrollbackValue}
@@ -339,7 +418,7 @@ function GeneralSection({
inputMode="numeric"
selectTextOnFocus
style={styles.terminalScrollbackInput}
accessibilityLabel="Terminal scrollback lines"
accessibilityLabel={t("settings.general.terminalScrollback.accessibilityLabel")}
/>
</View>
</View>
@@ -360,15 +439,16 @@ function DiagnosticsSection({
playbackTestResult,
handlePlaybackTest,
}: DiagnosticsSectionProps) {
const { t } = useTranslation();
const handlePlayPress = useCallback(() => {
void handlePlaybackTest();
}, [handlePlaybackTest]);
return (
<SettingsSection title="Diagnostics">
<SettingsSection title={t("settings.diagnostics.title")}>
<View style={settingsStyles.card}>
<View style={settingsStyles.row}>
<View style={settingsStyles.rowContent}>
<Text style={settingsStyles.rowTitle}>Test audio</Text>
<Text style={settingsStyles.rowTitle}>{t("settings.diagnostics.testAudio")}</Text>
{playbackTestResult ? (
<Text style={settingsStyles.rowHint}>{playbackTestResult}</Text>
) : null}
@@ -379,7 +459,9 @@ function DiagnosticsSection({
onPress={handlePlayPress}
disabled={!voiceAudioEngine || isPlaybackTestRunning}
>
{isPlaybackTestRunning ? "Playing..." : "Play test"}
{isPlaybackTestRunning
? t("settings.diagnostics.playing")
: t("settings.diagnostics.playTest")}
</Button>
</View>
</View>
@@ -394,14 +476,15 @@ interface AboutSectionProps {
}
function AboutSection({ appVersion, appVersionText, isDesktopApp }: AboutSectionProps) {
const { t } = useTranslation();
return (
<>
<SettingsSection title="About">
<SettingsSection title={t("settings.about.title")}>
<View style={settingsStyles.card}>
<View style={settingsStyles.row}>
<View style={settingsStyles.rowContent}>
<Text style={settingsStyles.rowTitle}>App version</Text>
<Text style={settingsStyles.rowHint}>This device</Text>
<Text style={settingsStyles.rowTitle}>{t("settings.about.appVersion")}</Text>
<Text style={settingsStyles.rowHint}>{t("settings.about.thisDevice")}</Text>
</View>
<Text style={styles.aboutValue}>{appVersionText}</Text>
</View>
@@ -423,12 +506,13 @@ function normalizeVersion(version: string | null | undefined): string | null {
}
function ConnectedHostsSection({ clientVersion }: { clientVersion: string | null }) {
const { t } = useTranslation();
const hosts = useHosts();
if (hosts.length === 0) {
return null;
}
return (
<SettingsSection title="Connected hosts">
<SettingsSection title={t("settings.about.connectedHosts")}>
<View style={settingsStyles.card}>
{hosts.map((host, index) => (
<HostVersionRow
@@ -452,6 +536,7 @@ function HostVersionRow({
showBorder: boolean;
clientVersion: string | null;
}) {
const { t } = useTranslation();
const isConnected = useHostRuntimeIsConnected(host.serverId);
const daemonVersion = useSessionStore(
(state) => state.sessions[host.serverId]?.serverInfo?.version ?? null,
@@ -469,7 +554,7 @@ function HostVersionRow({
let valueText: string;
if (!isConnected) {
valueText = "Offline";
valueText = t("settings.about.offline");
} else if (normalizedHost) {
valueText = formatVersionWithPrefix(normalizedHost);
} else {
@@ -488,7 +573,7 @@ function HostVersionRow({
{host.label}
</Text>
{isMismatch ? (
<Text style={settingsStyles.rowHint}>Version differs from this device</Text>
<Text style={settingsStyles.rowHint}>{t("settings.about.versionDiffers")}</Text>
) : null}
</View>
<Text style={valueStyle}>{valueText}</Text>
@@ -497,15 +582,21 @@ function HostVersionRow({
}
function getUpdateButtonLabel(
t: TFunction,
isInstalling: boolean,
latestVersion: string | null | undefined,
): string {
if (isInstalling) return "Installing...";
if (latestVersion) return `Update to ${formatVersionWithPrefix(latestVersion)}`;
return "Update";
if (isInstalling) return t("settings.about.updates.installing");
if (latestVersion) {
return t("settings.about.updates.updateTo", {
version: formatVersionWithPrefix(latestVersion),
});
}
return t("settings.about.updates.update");
}
function DesktopAppUpdateRow() {
const { t } = useTranslation();
const { settings, updateSettings } = useSettings();
const {
isDesktopApp,
@@ -541,6 +632,13 @@ function DesktopAppUpdateRow() {
},
[updateSettings],
);
const releaseChannelOptions = useMemo(
() => [
{ value: "stable" as const, label: t("settings.about.releaseChannel.stable") },
{ value: "beta" as const, label: t("settings.about.releaseChannel.beta") },
],
[t],
);
const handleInstallUpdate = useCallback(() => {
if (!isDesktopApp) {
@@ -548,10 +646,10 @@ function DesktopAppUpdateRow() {
}
void confirmDialog({
title: "Install desktop update",
message: "This updates Paseo on this computer",
confirmLabel: "Install update",
cancelLabel: "Cancel",
title: t("settings.about.updates.installTitle"),
message: t("settings.about.updates.installMessage"),
confirmLabel: t("settings.about.updates.installConfirm"),
cancelLabel: t("common.actions.cancel"),
})
.then((confirmed) => {
if (!confirmed) {
@@ -562,9 +660,12 @@ function DesktopAppUpdateRow() {
})
.catch((error) => {
console.error("[Settings] Failed to open app update confirmation", error);
Alert.alert("Error", "Unable to open the update confirmation dialog.");
Alert.alert(
t("settings.about.updates.alertTitle"),
t("settings.about.updates.alertMessage"),
);
});
}, [installUpdate, isDesktopApp]);
}, [installUpdate, isDesktopApp, t]);
if (!isDesktopApp) {
return null;
@@ -574,25 +675,27 @@ function DesktopAppUpdateRow() {
<>
<View style={ROW_WITH_BORDER_STYLE}>
<View style={settingsStyles.rowContent}>
<Text style={settingsStyles.rowTitle}>Release channel</Text>
<Text style={settingsStyles.rowTitle}>{t("settings.about.releaseChannel.label")}</Text>
<Text style={settingsStyles.rowHint}>
Switch to Beta to get updates sooner and help shape them
{t("settings.about.releaseChannel.description")}
</Text>
</View>
<SegmentedControl
size="sm"
value={settings.releaseChannel}
onValueChange={handleReleaseChannelChange}
options={RELEASE_CHANNEL_OPTIONS}
options={releaseChannelOptions}
/>
</View>
<View style={ROW_WITH_BORDER_STYLE}>
<View style={settingsStyles.rowContent}>
<Text style={settingsStyles.rowTitle}>App updates</Text>
<Text style={settingsStyles.rowTitle}>{t("settings.about.updates.label")}</Text>
<Text style={settingsStyles.rowHint}>{statusText}</Text>
{availableUpdate?.latestVersion ? (
<Text style={settingsStyles.rowHint}>
Ready to install: {formatVersionWithPrefix(availableUpdate.latestVersion)}
{t("settings.about.updates.readyToInstall", {
version: formatVersionWithPrefix(availableUpdate.latestVersion),
})}
</Text>
) : null}
{errorMessage ? <Text style={styles.aboutErrorText}>{errorMessage}</Text> : null}
@@ -604,7 +707,7 @@ function DesktopAppUpdateRow() {
onPress={handleCheckForUpdates}
disabled={isChecking || isInstalling}
>
{isChecking ? "Checking..." : "Check"}
{isChecking ? t("settings.about.updates.checking") : t("settings.about.updates.check")}
</Button>
<Button
variant="default"
@@ -612,7 +715,7 @@ function DesktopAppUpdateRow() {
onPress={handleInstallUpdate}
disabled={isChecking || isInstalling || !availableUpdate}
>
{getUpdateButtonLabel(isInstalling, availableUpdate?.latestVersion)}
{getUpdateButtonLabel(t, isInstalling, availableUpdate?.latestVersion)}
</Button>
</View>
</View>
@@ -760,6 +863,7 @@ interface SidebarProjectsButtonProps {
function SidebarProjectsButton({ isSelected, onSelect }: SidebarProjectsButtonProps) {
const { theme } = useUnistyles();
const { t } = useTranslation();
const accessibilityState = useMemo(() => ({ selected: isSelected }), [isSelected]);
const labelStyle = useMemo(
() => [sidebarStyles.label, isSelected && { color: theme.colors.foreground }],
@@ -778,7 +882,7 @@ function SidebarProjectsButton({ isSelected, onSelect }: SidebarProjectsButtonPr
color={isSelected ? theme.colors.foreground : theme.colors.foregroundMuted}
/>
<Text style={labelStyle} numberOfLines={1}>
Projects
{t("settings.projects")}
</Text>
</Pressable>
);
@@ -805,6 +909,7 @@ function HostPickerOption({
onPress,
}: HostPickerOptionProps) {
const { theme } = useUnistyles();
const { t } = useTranslation();
const leadingSlot = useMemo(
() => <Server size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />,
[theme.iconSize.sm, theme.colors.foregroundMuted],
@@ -815,10 +920,10 @@ function HostPickerOption({
() =>
isLocal ? (
<Text style={sidebarStyles.localMarker} testID="settings-host-local-marker">
Local
{t("settings.hostPicker.local")}
</Text>
) : undefined,
[isLocal],
[isLocal, t],
);
return (
<ComboboxItem
@@ -835,13 +940,14 @@ function HostPickerOption({
function AddHostOption({ active, onPress }: { active: boolean; onPress: () => void }) {
const { theme } = useUnistyles();
const { t } = useTranslation();
const leadingSlot = useMemo(
() => <Plus size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />,
[theme.iconSize.sm, theme.colors.foregroundMuted],
);
return (
<ComboboxItem
label="Add host"
label={t("settings.addHost")}
leadingSlot={leadingSlot}
active={active}
onPress={onPress}
@@ -872,6 +978,7 @@ function HostPicker({
onAddHost,
}: HostPickerProps) {
const { theme } = useUnistyles();
const { t } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
const triggerRef = useRef<View | null>(null);
const activeHost =
@@ -879,8 +986,8 @@ function HostPicker({
const options = useMemo<ComboboxOption[]>(() => {
const hostOptions = sortedHosts.map((host) => ({ id: host.serverId, label: host.label }));
return [...hostOptions, { id: ADD_HOST_OPTION_ID, label: "Add host" }];
}, [sortedHosts]);
return [...hostOptions, { id: ADD_HOST_OPTION_ID, label: t("settings.addHost") }];
}, [sortedHosts, t]);
const handleSelect = useCallback(
(id: string) => {
@@ -938,12 +1045,12 @@ function HostPicker({
style={triggerStyle}
onPress={handleOpen}
accessibilityRole="button"
accessibilityLabel="Switch host"
accessibilityLabel={t("settings.hostPicker.switchHost")}
testID="settings-host-picker"
>
<Monitor size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
<Text style={sidebarStyles.pickerTriggerLabel} numberOfLines={1}>
{activeHost?.label ?? "Host"}
{activeHost?.label ?? t("settings.groups.host")}
</Text>
<ChevronDown size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
</Pressable>
@@ -953,7 +1060,7 @@ function HostPicker({
onSelect={handleSelect}
renderOption={renderOption}
searchable={false}
title="Switch host"
title={t("settings.hostPicker.switchHost")}
desktopMinWidth={240}
open={isOpen}
onOpenChange={setIsOpen}
@@ -987,6 +1094,7 @@ function SettingsSidebar({
layout,
}: SettingsSidebarProps) {
const { theme } = useUnistyles();
const { t } = useTranslation();
const hosts = useHosts();
const localServerId = useLocalDaemonServerId();
const sortedHosts = useSortedHosts(hosts, localServerId);
@@ -1012,12 +1120,12 @@ function SettingsSidebar({
const sidebarBody = (
<>
<View style={sidebarStyles.list}>
<Text style={sidebarStyles.groupLabel}>App</Text>
<Text style={sidebarStyles.groupLabel}>{t("settings.groups.app")}</Text>
{items.map((item) => (
<Fragment key={item.id}>
<SidebarSectionButton
itemId={item.id}
label={item.label}
label={t(item.labelKey)}
icon={item.icon}
isSelected={selectedSectionId === item.id}
onSelect={onSelectSection}
@@ -1031,7 +1139,7 @@ function SettingsSidebar({
<SidebarSeparator />
{hasHosts ? (
<View style={sidebarStyles.list}>
<Text style={sidebarStyles.groupLabel}>Host</Text>
<Text style={sidebarStyles.groupLabel}>{t("settings.groups.host")}</Text>
<HostPicker
activeServerId={activeHostServerId}
sortedHosts={sortedHosts}
@@ -1043,7 +1151,7 @@ function SettingsSidebar({
<SidebarHostSectionButton
key={item.id}
itemId={item.id}
label={item.label}
label={t(item.labelKey)}
icon={item.icon}
isSelected={selectedHostSection === item.id}
onSelect={onSelectHostSection}
@@ -1054,14 +1162,14 @@ function SettingsSidebar({
<View style={sidebarStyles.list}>
<Pressable
accessibilityRole="button"
accessibilityLabel="Add host"
accessibilityLabel={t("settings.addHost")}
onPress={onAddHost}
testID="settings-add-host"
style={sidebarItemStyle}
>
<Plus size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
<Text style={sidebarStyles.label} numberOfLines={1}>
Add host
{t("settings.addHost")}
</Text>
</Pressable>
</View>
@@ -1078,7 +1186,7 @@ function SettingsSidebar({
{padding.top > 0 ? <View style={paddingTopStyle} /> : null}
<SidebarHeaderRow
icon={ArrowLeft}
label="Back"
label={t("settings.backToWorkspace")}
onPress={onBackToWorkspace}
testID="settings-back-to-workspace"
/>
@@ -1105,6 +1213,7 @@ export interface SettingsScreenProps {
export default function SettingsScreen({ view }: SettingsScreenProps) {
const router = useRouter();
const { theme } = useUnistyles();
const { t } = useTranslation();
const voiceAudioEngine = useVoiceAudioEngineOptional();
const { settings, isLoading: settingsLoading, updateSettings } = useAppSettings();
const [isAddHostMethodVisible, setIsAddHostMethodVisible] = useState(false);
@@ -1167,6 +1276,13 @@ export default function SettingsScreen({ view }: SettingsScreenProps) {
[updateSettings],
);
const handleLanguageChange = useCallback(
(language: AppLanguage) => {
void updateSettings({ language });
},
[updateSettings],
);
const handleTerminalScrollbackLinesChange = useCallback(
(terminalScrollbackLines: number) => {
void updateSettings({ terminalScrollbackLines });
@@ -1197,11 +1313,11 @@ export default function SettingsScreen({ view }: SettingsScreenProps) {
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error("[Settings] Playback test failed", error);
setPlaybackTestResult(`Playback failed: ${message}`);
setPlaybackTestResult(t("settings.diagnostics.playbackFailed", { message }));
} finally {
setIsPlaybackTestRunning(false);
}
}, [isPlaybackTestRunning, voiceAudioEngine]);
}, [isPlaybackTestRunning, t, voiceAudioEngine]);
const closeAddConnectionFlow = useCallback(() => {
setIsAddHostMethodVisible(false);
@@ -1341,15 +1457,15 @@ export default function SettingsScreen({ view }: SettingsScreenProps) {
if (view.kind === "host") {
const item = HOST_SECTION_ITEMS.find((s) => s.id === view.section);
if (!item) return null;
return { title: item.label, Icon: item.icon };
return { title: t(item.labelKey), Icon: item.icon };
}
if (view.kind === "section") {
const item = SIDEBAR_SECTION_ITEMS.find((s) => s.id === view.section);
if (!item) return null;
return { title: item.label, Icon: item.icon };
return { title: t(item.labelKey), Icon: item.icon };
}
if (view.kind === "project" || view.kind === "projects") {
return { title: "Projects", Icon: FolderGit2 };
return { title: t("settings.projects"), Icon: FolderGit2 };
}
return null;
})();
@@ -1373,6 +1489,7 @@ export default function SettingsScreen({ view }: SettingsScreenProps) {
isDesktopApp={isDesktopApp}
handleSendBehaviorChange={handleSendBehaviorChange}
handleServiceUrlBehaviorChange={handleServiceUrlBehaviorChange}
handleLanguageChange={handleLanguageChange}
handleTerminalScrollbackLinesChange={handleTerminalScrollbackLinesChange}
/>
);
@@ -1411,7 +1528,7 @@ export default function SettingsScreen({ view }: SettingsScreenProps) {
if (settingsLoading) {
return (
<View style={styles.loadingContainer}>
<Text style={styles.loadingText}>Loading settings...</Text>
<Text style={styles.loadingText}>{t("settings.loading")}</Text>
</View>
);
}
@@ -1444,7 +1561,7 @@ export default function SettingsScreen({ view }: SettingsScreenProps) {
if (isCompactLayout && view.kind === "root") {
return (
<View style={styles.container}>
<BackHeader title="Settings" onBack={handleBackToWorkspace} />
<BackHeader title={t("settings.title")} onBack={handleBackToWorkspace} />
<ScrollView style={scrollViewStyle} contentContainerStyle={insetBottomStyle}>
<SettingsSidebar
view={view}