feat: support i18n (ko, en)
This commit is contained in:
parent
153d8399fc
commit
4076ac31ce
12
App.tsx
12
App.tsx
@ -13,6 +13,9 @@ import { ThemeProvider } from 'styled-components/native';
|
||||
import LightTheme from './src/themes/lightTheme';
|
||||
import DarkTheme from './src/themes/darkTheme';
|
||||
|
||||
import './src/locales/i18n';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export type RootStackParams = {
|
||||
Main: undefined;
|
||||
Add: undefined;
|
||||
@ -23,6 +26,7 @@ const queryClient = new QueryClient();
|
||||
const Stack = createNativeStackNavigator<RootStackParams>();
|
||||
|
||||
const App = () => {
|
||||
const { t } = useTranslation();
|
||||
const colorScheme = useColorScheme();
|
||||
const theme = useMemo(
|
||||
() => (colorScheme === 'dark' ? DarkTheme : LightTheme),
|
||||
@ -54,18 +58,20 @@ const App = () => {
|
||||
name={'Main'}
|
||||
component={MainScreen}
|
||||
options={{
|
||||
title: '홈',
|
||||
title: t('title.home'),
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name={'Add'}
|
||||
component={CardEditScreen}
|
||||
options={{ title: '카드 추가' }}
|
||||
options={{
|
||||
title: t('title.card_add'),
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name={'Edit'}
|
||||
component={CardEditScreen}
|
||||
options={{ title: '카드 편집' }}
|
||||
options={{ title: t('title.card_edit') }}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
</NavigationContainer>
|
||||
|
@ -8,6 +8,7 @@ import styled from 'styled-components/native';
|
||||
|
||||
import CardConv from '../modules/CardConv';
|
||||
import { Card } from '../types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface CardViewProps {
|
||||
card: Card;
|
||||
@ -63,6 +64,8 @@ const BottomButtonText = styled.Text`
|
||||
const CardView = (props: CardViewProps) => {
|
||||
const { card, index, onPress: onPressFromProps, onEdit, onDelete } = props;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
onPressFromProps?.(index);
|
||||
}, [onPressFromProps, index]);
|
||||
@ -126,13 +129,13 @@ const CardView = (props: CardViewProps) => {
|
||||
style={styles.bottomMenuButton}
|
||||
onPress={onPressEdit}
|
||||
>
|
||||
<BottomButtonText>편집</BottomButtonText>
|
||||
<BottomButtonText>{t('card.edit')}</BottomButtonText>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={styles.bottomMenuButton}
|
||||
onPress={onPressDelete}
|
||||
>
|
||||
<BottomButtonText>삭제</BottomButtonText>
|
||||
<BottomButtonText>{t('card.remove')}</BottomButtonText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
41
src/locales/en.json
Normal file
41
src/locales/en.json
Normal 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
29
src/locales/i18n.ts
Normal 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
17
src/locales/i18next.d.ts
vendored
Normal 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
41
src/locales/ko.json
Normal 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": "저장"
|
||||
}
|
||||
}
|
@ -20,6 +20,7 @@ import { RootStackParams } from '../../App';
|
||||
import CardView from '../components/Card';
|
||||
import { addCard, updateCard } from '../data/cards';
|
||||
import { Card } from '../types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type TextFieldProps = TextInputProps & {
|
||||
title: string;
|
||||
@ -140,6 +141,7 @@ type CardAddScreenProps = NativeStackScreenProps<RootStackParams, 'Add'>;
|
||||
type CardEditScreenProps = NativeStackScreenProps<RootStackParams, 'Edit'>;
|
||||
|
||||
const CardEditScreen = (props: CardAddScreenProps | CardEditScreenProps) => {
|
||||
const { t } = useTranslation();
|
||||
const initialData = props.route.params?.card ?? undefined;
|
||||
|
||||
const [mode] = useState<'add' | 'edit'>(() => {
|
||||
@ -156,14 +158,14 @@ const CardEditScreen = (props: CardAddScreenProps | CardEditScreenProps) => {
|
||||
|
||||
const styledUid = useMemo(() => {
|
||||
if (!uid.isSuccess) {
|
||||
return '카드번호를 불러오는 중...';
|
||||
return t('card_edit.loading_card_number');
|
||||
}
|
||||
|
||||
return (
|
||||
uid.data.match(/[A-Za-z0-9]{4}/g)?.join(' - ') ??
|
||||
'잘못된 카드 번호입니다.'
|
||||
t('card_edit.invalid_card_number')
|
||||
);
|
||||
}, [uid]);
|
||||
}, [t, uid]);
|
||||
|
||||
const onChangeCardName = useCallback((s: string) => {
|
||||
setCardName(s);
|
||||
@ -228,7 +230,7 @@ const CardEditScreen = (props: CardAddScreenProps | CardEditScreenProps) => {
|
||||
sid: cardNumber,
|
||||
name: cardName,
|
||||
}}
|
||||
mainText={'카드 미리보기'}
|
||||
mainText={t('card_edit.card_preview')}
|
||||
index={0 /* dummy index */}
|
||||
disabledMainButton={true}
|
||||
hideBottomMenu={true}
|
||||
@ -236,26 +238,30 @@ const CardEditScreen = (props: CardAddScreenProps | CardEditScreenProps) => {
|
||||
|
||||
<View style={[styles.fieldItemContainer]}>
|
||||
<TextField
|
||||
title={'카드 이름'}
|
||||
title={t('card_edit.card_name')}
|
||||
value={cardName}
|
||||
onChangeText={onChangeCardName}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.fieldItemContainer}>
|
||||
<TextField title={'카드 번호'} value={styledUid} editable={false} />
|
||||
<TextField
|
||||
title={t('card_edit.card_number')}
|
||||
value={styledUid}
|
||||
editable={false}
|
||||
/>
|
||||
<Button
|
||||
containerStyle={styles.cardNumberChangeButton}
|
||||
onPress={changeCardNumber}
|
||||
disabled={!uid.isSuccess}
|
||||
text={'카드 번호 변경'}
|
||||
text={t('card_edit.change_card_number')}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Button
|
||||
onPress={save}
|
||||
containerStyle={styles.saveButton}
|
||||
text={'저장'}
|
||||
text={t('card_edit.save')}
|
||||
/>
|
||||
</ScrollView>
|
||||
</Container>
|
||||
|
@ -21,6 +21,7 @@ import CardView from '../components/Card';
|
||||
import { Card } from '../types';
|
||||
import { getCards, removeCard } from '../data/cards';
|
||||
import { RootStackParams } from '../../App';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const Container = styled(View)`
|
||||
flex: 1;
|
||||
@ -57,6 +58,8 @@ const CardList = (props: { cards: Card[] }) => {
|
||||
const cards = props.cards;
|
||||
const [enabledCardIndex, setEnabledCardIndex] = useState<number | null>(null);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const toggleHcef = useCallback(
|
||||
async (index: number) => {
|
||||
const card = cards[index];
|
||||
@ -83,15 +86,19 @@ const CardList = (props: { cards: Card[] }) => {
|
||||
(index: number) => {
|
||||
const card = cards[index];
|
||||
|
||||
Alert.alert('카드 삭제', `"${card.name}" 카드를 삭제하시겠습니까?`, [
|
||||
{
|
||||
text: '삭제',
|
||||
onPress: () => {
|
||||
deleteMutation.mutate(index);
|
||||
Alert.alert(
|
||||
t('alert.title.card_remove'),
|
||||
t('alert.body.card_remove', { cardName: card.name }),
|
||||
[
|
||||
{
|
||||
text: t('alert.button.confirm'),
|
||||
onPress: () => {
|
||||
deleteMutation.mutate(index);
|
||||
},
|
||||
},
|
||||
},
|
||||
{ text: '취소' },
|
||||
]);
|
||||
{ text: t('alert.button.cancel') },
|
||||
],
|
||||
);
|
||||
},
|
||||
[cards, deleteMutation],
|
||||
);
|
||||
@ -122,8 +129,8 @@ const CardList = (props: { cards: Card[] }) => {
|
||||
onDelete={onDelete}
|
||||
mainText={
|
||||
card.index === enabledCardIndex
|
||||
? '터치해서 비활성화'
|
||||
: '터치해서 활성화'
|
||||
? t('card.touch_to_activate')
|
||||
: t('card.touch_to_deactivate')
|
||||
}
|
||||
/>
|
||||
)}
|
||||
@ -135,7 +142,7 @@ const CardList = (props: { cards: Card[] }) => {
|
||||
} else {
|
||||
return (
|
||||
<View style={styles.placeholderContainer}>
|
||||
<PlaceholderText>카드를 추가해 주세요.</PlaceholderText>
|
||||
<PlaceholderText>{t('main.please_add_a_card')}</PlaceholderText>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user