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,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}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user