Complete client UI i18n migration
Adds client-side i18n support, language settings, translated UI copy, and locale parity coverage.
This commit is contained in:
@@ -4,6 +4,7 @@ import { Pressable, Text, View } from "react-native";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { ChevronDown, ExternalLink, Globe, Play, SquareTerminal } from "lucide-react-native";
|
||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { WorkspaceDescriptor } from "@/stores/session-store";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import { useHostRuntimeSnapshot } from "@/runtime/host-runtime";
|
||||
@@ -169,6 +170,7 @@ function HostLinkChildren({ hovered, disabled, label }: HostLinkChildrenProps):
|
||||
}
|
||||
|
||||
function HostLinkRow({ label, url, scriptName, onOpenInBrowserTab }: HostLinkProps): ReactElement {
|
||||
const { t } = useTranslation();
|
||||
const disabled = !url;
|
||||
const closeMenu = useDropdownMenuClose();
|
||||
|
||||
@@ -192,7 +194,7 @@ function HostLinkRow({ label, url, scriptName, onOpenInBrowserTab }: HostLinkPro
|
||||
return (
|
||||
<Pressable
|
||||
accessibilityRole="link"
|
||||
accessibilityLabel={`Open ${scriptName} at ${label}`}
|
||||
accessibilityLabel={t("workspace.scripts.accessibility.openAt", { scriptName, label })}
|
||||
disabled={disabled}
|
||||
hitSlop={2}
|
||||
onPress={handlePress}
|
||||
@@ -204,10 +206,11 @@ function HostLinkRow({ label, url, scriptName, onOpenInBrowserTab }: HostLinkPro
|
||||
}
|
||||
|
||||
function ExitCodeBadge({ code }: { code: number }): ReactElement {
|
||||
const { t } = useTranslation();
|
||||
const exitTextStyle = code === 0 ? styles.exitBadgeText : exitBadgeTextErrorStyle;
|
||||
return (
|
||||
<View style={styles.exitBadge}>
|
||||
<Text style={exitTextStyle}>exit {code}</Text>
|
||||
<Text style={exitTextStyle}>{t("workspace.scripts.states.exitCode", { code })}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -257,6 +260,7 @@ function ScriptRow({
|
||||
onViewTerminal,
|
||||
onOpenUrlInBrowserTab,
|
||||
}: ScriptRowProps): ReactElement {
|
||||
const { t } = useTranslation();
|
||||
const isRunning = script.lifecycle === "running";
|
||||
const isService = (script.type ?? "service") === "service";
|
||||
const exitCode = script.exitCode ?? null;
|
||||
@@ -309,21 +313,25 @@ function ScriptRow({
|
||||
if (isRunning && liveTerminalId) {
|
||||
primaryAction = (
|
||||
<ScriptActionButton
|
||||
accessibilityLabel={`View ${script.scriptName} terminal`}
|
||||
accessibilityLabel={t("workspace.scripts.accessibility.viewTerminal", {
|
||||
scriptName: script.scriptName,
|
||||
})}
|
||||
testID={`workspace-scripts-view-${script.scriptName}`}
|
||||
icon="view"
|
||||
label="View"
|
||||
label={t("workspace.scripts.actions.view")}
|
||||
onPress={handleView}
|
||||
/>
|
||||
);
|
||||
} else if (!isRunning) {
|
||||
primaryAction = (
|
||||
<ScriptActionButton
|
||||
accessibilityLabel={`Run ${script.scriptName} script`}
|
||||
accessibilityLabel={t("workspace.scripts.accessibility.runScript", {
|
||||
scriptName: script.scriptName,
|
||||
})}
|
||||
testID={`workspace-scripts-start-${script.scriptName}`}
|
||||
disabled={isStartPending}
|
||||
icon="start"
|
||||
label="Run"
|
||||
label={t("workspace.scripts.actions.run")}
|
||||
onPress={handleRun}
|
||||
/>
|
||||
);
|
||||
@@ -332,7 +340,9 @@ function ScriptRow({
|
||||
return (
|
||||
<View
|
||||
testID={`workspace-scripts-item-${script.scriptName}`}
|
||||
accessibilityLabel={`${script.scriptName} script`}
|
||||
accessibilityLabel={t("workspace.scripts.accessibility.script", {
|
||||
scriptName: script.scriptName,
|
||||
})}
|
||||
style={styles.scriptItem}
|
||||
>
|
||||
<View style={styles.scriptHeader}>
|
||||
@@ -372,6 +382,7 @@ export function WorkspaceScriptsButton({
|
||||
hideLabels,
|
||||
presentation = "split",
|
||||
}: WorkspaceScriptsButtonProps): ReactElement | null {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const client = useSessionStore((state) => state.sessions[serverId]?.client ?? null);
|
||||
const activeConnection = useHostRuntimeSnapshot(serverId)?.activeConnection ?? null;
|
||||
@@ -380,7 +391,7 @@ export function WorkspaceScriptsButton({
|
||||
const startScriptMutation = useMutation({
|
||||
mutationFn: async (scriptName: string) => {
|
||||
if (!client) {
|
||||
throw new Error("Daemon client not available");
|
||||
throw new Error(t("common.errors.daemonClientUnavailable"));
|
||||
}
|
||||
const result = await client.startWorkspaceScript(workspaceId, scriptName);
|
||||
if (result.error) {
|
||||
@@ -389,9 +400,14 @@ export function WorkspaceScriptsButton({
|
||||
return result;
|
||||
},
|
||||
onError: (error, scriptName) => {
|
||||
toast.show(error instanceof Error ? error.message : `Failed to start ${scriptName}`, {
|
||||
variant: "error",
|
||||
});
|
||||
toast.show(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: t("workspace.scripts.states.startFailed", { scriptName }),
|
||||
{
|
||||
variant: "error",
|
||||
},
|
||||
);
|
||||
},
|
||||
onSuccess: (result) => {
|
||||
if (result.terminalId) {
|
||||
@@ -432,7 +448,7 @@ export function WorkspaceScriptsButton({
|
||||
testID="workspace-scripts-button"
|
||||
style={triggerStyle}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Workspace scripts"
|
||||
accessibilityLabel={t("workspace.scripts.accessibility.trigger")}
|
||||
>
|
||||
<View style={styles.splitButtonContent}>
|
||||
<ThemedPlay
|
||||
@@ -440,7 +456,9 @@ export function WorkspaceScriptsButton({
|
||||
uniProps={triggerPlayMapping}
|
||||
{...triggerPlayProps}
|
||||
/>
|
||||
{!hideLabels && <Text style={styles.splitButtonText}>Scripts</Text>}
|
||||
{!hideLabels && (
|
||||
<Text style={styles.splitButtonText}>{t("workspace.scripts.title")}</Text>
|
||||
)}
|
||||
{presentation === "split" ? (
|
||||
<ThemedChevronDown size={14} uniProps={mutedColorMapping} />
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user