fix(settings): extract language picker and resolve undefined display on first launch

This commit is contained in:
YagUber 2026-04-13 18:08:07 -05:00
parent cbc9fc4fa6
commit eec51f787c
No known key found for this signature in database
3 changed files with 182 additions and 195 deletions

View file

@ -1,29 +1,29 @@
export const LOCALES = [
{ code: 'en', key: 'english' },
{ code: 'pt-BR', key: 'portuguese_br' },
{ code: 'pt-PT', key: 'portuguese_pt' },
{ code: 'de', key: 'german' },
{ code: 'ar', key: 'arabic' },
{ code: 'fr', key: 'french' },
{ code: 'it', key: 'italian' },
{ code: 'es', key: 'spanish' },
{ code: 'hr', key: 'croatian' },
{ code: 'zh-CN', key: 'chinese' },
{ code: 'hi', key: 'hindi' },
{ code: 'sr', key: 'serbian' },
{ code: 'he', key: 'hebrew' },
{ code: 'bg', key: 'bulgarian' },
{ code: 'pl', key: 'polish' },
{ code: 'cs', key: 'czech' },
{ code: 'tr', key: 'turkish' },
{ code: 'sl', key: 'slovenian' },
{ code: 'mk', key: 'macedonian' },
{ code: 'ru', key: 'russian' },
{ code: 'fil', key: 'filipino' },
{ code: 'nl-NL', key: 'dutch_nl' },
{ code: 'ro', key: 'romanian' },
{ code: 'sq', key: 'albanian' },
{ code: 'ca', key: 'catalan' },
{ code: 'vi', key: 'vietnamese' },
{ code: 'ja', key: 'japanese' }
{ code: 'en', key: 'english', name: 'English' },
{ code: 'pt-BR', key: 'portuguese_br', name: 'Português (BR)' },
{ code: 'pt-PT', key: 'portuguese_pt', name: 'Português (PT)' },
{ code: 'de', key: 'german', name: 'Deutsch' },
{ code: 'ar', key: 'arabic', name: 'العربية' },
{ code: 'fr', key: 'french', name: 'Français' },
{ code: 'it', key: 'italian', name: 'Italiano' },
{ code: 'es', key: 'spanish', name: 'Español' },
{ code: 'hr', key: 'croatian', name: 'Hrvatski' },
{ code: 'zh-CN', key: 'chinese', name: '中文(简体)' },
{ code: 'hi', key: 'hindi', name: 'हिन्दी' },
{ code: 'sr', key: 'serbian', name: 'Srpski' },
{ code: 'he', key: 'hebrew', name: 'עברית' },
{ code: 'bg', key: 'bulgarian', name: 'Български' },
{ code: 'pl', key: 'polish', name: 'Polski' },
{ code: 'cs', key: 'czech', name: 'Čeština' },
{ code: 'tr', key: 'turkish', name: 'Türkçe' },
{ code: 'sl', key: 'slovenian', name: 'Slovenščina' },
{ code: 'mk', key: 'macedonian', name: 'Македонски' },
{ code: 'ru', key: 'russian', name: 'Русский' },
{ code: 'fil', key: 'filipino', name: 'Filipino' },
{ code: 'nl-NL', key: 'dutch_nl', name: 'Nederlands' },
{ code: 'ro', key: 'romanian', name: 'Română' },
{ code: 'sq', key: 'albanian', name: 'Shqip' },
{ code: 'ca', key: 'catalan', name: 'Català' },
{ code: 'vi', key: 'vietnamese', name: 'Tiếng Việt' },
{ code: 'ja', key: 'japanese', name: '日本語' },
];

View file

