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
+10 -5
View File
@@ -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");