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
@@ -1,4 +1,5 @@
import { memo, useCallback, useMemo, useState, type ReactElement } from "react";
import { useTranslation } from "react-i18next";
import { Text, View } from "react-native";
import { FileText, Layers, MessageSquare, Undo2 } from "lucide-react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
@@ -42,7 +43,16 @@ export const RewindMenu = memo(function RewindMenu({
testID = "rewind-menu",
}: RewindMenuProps) {
const { theme } = useUnistyles();
const items = useRewindCapabilities(capabilities);
const { t } = useTranslation();
const rewindLabels = useMemo(
() => ({
conversation: t("rewind.actions.conversation"),
files: t("rewind.actions.files"),
both: t("rewind.actions.both"),
}),
[t],
);
const items = useRewindCapabilities(capabilities, rewindLabels);
const [isOpen, setIsOpen] = useState(false);
const [pendingMode, setPendingMode] = useState<RewindMode | null>(null);
const isLocked = isPendingProp || pendingMode !== null;
@@ -79,10 +89,10 @@ export const RewindMenu = memo(function RewindMenu({
const tooltipContent = useMemo(
() => (
<TooltipContent side="top" align="center" offset={8}>
<Text style={styles.tooltipText}>Rewind to this message</Text>
<Text style={styles.tooltipText}>{t("rewind.tooltip")}</Text>
</TooltipContent>
),
[],
[t],
);
if (items.length === 0) {
@@ -95,7 +105,7 @@ export const RewindMenu = memo(function RewindMenu({
<TooltipTrigger asChild>
<View style={styles.triggerSlot} collapsable={false}>
<DropdownMenuTrigger
accessibilityLabel="Rewind to this message"
accessibilityLabel={t("rewind.tooltip")}
accessibilityRole="button"
disabled={isLocked}
style={triggerStyle}
@@ -114,7 +124,7 @@ export const RewindMenu = memo(function RewindMenu({
</Tooltip>
<DropdownMenuContent align="end" minWidth={220} side="bottom" testID={`${testID}-content`}>
<View style={styles.warningHeader}>
<Text style={styles.warningText}>This action cannot be undone</Text>
<Text style={styles.warningText}>{t("rewind.warning")}</Text>
</View>
<DropdownMenuSeparator />
{items.map((item) => (
@@ -1,4 +1,5 @@
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { useMutation } from "@tanstack/react-query";
import { useToast } from "@/contexts/toast-context";
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
@@ -25,11 +26,12 @@ export function useRewindAgentMutation(input: UseRewindAgentMutationInput): {
isPending: boolean;
} {
const toast = useToast();
const { t } = useTranslation();
const composerRestore = useRewindComposerRestore();
const { isPending, mutateAsync } = useMutation({
mutationFn: async ({ mode }: RewindAgentInput) => {
if (!input.client || !input.agentId || !input.messageId) {
throw new Error("Daemon client not available");
throw new Error(t("common.errors.daemonClientUnavailable"));
}
await input.client.rewindAgent(input.agentId, input.messageId, mode);
if (mode !== "files") {
@@ -59,7 +61,7 @@ export function useRewindAgentMutation(input: UseRewindAgentMutationInput): {
composerRestore?.restoreTextIfComposerEmpty(variables.rewoundText);
},
onError: (error) => {
toast.error(error instanceof Error ? error.message : "Failed to rewind agent");
toast.error(error instanceof Error ? error.message : t("rewind.errors.failed"));
},
});
@@ -33,4 +33,21 @@ describe("resolveRewindMenuItems", () => {
},
]);
});
test("uses caller-provided labels for available capabilities", () => {
expect(
resolveRewindMenuItems(
{
supportsRewindConversation: true,
supportsRewindFiles: true,
supportsRewindBoth: false,
},
{
conversation: "Conversation label",
files: "Files label",
both: "Both label",
},
).map((item) => item.label),
).toEqual(["Conversation label", "Files label"]);
});
});
@@ -9,6 +9,18 @@ export interface RewindMenuItem {
testID: string;
}
export interface RewindMenuLabels {
conversation: string;
files: string;
both: string;
}
const DEFAULT_REWIND_MENU_LABELS: RewindMenuLabels = {
conversation: "Rewind conversation",
files: "Rewind files",
both: "Rewind conversation and files",
};
export function resolveRewindMenuItems(
capabilities:
| Pick<
@@ -17,29 +29,31 @@ export function resolveRewindMenuItems(
>
| null
| undefined,
labelsInput?: Partial<RewindMenuLabels>,
): RewindMenuItem[] {
if (!capabilities) {
return [];
}
const labels = { ...DEFAULT_REWIND_MENU_LABELS, ...labelsInput };
const items: RewindMenuItem[] = [];
if (capabilities.supportsRewindConversation) {
items.push({
mode: "conversation",
label: "Rewind conversation",
label: labels.conversation,
testID: "rewind-menu-conversation",
});
}
if (capabilities.supportsRewindFiles) {
items.push({
mode: "files",
label: "Rewind files",
label: labels.files,
testID: "rewind-menu-files",
});
}
if (capabilities.supportsRewindBoth) {
items.push({
mode: "both",
label: "Rewind conversation and files",
label: labels.both,
testID: "rewind-menu-both",
});
}
@@ -48,6 +62,7 @@ export function resolveRewindMenuItems(
export function useRewindCapabilities(
capabilities: Parameters<typeof resolveRewindMenuItems>[0],
labels?: Partial<RewindMenuLabels>,
): RewindMenuItem[] {
return useMemo(() => resolveRewindMenuItems(capabilities), [capabilities]);
return useMemo(() => resolveRewindMenuItems(capabilities, labels), [capabilities, labels]);
}