@ -14,7 +14,7 @@ import {
FlatList,
Image,
} from 'react-native';
import { BottomSheetModal, BottomSheetView, BottomSheetBackdrop, BottomSheetScrollView } from '@gorhom/bottom-sheet';
import { BottomSheetModal, BottomSheetBackdrop } from '@gorhom/bottom-sheet';
import { useTranslation } from 'react-i18next';
import { mmkvStorage } from '../services/mmkvStorage';
import { useNavigation } from '@react-navigation/native';
@ -46,8 +46,7 @@ import { IntegrationsSettingsContent } from './settings/IntegrationsSettingsScre
import { AboutSettingsContent, AboutFooter } from './settings/AboutSettingsScreen';
import { PrivacySettingsContent } from './settings/PrivacySettingsScreen';
import { SettingsCard, SettingItem, ChevronRight, CustomSwitch } from './settings/SettingsComponents';
import { useBottomSheetBackHandler } from '../hooks/useBottomSheetBackHandler';
import { LOCALES } from '../constants/locales';
import { LanguageSettingItem } from './settings/LanguageSettingItem';
import { useSimklIntegration } from '../hooks/useSimklIntegration';
import SimklIcon from '../components/icons/SimklIcon';
@ -156,8 +155,6 @@ const SettingsScreen: React.FC = () => {
];
const { settings, updateSetting } = useSettings();
const [hasUpdateBadge, setHasUpdateBadge] = useState(false);
const languageSheetRef = useRef<BottomSheetModal>(null);
const { onChange, onDismiss } = useBottomSheetBackHandler();
const insets = useSafeAreaInsets();
// Render backdrop for bottom sheet
@ -439,15 +436,7 @@ const SettingsScreen: React.FC = () => {
return (
<>
<SettingsCard title="GENERAL" isTablet={isTablet}>
<SettingItem
title={t('settings.language')}
description={t(`settings.${LOCALES.find(l => l.code === i18n.language)?.key}`)}
icon="globe"
renderControl={() => <ChevronRight />}
onPress={() => languageSheetRef.current?.present()}
isLast={true}
isTablet={isTablet}
/>
<LanguageSettingItem isTablet={isTablet} isLast />
</SettingsCard>
<AppearanceSettingsContent isTablet={isTablet} />
</>
@ -637,64 +626,6 @@ const SettingsScreen: React.FC = () => {
onClose={() => setAlertVisible(false)}
/>
<BottomSheetModal
ref={languageSheetRef}
index={0}
snapPoints={['65%']}
enablePanDownToClose={true}
backdropComponent={renderBackdrop}
backgroundStyle={{
backgroundColor: currentTheme.colors.darkGray || '#0A0C0C',
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
}}
handleIndicatorStyle={{
backgroundColor: currentTheme.colors.mediumGray,
width: 40,
}}
onChange={onChange(languageSheetRef)}
onDismiss={onDismiss(languageSheetRef)}
>
<View style={[styles.bottomSheetHeader, { backgroundColor: currentTheme.colors.darkGray || '#0A0C0C' }]}>
<Text style={[styles.bottomSheetTitle, { color: currentTheme.colors.white }]}>
{t('settings.select_language')}
</Text>
<TouchableOpacity onPress={() => languageSheetRef.current?.dismiss()}>
<Feather name="x" size={24} color={currentTheme.colors.lightGray} />
</TouchableOpacity>
</View>
<BottomSheetScrollView
style={{ backgroundColor: currentTheme.colors.darkGray || '#0A0C0C' }}
contentContainerStyle={[styles.bottomSheetContent, { paddingBottom: insets.bottom + 16 }]}
>
{
LOCALES.sort((a, b) => a.key.localeCompare(b.key)).map(l =>
<TouchableOpacity
key={l.key}
style={[
styles.languageOption,
i18n.language === l.code && { backgroundColor: currentTheme.colors.primary + '20' }
]}
onPress={() => {
i18n.changeLanguage(l.code);
languageSheetRef.current?.dismiss();
}}
>
<Text style={[
styles.languageText,
{ color: currentTheme.colors.highEmphasis },
i18n.language === l.code && { color: currentTheme.colors.primary, fontWeight: 'bold' }
]}>
{t(`settings.${l.key}`)}
</Text>
{i18n.language === l.code && (
<Feather name="check" size={20} color={currentTheme.colors.primary} />
)}
</TouchableOpacity>
)
}
</BottomSheetScrollView>
</BottomSheetModal>
</View>
);
}
@ -772,14 +703,7 @@ const SettingsScreen: React.FC = () => {
(settingsConfig?.categories?.['playback']?.visible !== false)
) && (
<SettingsCard title="GENERAL">
<SettingItem
title={t('settings.language')}
description={t(`settings.${LOCALES.find(l => l.code === i18n.language)?.key}`)
}
icon="globe"
renderControl={() => <ChevronRight />}
onPress={() => languageSheetRef.current?.present()}
/>
<LanguageSettingItem />
{(settingsConfig?.categories?.['content']?.visible !== false) && (
<SettingItem
title={t('settings.content_discovery')}
@ -974,64 +898,6 @@ const SettingsScreen: React.FC = () => {
onClose={() => setAlertVisible(false)}
/>
<BottomSheetModal
ref={languageSheetRef}
index={0}
snapPoints={['65%']}
enablePanDownToClose={true}
backdropComponent={renderBackdrop}
backgroundStyle={{
backgroundColor: currentTheme.colors.darkGray || '#0A0C0C',
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
}}
handleIndicatorStyle={{
backgroundColor: currentTheme.colors.mediumGray,
width: 40,
}}
onChange={onChange(languageSheetRef)}
onDismiss={onDismiss(languageSheetRef)}
>
<View style={[styles.bottomSheetHeader, { backgroundColor: currentTheme.colors.darkGray || '#0A0C0C' }]}>
<Text style={[styles.bottomSheetTitle, { color: currentTheme.colors.white }]}>
{t('settings.select_language')}
</Text>
<TouchableOpacity onPress={() => languageSheetRef.current?.dismiss()}>
<Feather name="x" size={24} color={currentTheme.colors.lightGray} />
</TouchableOpacity>
</View>
<BottomSheetScrollView
style={{ backgroundColor: currentTheme.colors.darkGray || '#0A0C0C' }}
contentContainerStyle={[styles.bottomSheetContent, { paddingBottom: insets.bottom + 16 }]}
>
{
LOCALES.sort((a, b) => a.key.localeCompare(b.key)).map(l =>
<TouchableOpacity
key={l.key}
style={[
styles.languageOption,
i18n.language === l.code && { backgroundColor: currentTheme.colors.primary + '20' }
]}
onPress={() => {
i18n.changeLanguage(l.code);
languageSheetRef.current?.dismiss();
}}
>
<Text style={[
styles.languageText,
{ color: currentTheme.colors.highEmphasis },
i18n.language === l.code && { color: currentTheme.colors.primary, fontWeight: 'bold' }
]}>
{t(`settings.${l.key}`)}
</Text>
{i18n.language === l.code && (
<Feather name="check" size={20} color={currentTheme.colors.primary} />
)}
</TouchableOpacity>
)
}
</BottomSheetScrollView>
</BottomSheetModal>
</View>
);
};
@ -1043,36 +909,6 @@ const styles = StyleSheet.create({
actionSheetContent: {
flex: 1,
},
bottomSheetHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 20,
paddingVertical: 16,
borderBottomWidth: 1,
borderBottomColor: 'rgba(255,255,255,0.1)',
},
bottomSheetTitle: {
fontSize: 18,
fontWeight: '600',
},
bottomSheetContent: {
paddingHorizontal: 16,
paddingTop: 8,
paddingBottom: 24,
},
languageOption: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 14,
paddingHorizontal: 16,
borderRadius: 8,
marginBottom: 8,
},
languageText: {
fontSize: 16,
},
// Mobile styles
contentContainer: {
flex: 1,

View file

@ -0,0 +1,151 @@
import React, { useCallback, useRef } from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { BottomSheetBackdrop, BottomSheetModal, BottomSheetScrollView } from '@gorhom/bottom-sheet';
import { Feather } from '@expo/vector-icons';
import { useTranslation } from 'react-i18next';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTheme } from '../../contexts/ThemeContext';
import { useBottomSheetBackHandler } from '../../hooks/useBottomSheetBackHandler';
import { ChevronRight, SettingItem } from './SettingsComponents';
import { LOCALES } from '../../constants/locales';
const SORTED_LOCALES = [...LOCALES].sort((a, b) => a.name.localeCompare(b.name));
interface LanguageSettingItemProps {
isTablet?: boolean;
isLast?: boolean;
}
export const LanguageSettingItem: React.FC<LanguageSettingItemProps> = ({ isTablet = false, isLast = false }) => {
const { t, i18n } = useTranslation();
const { currentTheme } = useTheme();
const insets = useSafeAreaInsets();
const sheetRef = useRef<BottomSheetModal>(null);
const { onChange, onDismiss } = useBottomSheetBackHandler();
const currentLocale =
LOCALES.find(l => l.code === i18n.language) ??
LOCALES.find(l => l.code === i18n.language?.split('-')[0]);
const renderBackdrop = useCallback(
(props: any) => (
<BottomSheetBackdrop {...props} disappearsOnIndex={-1} appearsOnIndex={0} opacity={0.5} />
),
[]
);
return (
<>
<SettingItem
title={t('settings.language')}
description={currentLocale?.name}
icon="globe"
renderControl={() => <ChevronRight />}
onPress={() => sheetRef.current?.present()}
isLast={isLast}
isTablet={isTablet}
/>
<BottomSheetModal
ref={sheetRef}
index={0}
snapPoints={['65%']}
enablePanDownToClose
backdropComponent={renderBackdrop}
backgroundStyle={{
backgroundColor: currentTheme.colors.darkGray || '#0A0C0C',
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
}}
handleIndicatorStyle={{
backgroundColor: currentTheme.colors.mediumGray,
width: 40,
}}
onChange={onChange(sheetRef)}
onDismiss={onDismiss(sheetRef)}
>
<View style={[styles.sheetHeader, { backgroundColor: currentTheme.colors.darkGray || '#0A0C0C' }]}>
<Text style={[styles.sheetTitle, { color: currentTheme.colors.white }]}>
{t('settings.select_language')}
</Text>
<TouchableOpacity onPress={() => sheetRef.current?.dismiss()}>
<Feather name="x" size={24} color={currentTheme.colors.lightGray} />
</TouchableOpacity>
</View>
<BottomSheetScrollView
style={{ backgroundColor: currentTheme.colors.darkGray || '#0A0C0C' }}
contentContainerStyle={[styles.sheetContent, { paddingBottom: insets.bottom + 16 }]}
>
{SORTED_LOCALES.map(l => {
const isSelected = i18n.language === l.code ||
i18n.language?.split('-')[0] === l.code;
return (
<TouchableOpacity
key={l.code}
style={[
styles.languageOption,
isSelected && { backgroundColor: currentTheme.colors.primary + '20' },
]}
onPress={() => {
i18n.changeLanguage(l.code);
sheetRef.current?.dismiss();
}}
>
<Text style={[
styles.languageName,
{ color: isSelected ? currentTheme.colors.primary : currentTheme.colors.highEmphasis },
isSelected && { fontWeight: 'bold' },
]}>
{l.name}
</Text>
<Text style={[styles.languageCode, { color: currentTheme.colors.mediumEmphasis }]}>
{l.code.toUpperCase()}
</Text>
{isSelected && (
<Feather name="check" size={20} color={currentTheme.colors.primary} />
)}
</TouchableOpacity>
);
})}
</BottomSheetScrollView>
</BottomSheetModal>
</>
);
};
const styles = StyleSheet.create({
sheetHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 20,
paddingVertical: 16,
borderBottomWidth: 1,
borderBottomColor: 'rgba(255,255,255,0.1)',
},
sheetTitle: {
fontSize: 18,
fontWeight: '600',
},
sheetContent: {
paddingHorizontal: 16,
paddingTop: 8,
paddingBottom: 24,
},
languageOption: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 14,
paddingHorizontal: 16,
borderRadius: 8,
marginBottom: 8,
},
languageName: {
flex: 1,
fontSize: 16,
},
languageCode: {
fontSize: 12,
marginRight: 8,
},
});