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,4 +1,5 @@
|
||||
import { useCallback, useMemo, useReducer } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { ComposerAttachment } from "@/attachments/types";
|
||||
import { splitComposerAttachmentsForSubmit } from "@/composer/attachments/submit";
|
||||
import { useCreateFlowStore } from "@/stores/create-flow-store";
|
||||
@@ -105,6 +106,7 @@ export function useDraftAgentCreateFlow<TDraftAgent, TCreateResult>({
|
||||
onCreateSuccess,
|
||||
onCreateError,
|
||||
}: UseDraftAgentCreateFlowOptions<TDraftAgent, TCreateResult>) {
|
||||
const { t } = useTranslation();
|
||||
const [machine, dispatch] = useReducer(
|
||||
reducer,
|
||||
initialAttempt,
|
||||
@@ -163,7 +165,7 @@ export function useDraftAgentCreateFlow<TDraftAgent, TCreateResult>({
|
||||
async ({ attempt, cwd }: { attempt: CreateAttempt; cwd: string }) => {
|
||||
const pendingServerId = getPendingServerId();
|
||||
if (!pendingServerId) {
|
||||
const error = new Error("No host selected");
|
||||
const error = new Error(t("composer.errors.noHostSelected"));
|
||||
dispatch({ type: "DRAFT_SET_ERROR", message: error.message });
|
||||
throw error;
|
||||
}
|
||||
@@ -204,7 +206,8 @@ export function useDraftAgentCreateFlow<TDraftAgent, TCreateResult>({
|
||||
|
||||
await onCreateSuccess({ result: createResult.result, attempt });
|
||||
} catch (error) {
|
||||
const resolved = error instanceof Error ? error : new Error("Failed to create agent");
|
||||
const resolved =
|
||||
error instanceof Error ? error : new Error(t("composer.errors.failedToCreateAgent"));
|
||||
dispatch({ type: "CREATE_FAILED", message: resolved.message });
|
||||
markPendingCreateLifecycle({ draftId, lifecycle: "abandoned" });
|
||||
clearPendingCreateAttempt({ draftId });
|
||||
@@ -222,6 +225,7 @@ export function useDraftAgentCreateFlow<TDraftAgent, TCreateResult>({
|
||||
onBeforeSubmit,
|
||||
onCreateError,
|
||||
onCreateSuccess,
|
||||
t,
|
||||
updatePendingAgentId,
|
||||
],
|
||||
);
|
||||
@@ -229,7 +233,7 @@ export function useDraftAgentCreateFlow<TDraftAgent, TCreateResult>({
|
||||
const handleCreateFromInput = useCallback(
|
||||
async ({ text, attachments, cwd }: SubmitContext) => {
|
||||
if (isSubmitting) {
|
||||
throw new Error("Already loading");
|
||||
throw new Error(t("composer.errors.alreadyLoading"));
|
||||
}
|
||||
|
||||
dispatch({ type: "DRAFT_SET_ERROR", message: "" });
|
||||
@@ -238,7 +242,7 @@ export function useDraftAgentCreateFlow<TDraftAgent, TCreateResult>({
|
||||
|
||||
const trimmedPrompt = text.trim();
|
||||
if (!trimmedPrompt && !allowEmptyText) {
|
||||
const error = new Error("Initial prompt is required");
|
||||
const error = new Error(t("composer.errors.initialPromptRequired"));
|
||||
dispatch({ type: "DRAFT_SET_ERROR", message: error.message });
|
||||
throw error;
|
||||
}
|
||||
@@ -256,7 +260,7 @@ export function useDraftAgentCreateFlow<TDraftAgent, TCreateResult>({
|
||||
|
||||
const pendingServerId = getPendingServerId();
|
||||
if (!pendingServerId) {
|
||||
const error = new Error("No host selected");
|
||||
const error = new Error(t("composer.errors.noHostSelected"));
|
||||
dispatch({ type: "DRAFT_SET_ERROR", message: error.message });
|
||||
throw error;
|
||||
}
|
||||
@@ -294,6 +298,7 @@ export function useDraftAgentCreateFlow<TDraftAgent, TCreateResult>({
|
||||
onCreateStart,
|
||||
runCreateAttempt,
|
||||
setPendingCreateAttempt,
|
||||
t,
|
||||
validateBeforeSubmit,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||
import { Import as ImportIcon } from "lucide-react-native";
|
||||
import type { Theme } from "@/styles/theme";
|
||||
@@ -13,6 +14,7 @@ interface ComposerImportPillProps {
|
||||
}
|
||||
|
||||
export function ComposerImportPill({ onPress, disabled = false }: ComposerImportPillProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const handleHoverIn = useCallback(() => setIsHovered(true), []);
|
||||
const handleHoverOut = useCallback(() => setIsHovered(false), []);
|
||||
@@ -22,7 +24,7 @@ export function ComposerImportPill({ onPress, disabled = false }: ComposerImport
|
||||
<Pressable
|
||||
testID="composer-import-agent-pill"
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Import session"
|
||||
accessibilityLabel={t("importSession.title")}
|
||||
onPress={onPress}
|
||||
disabled={disabled}
|
||||
onHoverIn={handleHoverIn}
|
||||
@@ -31,7 +33,7 @@ export function ComposerImportPill({ onPress, disabled = false }: ComposerImport
|
||||
>
|
||||
<ThemedImportIcon size={14} uniProps={iconColorMapping} />
|
||||
<Text style={styles.label} numberOfLines={1}>
|
||||
Import session
|
||||
{t("importSession.title")}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { Keyboard, ScrollView, Text, View } from "react-native";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ReanimatedAnimated from "react-native-reanimated";
|
||||
import { StyleSheet } from "react-native-unistyles";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
@@ -130,6 +131,8 @@ async function submitDraftCreateRequest(input: {
|
||||
effectiveThinkingOptionId: string | null;
|
||||
featureValues: Record<string, unknown> | undefined;
|
||||
};
|
||||
hostDisconnectedMessage: string;
|
||||
selectModelMessage: string;
|
||||
}): Promise<{ agentId: string | null; result: AgentSnapshotPayload }> {
|
||||
const {
|
||||
attempt,
|
||||
@@ -146,12 +149,12 @@ async function submitDraftCreateRequest(input: {
|
||||
invariant(workspaceDirectory, "Workspace directory is required");
|
||||
invariant(workspaceExecutionAuthority, "Workspace authority is required");
|
||||
if (!client) {
|
||||
throw new Error("Host is not connected");
|
||||
throw new Error(input.hostDisconnectedMessage);
|
||||
}
|
||||
|
||||
const provider = autoSubmitConfig?.provider ?? composerState.selectedProvider;
|
||||
if (!provider) {
|
||||
throw new Error("Select a model");
|
||||
throw new Error(input.selectModelMessage);
|
||||
}
|
||||
const modeIdOverride = resolveDraftModeIdOverride({
|
||||
autoSubmitConfig,
|
||||
@@ -199,6 +202,7 @@ function buildDraftAgentSnapshot(input: {
|
||||
selectedProvider: string | null;
|
||||
agentControls: { features?: Agent["features"] };
|
||||
};
|
||||
selectModelMessage: string;
|
||||
}): Agent {
|
||||
const { attempt, serverId, tabId, workspaceDirectory, autoSubmitConfig, composerState } = input;
|
||||
invariant(workspaceDirectory, "Workspace directory is required");
|
||||
@@ -213,7 +217,7 @@ function buildDraftAgentSnapshot(input: {
|
||||
});
|
||||
const provider = autoSubmitConfig?.provider ?? composerState.selectedProvider;
|
||||
if (!provider) {
|
||||
throw new Error("Select a model");
|
||||
throw new Error(input.selectModelMessage);
|
||||
}
|
||||
return {
|
||||
serverId,
|
||||
@@ -309,6 +313,7 @@ export function WorkspaceDraftAgentTab({
|
||||
onOpenWorkspaceFile,
|
||||
onOpenImportSheet,
|
||||
}: WorkspaceDraftAgentTabProps) {
|
||||
const { t } = useTranslation();
|
||||
const insets = useSafeAreaInsets();
|
||||
const client = useHostRuntimeClient(serverId);
|
||||
const isConnected = useHostRuntimeIsConnected(serverId);
|
||||
@@ -456,6 +461,7 @@ export function WorkspaceDraftAgentTab({
|
||||
workspaceDirectory: draftWorkingDirectory,
|
||||
autoSubmitConfig,
|
||||
composerState,
|
||||
selectModelMessage: t("workspaceSetup.errors.selectModel"),
|
||||
}),
|
||||
createRequest: async ({ attempt, text, images, attachments }) =>
|
||||
submitDraftCreateRequest({
|
||||
@@ -468,6 +474,8 @@ export function WorkspaceDraftAgentTab({
|
||||
workspaceExecutionAuthority,
|
||||
autoSubmitConfig,
|
||||
composerState,
|
||||
hostDisconnectedMessage: t("workspace.terminal.hostDisconnected"),
|
||||
selectModelMessage: t("workspaceSetup.errors.selectModel"),
|
||||
}),
|
||||
onCreateSuccess: ({ result }) => {
|
||||
clearDraftInput("sent");
|
||||
|
||||
Reference in New Issue
Block a user