1
0
mirror of synced 2024-11-27 17:00:55 +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 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>

View File

@ -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
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 { 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>

View File

@ -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>
);
}