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,6 @@
import { useCallback, useMemo, useState } from "react";
import type { TFunction } from "i18next";
import { useTranslation } from "react-i18next";
import { Alert, Pressable, Text, View, type PressableStateCallbackType } from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { settingsStyles } from "@/styles/settings";
@@ -29,18 +31,32 @@ interface ProviderStatus {
modelCount: number | null;
}
function getProviderStatus(status: string, enabled: boolean, modelCount: number): ProviderStatus {
if (!enabled) return { tone: "muted", label: "Disabled", modelCount: null };
if (status === "loading") return { tone: "loading", label: "Loading", modelCount: null };
if (status === "error") return { tone: "danger", label: "Error", modelCount: null };
function getProviderStatus(
status: string,
enabled: boolean,
modelCount: number,
t: TFunction,
): ProviderStatus {
if (!enabled)
return { tone: "muted", label: t("settings.providers.statuses.disabled"), modelCount: null };
if (status === "loading") {
return { tone: "loading", label: t("settings.providers.statuses.loading"), modelCount: null };
}
if (status === "error") {
return { tone: "danger", label: t("settings.providers.statuses.error"), modelCount: null };
}
if (status === "ready") {
return {
tone: "success",
label: "Available",
label: t("settings.providers.statuses.available"),
modelCount: modelCount > 0 ? modelCount : null,
};
}
return { tone: "warning", label: "Not installed", modelCount: null };
return {
tone: "warning",
label: t("settings.providers.statuses.notInstalled"),
modelCount: null,
};
}
interface ProviderRowProps {
@@ -62,6 +78,7 @@ function ProviderRow({
onPress,
onToggleEnabled,
}: ProviderRowProps) {
const { t } = useTranslation();
const { theme } = useUnistyles();
const ProviderIcon = getProviderIcon(def.id);
const providerError =
@@ -72,7 +89,7 @@ function ProviderRow({
? entry.error.trim()
: null;
const modelCount = entry.models?.length ?? 0;
const providerStatus = getProviderStatus(entry.status, enabled, modelCount);
const providerStatus = getProviderStatus(entry.status, enabled, modelCount, t);
const handlePress = useCallback(() => {
onPress(def.id);
@@ -99,7 +116,7 @@ function ProviderRow({
style={rowStyle}
onPress={handlePress}
accessibilityRole="button"
accessibilityLabel={`${def.label} provider details`}
accessibilityLabel={t("settings.providers.providerDetails", { name: def.label })}
>
{({ hovered }: PressableStateCallbackType & { hovered?: boolean }) => (
<>
@@ -128,7 +145,7 @@ function ProviderRow({
value={enabled}
onValueChange={handleToggleValueChange}
disabled={isToggling}
accessibilityLabel={`Enable ${def.label}`}
accessibilityLabel={t("settings.providers.enableProvider", { name: def.label })}
/>
</>
)}
@@ -150,6 +167,7 @@ function getDotColor(tone: StatusTone, theme: ReturnType<typeof useUnistyles>["t
}
function StatusIndicator({ status }: { status: ProviderStatus }) {
const { t } = useTranslation();
const { theme } = useUnistyles();
const dotStyle = useMemo(
() => [styles.statusDot, { backgroundColor: getDotColor(status.tone, theme) }],
@@ -168,7 +186,9 @@ function StatusIndicator({ status }: { status: ProviderStatus }) {
<>
<Text style={styles.separator}>·</Text>
<Text style={styles.statusLabel}>
{status.modelCount === 1 ? "1 model" : `${status.modelCount} models`}
{status.modelCount === 1
? t("settings.providers.models.one")
: t("settings.providers.models.many", { count: status.modelCount })}
</Text>
</>
) : null}
@@ -181,6 +201,7 @@ export interface ProvidersSectionProps {
}
export function ProvidersSection({ serverId }: ProvidersSectionProps) {
const { t } = useTranslation();
const isConnected = useHostRuntimeIsConnected(serverId);
const { entries, isLoading, refresh } = useProvidersSnapshot(serverId);
const { patchConfig } = useDaemonConfig(serverId);
@@ -205,14 +226,14 @@ export function ProvidersSection({ serverId }: ProvidersSectionProps) {
await patchConfig({ providers: { [providerId]: { enabled } } });
} catch (error) {
Alert.alert(
"Unable to update provider",
t("settings.providers.updateErrorTitle"),
error instanceof Error ? error.message : String(error),
);
} finally {
setPendingProviderId((current) => (current === providerId ? null : current));
}
},
[patchConfig],
[patchConfig, t],
);
const handleInstall = useCallback(
@@ -224,31 +245,31 @@ export function ProvidersSection({ serverId }: ProvidersSectionProps) {
await refresh([entry.id]);
} catch (error) {
Alert.alert(
"Unable to add provider",
t("settings.providers.addErrorTitle"),
error instanceof Error ? error.message : String(error),
);
} finally {
setInstallingProviderId((current) => (current === entry.id ? null : current));
}
},
[installingProviderId, patchConfig, refresh],
[installingProviderId, patchConfig, refresh, t],
);
return (
<>
<SettingsSection
title="Providers"
title={t("settings.providers.title")}
testID="host-page-providers-card"
style={styles.sectionSpacing}
>
{!hasServer || !isConnected ? (
<View style={EMPTY_CARD_STYLE}>
<Text style={styles.emptyText}>Connect to this host to see providers</Text>
<Text style={styles.emptyText}>{t("settings.providers.unavailable")}</Text>
</View>
) : null}
{hasServer && isConnected && isLoading ? (
<View style={EMPTY_CARD_STYLE}>
<Text style={styles.emptyText}>Loading...</Text>
<Text style={styles.emptyText}>{t("settings.providers.loading")}</Text>
</View>
) : null}
{hasServer && isConnected && !isLoading && providerDefinitions.length > 0 ? (
@@ -275,7 +296,7 @@ export function ProvidersSection({ serverId }: ProvidersSectionProps) {
{hasServer && isConnected ? (
<SettingsSection
title="Add provider"
title={t("settings.providers.addProvider")}
testID="host-page-add-provider-card"
style={styles.addProviderSection}
>