1
0
mirror of synced 2024-12-18 01:35:58 +01:00

feat: support i18n (ko, en)

This commit is contained in:
노주찬 2023-03-29 23:33:45 +09:00
parent 153d8399fc
commit 4076ac31ce
8 changed files with 174 additions and 24 deletions

12
App.tsx
View File

@ -13,6 +13,9 @@ import { ThemeProvider } from 'styled-components/native';
import LightTheme from './src/themes/lightTheme'; import LightTheme from './src/themes/lightTheme';
import DarkTheme from './src/themes/darkTheme'; import DarkTheme from './src/themes/darkTheme';
import './src/locales/i18n';
import { useTranslation } from 'react-i18next';
export type RootStackParams = { export type RootStackParams = {
Main: undefined; Main: undefined;
Add: undefined; Add: undefined;
@ -23,6 +26,7 @@ const queryClient = new QueryClient();
const Stack = createNativeStackNavigator<RootStackParams>(); const Stack = createNativeStackNavigator<RootStackParams>();
const App = () => { const App = () => {
const { t } = useTranslation();
const colorScheme = useColorScheme(); const colorScheme = useColorScheme();
const theme = useMemo( const theme = useMemo(
() => (colorScheme === 'dark' ? DarkTheme : LightTheme), () => (colorScheme === 'dark' ? DarkTheme : LightTheme),
@ -54,18 +58,20 @@ const App = () => {
name={'Main'} name={'Main'}
component={MainScreen} component={MainScreen}
options={{ options={{
title: '홈', title: t('title.home'),
}} }}
/> />
<Stack.Screen <Stack.Screen
name={'Add'} name={'Add'}
component={CardEditScreen} component={CardEditScreen}
options={{ title: '카드 추가' }} options={{
title: t('title.card_add'),
}}
/> />
<Stack.Screen <Stack.Screen
name={'Edit'} name={'Edit'}
component={CardEditScreen} component={CardEditScreen}
options={{ title: '카드 편집' }} options={{ title: t('title.card_edit') }}
/> />
</Stack.Navigator> </Stack.Navigator>
</NavigationContainer> </NavigationContainer>

View File

@ -8,6 +8,7 @@ import styled from 'styled-components/native';
import CardConv from '../modules/CardConv'; import CardConv from '../modules/CardConv';
import { Card } from '../types'; import { Card } from '../types';
import { useTranslation } from 'react-i18next';
interface CardViewProps { interface CardViewProps {
card: Card; card: Card;
@ -63,6 +64,8 @@ const BottomButtonText = styled.Text`
const CardView = (props: CardViewProps) => { const CardView = (props: CardViewProps) => {
const { card, index, onPress: onPressFromProps, onEdit, onDelete } = props; const { card, index, onPress: onPressFromProps, onEdit, onDelete } = props;
const { t } = useTranslation();
const onPress = useCallback(() => { const onPress = useCallback(() => {
onPressFromProps?.(index); onPressFromProps?.(index);
}, [onPressFromProps, index]); }, [onPressFromProps, index]);
@ -126,13 +129,13 @@ const CardView = (props: CardViewProps) => {
style={styles.bottomMenuButton} style={styles.bottomMenuButton}
onPress={onPressEdit} onPress={onPressEdit}
> >
<BottomButtonText></BottomButtonText> <BottomButtonText>{t('card.edit')}</BottomButtonText>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
style={styles.bottomMenuButton} style={styles.bottomMenuButton}
onPress={onPressDelete} onPress={onPressDelete}
> >
<BottomButtonText></BottomButtonText> <BottomButtonText>{t('card.remove')}</BottomButtonText>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
)} )}

41
src/locales/en.json Normal file
View File

@ -0,0 +1,41 @@
{
"title": {
"home": "Home",
"card_add": "Add",
"card_edit": "Edit"
},
"alert": {
"title": {
"error": "Error",
"card_remove": "Remove"
},
"body": {
"hcef_not_support": "This device does not support HCE-F. Please try again with another device.",
"hcef_init_fail": "Failed to initialize HCE-F.\nAfter activating NFC, please restart the app\n",
"card_remove": "Do you want to remove the card \"{{cardName}}\"?"
},
"button": {
"confirm": "Confirm",
"remove": "Remove",
"cancel": "Cancel"
}
},
"main": {
"please_add_a_card": "Please add a card."
},
"card": {
"touch_to_activate": "Tap to activate",
"touch_to_deactivate": "Tap to deactivate",
"edit": "Edit",
"remove": "Remove"
},
"card_edit": {
"loading_card_number": "Loading card number...",
"invalid_card_number": "Invalid card number",
"card_preview": "Card Preview",
"card_name": "Card Name",
"card_number": "Card Number",
"change_card_number": "Change card number",
"save": "Save"
}
}

29
src/locales/i18n.ts Normal file
View File

@ -0,0 +1,29 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import RNLanguageDetector from '@os-team/i18next-react-native-language-detector';
import { default as ko } from './ko.json';
import { default as en } from './en.json';
const resources = {
ko: {
translation: ko,
},
en: {
translation: en,
},
};
i18n
.use(initReactI18next)
.use(RNLanguageDetector)
.init({
resources,
compatibilityJSON: 'v3',
fallbackLng: 'en',
interpolation: {
escapeValue: false,
},
});
export default i18n;

17
src/locales/i18next.d.ts vendored Normal file
View File

@ -0,0 +1,17 @@
// import the original type declarations
import 'i18next';
// import all namespaces (for the default language, only)
import { default as ko } from './ko.json';
declare module 'i18next' {
// Extend CustomTypeOptions
interface CustomTypeOptions {
// custom namespace type, if you changed it
defaultNS: 'translation';
// custom resources type
resources: {
translation: typeof ko;
};
// other
}
}

41
src/locales/ko.json Normal file
View File

@ -0,0 +1,41 @@
{
"title": {
"home": "홈",
"card_add": "카드 추가",
"card_edit": "카드 편집"
},
"alert": {
"title": {
"error": "오류",
"card_remove": "카드 삭제"
},
"body": {
"hcef_not_support": "이 기기는 HCE-F를 지원하지 않습니다. 다른 기기로 다시 시도해 주세요.",
"hcef_init_fail": "HCE-F 초기 설정에 실패했습니다.\n앱을 종료한 뒤, NFC를 활성화하고 다시 실행해 주세요.",
"card_remove": "\"{{cardName}}\" 카드를 삭제하시겠습니까?"
},
"button": {
"confirm": "확인",
"remove": "삭제",
"cancel": "취소"
}
},
"main": {
"please_add_a_card": "카드를 추가해 주세요."
},
"card": {
"touch_to_activate": "터치해서 활성화",
"touch_to_deactivate": "터치해서 비활성화",
"edit": "편집",
"remove": "삭제"
},
"card_edit": {
"loading_card_number": "카드 번호를 불러오는 중...",
"invalid_card_number": "잘못된 카드 번호입니다.",
"card_preview": "카드 미리보기",
"card_name": "카드 이름",
"card_number": "카드 번호",
"change_card_number": "카드 번호 변경",
"save": "저장"
}
}

View File

@ -20,6 +20,7 @@ import { RootStackParams } from '../../App';
import CardView from '../components/Card'; import CardView from '../components/Card';
import { addCard, updateCard } from '../data/cards'; import { addCard, updateCard } from '../data/cards';
import { Card } from '../types'; import { Card } from '../types';
import { useTranslation } from 'react-i18next';
type TextFieldProps = TextInputProps & { type TextFieldProps = TextInputProps & {
title: string; title: string;
@ -140,6 +141,7 @@ type CardAddScreenProps = NativeStackScreenProps<RootStackParams, 'Add'>;
type CardEditScreenProps = NativeStackScreenProps<RootStackParams, 'Edit'>; type CardEditScreenProps = NativeStackScreenProps<RootStackParams, 'Edit'>;
const CardEditScreen = (props: CardAddScreenProps | CardEditScreenProps) => { const CardEditScreen = (props: CardAddScreenProps | CardEditScreenProps) => {
const { t } = useTranslation();
const initialData = props.route.params?.card ?? undefined; const initialData = props.route.params?.card ?? undefined;
const [mode] = useState<'add' | 'edit'>(() => { const [mode] = useState<'add' | 'edit'>(() => {
@ -156,14 +158,14 @@ const CardEditScreen = (props: CardAddScreenProps | CardEditScreenProps) => {
const styledUid = useMemo(() => { const styledUid = useMemo(() => {
if (!uid.isSuccess) { if (!uid.isSuccess) {
return '카드번호를 불러오는 중...'; return t('card_edit.loading_card_number');
} }
return ( return (
uid.data.match(/[A-Za-z0-9]{4}/g)?.join(' - ') ?? uid.data.match(/[A-Za-z0-9]{4}/g)?.join(' - ') ??
'잘못된 카드 번호입니다.' t('card_edit.invalid_card_number')
); );
}, [uid]); }, [t, uid]);
const onChangeCardName = useCallback((s: string) => { const onChangeCardName = useCallback((s: string) => {
setCardName(s); setCardName(s);
@ -228,7 +230,7 @@ const CardEditScreen = (props: CardAddScreenProps | CardEditScreenProps) => {
sid: cardNumber, sid: cardNumber,
name: cardName, name: cardName,
}} }}
mainText={'카드 미리보기'} mainText={t('card_edit.card_preview')}
index={0 /* dummy index */} index={0 /* dummy index */}
disabledMainButton={true} disabledMainButton={true}
hideBottomMenu={true} hideBottomMenu={true}
@ -236,26 +238,30 @@ const CardEditScreen = (props: CardAddScreenProps | CardEditScreenProps) => {
<View style={[styles.fieldItemContainer]}> <View style={[styles.fieldItemContainer]}>
<TextField <TextField
title={'카드 이름'} title={t('card_edit.card_name')}
value={cardName} value={cardName}
onChangeText={onChangeCardName} onChangeText={onChangeCardName}
/> />
</View> </View>
<View style={styles.fieldItemContainer}> <View style={styles.fieldItemContainer}>
<TextField title={'카드 번호'} value={styledUid} editable={false} /> <TextField
title={t('card_edit.card_number')}
value={styledUid}
editable={false}
/>
<Button <Button
containerStyle={styles.cardNumberChangeButton} containerStyle={styles.cardNumberChangeButton}
onPress={changeCardNumber} onPress={changeCardNumber}
disabled={!uid.isSuccess} disabled={!uid.isSuccess}
text={'카드 번호 변경'} text={t('card_edit.change_card_number')}
/> />
</View> </View>
<Button <Button
onPress={save} onPress={save}
containerStyle={styles.saveButton} containerStyle={styles.saveButton}
text={'저장'} text={t('card_edit.save')}
/> />
</ScrollView> </ScrollView>
</Container> </Container>

View File

@ -21,6 +21,7 @@ import CardView from '../components/Card';
import { Card } from '../types'; import { Card } from '../types';
import { getCards, removeCard } from '../data/cards'; import { getCards, removeCard } from '../data/cards';
import { RootStackParams } from '../../App'; import { RootStackParams } from '../../App';
import { useTranslation } from 'react-i18next';
const Container = styled(View)` const Container = styled(View)`
flex: 1; flex: 1;
@ -57,6 +58,8 @@ const CardList = (props: { cards: Card[] }) => {
const cards = props.cards; const cards = props.cards;
const [enabledCardIndex, setEnabledCardIndex] = useState<number | null>(null); const [enabledCardIndex, setEnabledCardIndex] = useState<number | null>(null);
const { t } = useTranslation();
const toggleHcef = useCallback( const toggleHcef = useCallback(
async (index: number) => { async (index: number) => {
const card = cards[index]; const card = cards[index];
@ -83,15 +86,19 @@ const CardList = (props: { cards: Card[] }) => {
(index: number) => { (index: number) => {
const card = cards[index]; const card = cards[index];
Alert.alert('카드 삭제', `"${card.name}" 카드를 삭제하시겠습니까?`, [ Alert.alert(
{ t('alert.title.card_remove'),
text: '삭제', t('alert.body.card_remove', { cardName: card.name }),
onPress: () => { [
deleteMutation.mutate(index); {
text: t('alert.button.confirm'),
onPress: () => {
deleteMutation.mutate(index);
},
}, },
}, { text: t('alert.button.cancel') },
{ text: '취소' }, ],
]); );
}, },
[cards, deleteMutation], [cards, deleteMutation],
); );
@ -122,8 +129,8 @@ const CardList = (props: { cards: Card[] }) => {
onDelete={onDelete} onDelete={onDelete}
mainText={ mainText={
card.index === enabledCardIndex card.index === enabledCardIndex
? '터치해서 비활성화' ? t('card.touch_to_activate')
: '터치해서 활성화' : t('card.touch_to_deactivate')
} }
/> />
)} )}
@ -135,7 +142,7 @@ const CardList = (props: { cards: Card[] }) => {
} else { } else {
return ( return (
<View style={styles.placeholderContainer}> <View style={styles.placeholderContainer}>
<PlaceholderText> .</PlaceholderText> <PlaceholderText>{t('main.please_add_a_card')}</PlaceholderText>
</View> </View>
); );
} }