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 {
type PressableStateCallbackType,
} from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { useTranslation } from "react-i18next";
import { File, Folder } from "lucide-react-native";
import type { Theme } from "@/styles/theme";
import { getAutocompleteScrollOffset } from "./autocomplete-utils";
@@ -122,11 +123,14 @@ export function Autocomplete({
onSelect,
isLoading = false,
errorMessage,
loadingText = "Loading...",
emptyText = "No results found",
loadingText,
emptyText,
maxHeight = 220,
}: AutocompleteProps) {
const { t } = useTranslation();
const { theme } = useUnistyles();
const resolvedLoadingText = loadingText ?? t("common.states.loading");
const resolvedEmptyText = emptyText ?? t("common.empty.noResults");
const scrollRef = useRef<ScrollView>(null);
const rowLayoutsRef = useRef<Map<number, { top: number; height: number }>>(new Map());
const viewportHeightRef = useRef(0);
@@ -213,7 +217,7 @@ export function Autocomplete({
return (
<View style={containerStyle}>
<View style={styles.emptyItem}>
<Text style={styles.emptyText}>{loadingText}</Text>
<Text style={styles.emptyText}>{resolvedLoadingText}</Text>
</View>
</View>
);
@@ -233,7 +237,7 @@ export function Autocomplete({
return (
<View style={containerStyle}>
<View style={styles.emptyItem}>
<Text style={styles.emptyText}>{emptyText}</Text>
<Text style={styles.emptyText}>{resolvedEmptyText}</Text>
</View>
</View>
);
+12 -7
View File
@@ -23,6 +23,7 @@ import {
type StyleProp,
type ViewStyle,
} from "react-native";
import { useTranslation } from "react-i18next";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { useIsCompactFormFactor } from "@/constants/layout";
import {
@@ -1240,15 +1241,15 @@ export function Combobox({
renderOption,
onSearchQueryChange,
searchable = true,
placeholder = "Search...",
placeholder,
searchPlaceholder,
emptyText = "No options match your search.",
emptyText,
allowCustomValue = false,
customValuePrefix = "Use",
customValueDescription,
customValueKind,
optionsPosition = "below-search",
title = "Select",
title,
header,
mobileChildrenScrollEnabled = true,
presentation,
@@ -1263,7 +1264,11 @@ export function Combobox({
anchorRef,
children,
}: ComboboxProps): ReactElement | null {
const { t } = useTranslation();
const { theme } = useUnistyles();
const resolvedPlaceholder = placeholder ?? t("common.placeholders.search");
const resolvedEmptyText = emptyText ?? t("common.empty.noOptionsMatchSearch");
const resolvedTitle = title ?? t("common.actions.select");
const isMobile = useIsCompactFormFactor();
const titleColor = theme.colors.foreground;
const effectiveOptionsPosition = resolveEffectiveOptionsPosition(isMobile, optionsPosition);
@@ -1519,7 +1524,7 @@ export function Combobox({
[],
);
const effectiveSearchPlaceholder = searchPlaceholder ?? placeholder;
const effectiveSearchPlaceholder = searchPlaceholder ?? resolvedPlaceholder;
const hasChildren = Boolean(children);
if (isMobile) {
@@ -1531,7 +1536,7 @@ export function Combobox({
handleSheetDismiss={handleSheetDismiss}
handleIndicatorStyle={handleIndicatorStyle}
titleColor={titleColor}
title={title}
title={resolvedTitle}
header={header}
onClose={handleClose}
stickyHeader={stickyHeader}
@@ -1547,7 +1552,7 @@ export function Combobox({
orderedVisibleOptions={orderedVisibleOptions}
value={value}
activeIndex={activeIndex}
emptyText={emptyText}
emptyText={resolvedEmptyText}
handleSelect={handleSelect}
renderOption={renderOption}
>
@@ -1580,7 +1585,7 @@ export function Combobox({
orderedVisibleOptions={orderedVisibleOptions}
value={value}
activeIndex={activeIndex}
emptyText={emptyText}
emptyText={resolvedEmptyText}
handleSelect={handleSelect}
renderOption={renderOption}
hasChildren={hasChildren}
@@ -12,6 +12,7 @@ import {
type ReactNode,
type Ref,
} from "react";
import { useTranslation } from "react-i18next";
import {
ActivityIndicator,
Dimensions,
@@ -385,6 +386,7 @@ export function ContextMenuContent({
mobileMode?: MobileMenuMode;
testID?: string;
}>): ReactElement | null {
const { t } = useTranslation();
const context = useContextMenuContext("ContextMenuContent");
const { theme } = useUnistyles();
const webScrollbarStyle = useWebScrollbarStyle();
@@ -560,7 +562,7 @@ export function ContextMenuContent({
<View style={styles.overlay}>
<Pressable
accessibilityRole="button"
accessibilityLabel="Menu backdrop"
accessibilityLabel={t("menu.backdrop")}
style={styles.backdrop}
onPress={handleClose}
testID={testID ? `${testID}-backdrop` : undefined}
@@ -11,6 +11,7 @@ import {
type ReactElement,
type ReactNode,
} from "react";
import { useTranslation } from "react-i18next";
import {
ActivityIndicator,
Modal,
@@ -442,6 +443,7 @@ export function DropdownMenuContent({
scrollable?: boolean;
testID?: string;
}>): ReactElement | null {
const { t } = useTranslation();
const { open, setOpen, triggerRef, flushPendingSelect } =
useDropdownMenuContext("DropdownMenuContent");
const [modalVisible, setModalVisible] = useState(false);
@@ -625,7 +627,7 @@ export function DropdownMenuContent({
<View style={styles.overlay}>
<Pressable
accessibilityRole="button"
accessibilityLabel="Menu backdrop"
accessibilityLabel={t("menu.backdrop")}
style={styles.backdrop}
onPress={handleClose}
testID={testID ? `${testID}-backdrop` : undefined}