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 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>
|
||||||
|
@ -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
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 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>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user