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,6 +10,7 @@ import {
import { Pressable, Text, TextInput, View } from "react-native";
import { ArrowLeft, ArrowRight, MousePointer2, PencilRuler, RotateCw } from "lucide-react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { useTranslation } from "react-i18next";
import {
buildWorkspaceAttachmentScopeKey,
useWorkspaceAttachments,
@@ -55,7 +56,7 @@ function truncateText(value: string, maxLength: number): string {
return value.length > maxLength ? `${value.slice(0, maxLength).trim()}...` : value;
}
function getWebviewLoadErrorMessage(event: Event): string | null {
function getWebviewLoadErrorMessage(event: Event, failedToLoadLabel: string): string | null {
const details = event as Event & {
errorCode?: unknown;
errorDescription?: unknown;
@@ -69,7 +70,7 @@ function getWebviewLoadErrorMessage(event: Event): string | null {
const description =
typeof details.errorDescription === "string" && details.errorDescription.trim()
? details.errorDescription.trim()
: "Failed to load page";
: failedToLoadLabel;
const url =
typeof details.validatedURL === "string" && details.validatedURL.trim()
? details.validatedURL.trim()
@@ -78,7 +79,7 @@ function getWebviewLoadErrorMessage(event: Event): string | null {
return url ? `${description}: ${url}` : description;
}
function getLoadUrlRejectionMessage(error: unknown): string | null {
function getLoadUrlRejectionMessage(error: unknown, failedToLoadLabel: string): string | null {
if (error instanceof Error && error.message.trim()) {
if (error.message.includes("ERR_ABORTED") || error.message.includes("ERR_BLOCKED_BY_CLIENT")) {
return null;
@@ -91,18 +92,21 @@ function getLoadUrlRejectionMessage(error: unknown): string | null {
}
return error.trim();
}
return "Failed to load page";
return failedToLoadLabel;
}
function getUnsafeNavigationMessage(url: string): string | null {
function getUnsafeNavigationMessage(
url: string,
labels: { invalidUrl: string; unsupportedProtocol: (protocol: string) => string },
): string | null {
try {
const parsed = new URL(url);
if (ALLOWED_BROWSER_PROTOCOLS.has(parsed.protocol) || parsed.href === "about:blank") {
return null;
}
return `Blocked unsupported browser URL: ${parsed.protocol}`;
return labels.unsupportedProtocol(parsed.protocol);
} catch {
return "Invalid browser URL";
return labels.invalidUrl;
}
}
@@ -283,6 +287,7 @@ export function BrowserPane({
onFocusPane?: () => void;
}) {
const { theme } = useUnistyles();
const { t } = useTranslation();
const browser = useBrowserStore((state) => state.browsersById[browserId] ?? null);
const updateBrowser = useBrowserStore((state) => state.updateBrowser);
const webviewRef = useRef<ElectronWebview | null>(null);
@@ -327,6 +332,17 @@ export function BrowserPane({
() => [styles.metaError, { color: theme.colors.palette.red[500] }],
[theme.colors.palette.red],
);
const browserErrorLabels = useMemo(
() => ({
failedToLoad: t("workspace.browser.errors.failedToLoad"),
invalidUrl: t("workspace.browser.errors.invalidUrl"),
unsupportedProtocol: (protocol: string) =>
t("workspace.browser.errors.unsupportedProtocol", { protocol }),
}),
[t],
);
const browserErrorLabelsRef = useRef(browserErrorLabels);
browserErrorLabelsRef.current = browserErrorLabels;
useEffect(() => {
const nextUrl = browser?.url ?? "https://example.com";
@@ -384,7 +400,10 @@ export function BrowserPane({
host.replaceChildren();
const initialUnsafeNavigationMessage = getUnsafeNavigationMessage(initialUrlRef.current);
const initialUnsafeNavigationMessage = getUnsafeNavigationMessage(
initialUrlRef.current,
browserErrorLabelsRef.current,
);
const webview = document.createElement("webview") as ElectronWebview;
webviewRef.current = webview;
webview.setAttribute("partition", `persist:paseo-browser-${browserId}`);
@@ -459,7 +478,7 @@ export function BrowserPane({
updateBrowserRef.current(browserIdRef.current, { faviconUrl: favicons[0] ?? null });
};
const handleLoadFailed = (event: Event) => {
const message = getWebviewLoadErrorMessage(event);
const message = getWebviewLoadErrorMessage(event, browserErrorLabelsRef.current.failedToLoad);
if (!message) {
return;
}
@@ -519,43 +538,46 @@ export function BrowserPane({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [browserId, onFocusPane]);
const navigate = useCallback((nextUrl: string) => {
const normalizedUrl = normalizeWorkspaceBrowserUrl(nextUrl);
const webview = webviewRef.current;
const unsafeNavigationMessage = getUnsafeNavigationMessage(normalizedUrl);
const previousUrl = browserRef.current?.url ?? initialUrlRef.current;
pendingNavigationUrlRef.current = unsafeNavigationMessage ? null : normalizedUrl;
updateBrowserRef.current(browserIdRef.current, {
url: normalizedUrl,
isLoading: unsafeNavigationMessage === null,
...(normalizedUrl !== previousUrl ? { faviconUrl: null } : {}),
lastError: null,
});
setDraftUrl((current) => (current === normalizedUrl ? current : normalizedUrl));
if (unsafeNavigationMessage) {
const navigate = useCallback(
(nextUrl: string) => {
const normalizedUrl = normalizeWorkspaceBrowserUrl(nextUrl);
const webview = webviewRef.current;
const unsafeNavigationMessage = getUnsafeNavigationMessage(normalizedUrl, browserErrorLabels);
const previousUrl = browserRef.current?.url ?? initialUrlRef.current;
pendingNavigationUrlRef.current = unsafeNavigationMessage ? null : normalizedUrl;
updateBrowserRef.current(browserIdRef.current, {
isLoading: false,
lastError: unsafeNavigationMessage,
url: normalizedUrl,
isLoading: unsafeNavigationMessage === null,
...(normalizedUrl !== previousUrl ? { faviconUrl: null } : {}),
lastError: null,
});
return;
}
if (webview?.loadURL) {
void webview.loadURL(normalizedUrl).catch((error: unknown) => {
const message = getLoadUrlRejectionMessage(error);
if (!message) {
return;
}
setDraftUrl((current) => (current === normalizedUrl ? current : normalizedUrl));
if (unsafeNavigationMessage) {
updateBrowserRef.current(browserIdRef.current, {
isLoading: false,
lastError: message,
lastError: unsafeNavigationMessage,
});
});
return;
}
if (webview) {
webview.setAttribute("src", normalizedUrl);
}
}, []);
return;
}
if (webview?.loadURL) {
void webview.loadURL(normalizedUrl).catch((error: unknown) => {
const message = getLoadUrlRejectionMessage(error, browserErrorLabels.failedToLoad);
if (!message) {
return;
}
updateBrowserRef.current(browserIdRef.current, {
isLoading: false,
lastError: message,
});
});
return;
}
if (webview) {
webview.setAttribute("src", normalizedUrl);
}
},
[browserErrorLabels],
);
const handleBack = useCallback(() => {
webviewRef.current?.goBack?.();
@@ -929,10 +951,8 @@ export function BrowserPane({
if (!isElectronRuntime()) {
return (
<View style={styles.unavailableState}>
<Text style={titleStyle}>Browser is desktop-only</Text>
<Text style={subtitleStyle}>
Open this workspace in Electron to use the built-in browser.
</Text>
<Text style={titleStyle}>{t("workspace.browser.unavailable.title")}</Text>
<Text style={subtitleStyle}>{t("workspace.browser.unavailable.subtitle")}</Text>
</View>
);
}
@@ -943,7 +963,7 @@ export function BrowserPane({
<View style={styles.chromeLeft}>
<Pressable
accessibilityRole="button"
accessibilityLabel="Back"
accessibilityLabel={t("workspace.browser.controls.back")}
disabled={!browser?.canGoBack}
onPress={handleBack}
style={backIconButtonStyle}
@@ -952,7 +972,7 @@ export function BrowserPane({
</Pressable>
<Pressable
accessibilityRole="button"
accessibilityLabel="Forward"
accessibilityLabel={t("workspace.browser.controls.forward")}
disabled={!browser?.canGoForward}
onPress={handleForward}
style={forwardIconButtonStyle}
@@ -961,7 +981,11 @@ export function BrowserPane({
</Pressable>
<Pressable
accessibilityRole="button"
accessibilityLabel={browser?.isLoading ? "Stop loading" : "Refresh"}
accessibilityLabel={
browser?.isLoading
? t("workspace.browser.controls.stopLoading")
: t("workspace.browser.controls.refresh")
}
onPress={handleRefresh}
style={baseIconButtonStyle}
>
@@ -970,13 +994,13 @@ export function BrowserPane({
</View>
<View style={styles.urlBarWrap}>
<TextInput
accessibilityLabel="Browser URL"
accessibilityLabel={t("workspace.browser.controls.browserUrl")}
autoCapitalize="none"
autoCorrect={false}
onChangeText={setDraftUrl}
onFocus={handleUrlBarFocus}
onSubmitEditing={handleNavigateDraftUrl}
placeholder="Enter URL"
placeholder={t("workspace.browser.controls.enterUrl")}
placeholderTextColor={theme.colors.foregroundMuted}
ref={urlInputRef}
style={urlInputStyle}
@@ -988,7 +1012,7 @@ export function BrowserPane({
<>
<Pressable
accessibilityRole="button"
accessibilityLabel="Open browser dev tools"
accessibilityLabel={t("workspace.browser.controls.openDevTools")}
onPress={handleOpenDevTools}
style={baseIconButtonStyle}
>
@@ -996,7 +1020,11 @@ export function BrowserPane({
</Pressable>
<Pressable
accessibilityRole="button"
accessibilityLabel={selectorActive ? "Cancel element selector" : "Select element"}
accessibilityLabel={
selectorActive
? t("workspace.browser.controls.cancelSelector")
: t("workspace.browser.controls.selectElement")
}
onPress={handleToggleElementSelector}
style={selectorIconButtonStyle}
>