react native(expo)多语言适配
项目基于 expo框架 开发。请先配置好 expo 开发环境
1.引入i18n-js
npx expo install i18n-js
2.新建languages文件夹,其中包括英文、中文等语种目录。结构如下:
*.json文件为语种翻译后的json键值对,用于UI中引用;
{
"appName": "xxxx",
"appVersion": "xxxx",
"Login": {
"login": "登录"
},
"Register": {
"register": "注册"
},
"PaymentWay": {
"alipay": "支付宝",
"wxpay": "微信支付",
"bank": "银行卡",
"Instant": "即时支付",
"reviewing": "审核中",
"ReviewRejected": "审核拒绝",
"ReviewSuccess": "审核通过",
"walletAddress": "钱包地址",
"walletDetail": "钱包详情",
"info1": "买家将直接使用您选择的收款方式付款。交易时,请始终检查您的收款账户以确认您已收到全额付款。",
"info2": "请确保您设置的账户为本人实名账户,非本人实名账户付款会导致订单失败且账号被冻结。",
"PaymentTerms": "收付款方式",
"PaymentTermsDetail": "收付款方式详情",
"SelectPaymentTerms": "选择收付款方式",
"SelectIncomeTerms": "请选择收款方式",
"IncomeTerms": "选择收款方式",
"selectCurrency": "请选择货币",
"Currency": "选择货币",
"NotImgMessage": "未读取到图片信息",
"realName": "请输入真实姓名",
"Name": "姓名",
"Account": "账号",
"AccountLimit": "账号长度必须在%{min_limit}-%{max_limit}之间",
"selectedPaymentWayTips": "请先选择收付款方式",
"PaymentTermsInfo": "请补全收付款方式信息",
"bankName": "请输入银行名称",
"bankName2": "银行名称",
"openBankName": "请输入开户行",
"openBankName2": "开户行",
"qrcode": "请上传二维码",
"qrcode2": "二维码",
"addPaymentTerms": "添加收付款账号",
"info3": "某些支付方式可能会有支付服务提供方设定的手续费和每日限额,请联系支付服务提供方了解详情。",
"edit": "完成修改",
"add": "完成添加"
}
}
languages下的index.js配置如下:
import {getLocales} from "expo-localization";
import {I18n} from "i18n-js";
import {enStringJson} from "./en";
import {jaStringJson} from "./japanese";
import {zhStringJson} from "./zh";
import LogUtil from "../../utils/log_util";
import {zhTwStringJson} from "./zh_tw";
import {esStringJson} from "./es";
import AsyncStorage from "@react-native-async-storage/async-storage";
// Set the key-value pairs for the different languages you want to support.
const translations = {
'en': enStringJson,
'en-US': enStringJson,
// ja: jaStringJson,
'zh': zhStringJson,
'zh-CN': zhStringJson,
'zh-Hans-CN': zhStringJson,
'zh-TW': zhTwStringJson,
'zh-Hant-TW': zhTwStringJson,
'zh-HK': zhTwStringJson,
'zh-SG': zhTwStringJson,
'es': esStringJson,
'es-ES': esStringJson,
'es-AD': esStringJson,
'es-AR': esStringJson,
'es-US': esStringJson,
'es-MX': esStringJson,
'es-GT': esStringJson,
};
export const i18n = new I18n(translations);
export async function initI18n() {
let res = await AsyncStorage.getItem('language');
if (res) {
i18n.locale = res;
} else {
// Set the locale once at the beginning of your app.
let languageTag = getLocales()[0].languageTag;
if (translations[languageTag]) {
i18n.locale = languageTag;
await AsyncStorage.setItem('language', languageTag);
} else {
i18n.locale = 'en-US';
await AsyncStorage.setItem('language', 'en-US');
}
}
// When a value is missing from a language it'll fall back to another language with the key present.
i18n.enableFallback = true;
// To see the fallback mechanism uncomment the line below to force the app to use the Japanese language.
// i18n.locale = 'ja';
LogUtil.log('initI18n languageCode = ', getLocales()[0].languageCode, '', getLocales()[0].languageTag);
return i18n.locale;
}
3.在app.js中初始化引用
import {Provider, Toast} from "@ant-design/react-native";
import VConsole from '@kafudev/react-native-vconsole';
import * as Font from 'expo-font';
import { useEffect, useState } from "react";
import { StatusBar, View } from 'react-native';
import Loading from "./components/ui/Loading";
import Navigations from './navigations';
import {initI18n} from "./assets/languages";
import {JPushInit} from "./utils/jpush_utils";
import { eventBus } from "./utils/eventbus_util";
import LogUtil from "./utils/log_util";
import { EVENT } from "./constants/event";
import enUS from '@ant-design/react-native/lib/locale-provider/en_US'
import zhCN from '@ant-design/react-native/lib/locale-provider/zh_CN'
export default () => {
StatusBar.setBarStyle('dark-content')
const [isReady, setReady] = useState(false);
let [currentLocale, setCurrentLocale] = useState();
useEffect(() => {
loadAtdFont().then(res => { });
}, []);
useEffect(() => {
let listener = eventBus.addListener(EVENT.GLOBAL_REFRESH_LANGUAGE, (args) => {
LogUtil.log('application GLOBAL_REFRESH_LANGUAGE args', args);
setCurrentLocale(args?.language?.includes('zh') ? zhCN : enUS);
});
return () => {
listener.remove()
}
}, [])
const loadAtdFont = async () => {
// 初始化JPush
// JPushInit();
// initial多语言
const localTag = await initI18n();
setCurrentLocale(localTag.includes('zh') ? zhCN : enUS);
await Font.loadAsync(
'antoutline',
// eslint-disable-next-line
require('@ant-design/icons-react-native/fonts/antoutline.ttf')
);
await Font.loadAsync(
'antfill',
// eslint-disable-next-line
require('@ant-design/icons-react-native/fonts/antfill.ttf')
);
// 吐司全局配置
Toast.config({ duration: 1.5 });
// eslint-disable-next-line
setReady(true);
}
return !isReady ? (
<Loading />
) : (<Provider theme={{}} locale={currentLocale}>
<View style={{ flex: 1 }}>
<StatusBar translucent={true} backgroundColor="rgba(0, 0, 0, 0)" />
<Navigations />
{process.env.EXPO_PUBLIC_ConsoleFetch ? (
<VConsole
// 使用 'react-native-config-reader' 库获获取额外信息
appInfo={{
原生构建类型: 'all',
原生版本号: '0.0.1',
原生构建时间: 'none',
热更新版本号: '00001',
热更新详情: 'UI更新',
}}
// 另外的的面板
// panels={panels}
// console.time 可辨别是否开启 debug 网页
console={process.env.EXPO_PUBLIC_ConsoleFetch ? !console.time : true}
/>
) : null}
</View>
</Provider>
);
}
4.代码中引用
i18n.t('appName');
i18n.t('Login.login');
i18n.t('PaymentWay.AccountLimit', { min_limit: 10, max_limit: 100 });
5.多语言切换功能
5.1 引入ant 适用于react native的UI库
@ant-design/react-native
@react-native-async-storage/async-storage
新建eventBus、EVENT类用于切换语种后通知页面刷新
import RCTDeviceEventEmitter from 'react-native/Libraries/EventEmitter/RCTDeviceEventEmitter';
export const eventBus = RCTDeviceEventEmitter;
export const EVENT = {
GLOBAL_REFRESH_LANGUAGE: 'GLOBAL_REFRESH_LANGUAGE',
}
新建AntPopup组件用于弹窗选择语种
import react from "react";
import {scaleSize, screenH} from "../../utils/screen_util";
import {Modal} from "@ant-design/react-native";
import React from "react";
import TextWrapper from "./TextWrapper";
import stl from "../../stl";
import ViewWrapper from "./ViewWrapper";
const AntPopup = (props) => {
return (
<Modal
style={{
borderTopStartRadius: scaleSize(6),
borderTopEndRadius: scaleSize(6),
}}
popup={props.popup ?? true}
transparent={props.transparent ?? false}
maskClosable={true}
visible={props.showPopup}
animationType="slide-up"
onClose={props.onClose}
onRequestClose={props.onRequestClose}
>
<ViewWrapper style={[{ maxHeight: screenH / 2, paddingVertical: scaleSize(20), paddingHorizontal: 0, alignItems: 'center'}, props.style]}>
{props.title ? (
<TextWrapper style={[stl.fontSize19, stl.FC626262, stl.MB27]}>
{props.title}
</TextWrapper>
) : null}
{props.children}
</ViewWrapper>
</Modal>
);
}
export default react.memo(AntPopup);
5.2 新建语种切换页面 AnotherSettingScreen
import ViewWrapper from "../../../components/ui/ViewWrapper";
import HeaderView2 from "../../../components/HeaderView2";
import NavPaddingGray from "../../../components/ui/NavPaddingGray";
import ArrowLineStyle from "../../../components/ui/ArrowLineStyle";
import React, {useEffect} from "react";
import {scaleSize} from "../../../utils/screen_util";
import AntPopup from "../../../components/ui/AntPopup";
import stl from "../../../stl";
import {Radio} from "@ant-design/react-native";
import ImageWrapper from "../../../components/ui/ImageWrapper";
import TextWrapper from "../../../components/ui/TextWrapper";
import LogUtil from "../../../utils/log_util";
import {i18n} from "../../../assets/languages";
import {getLocales, useLocales} from "expo-localization";
import AsyncStorage from "@react-native-async-storage/async-storage";
import {View} from "react-native";
import {eventBus} from "../../../utils/eventbus_util";
import {EVENT} from "../../../constants/event";
import { APP_VERSION_CODE } from "../../../constants/common";
import Loading from "../../../components/ui/Loading";
const AnotherSettingScreen = ({route, navigation}) => {
const languageOptions = {
type: 'language',
title: i18n.t('MyRoute.anotherSetting.selectLang'),
list: [
{icon: '', value: 'zh-Hans-CN', label: i18n.t('MyRoute.anotherSetting.zh'), desc: ''},
{icon: '', value: 'zh-Hant-TW', label: i18n.t('MyRoute.anotherSetting.zh-tw'), desc: ''},
{icon: '', value: 'en-US', label: i18n.t('MyRoute.anotherSetting.en-US'), desc: ''},
// {icon: '', value: 'es-ES', label: i18n.t('MyRoute.anotherSetting.es-ES'), desc: ''}
]
}
const [showPopup, setShowPopup] = React.useState(false);
const [selectValue, setSelectValue] = React.useState();
const [currentLanguage, setCurrentLanguage] = React.useState();
useEffect(() => {
AsyncStorage.getItem('language').then(res => {
LogUtil.log('AsyncStorage.getItem language = ', res);
if (res) {
setSelectValue(res);
setCurrentLanguage(formatCurrentLanguage(res));
}
})
}, []);
useLocales();
function formatCurrentLanguage(value) {
LogUtil.log('formatCurrentLanguage value = ', value);
return value === 'zh-Hans-CN' ? i18n.t('MyRoute.anotherSetting.zh') : (value === 'zh-Hant-TW' ? i18n.t('MyRoute.anotherSetting.zh-tw') : (value === 'en-US' ? i18n.t('MyRoute.anotherSetting.en-US') : (value === 'es-ES' ? i18n.t('MyRoute.anotherSetting.es-ES') : null)));
}
function handleChangeLang() {
setShowPopup(true);
}
function handleUpdateAppVersion() {
}
const onChange = async (event, type) => {
LogUtil.log('radio checked', event.target.value);
// let eventToJson = JSON.parse(event.target.value);
Loading.show();
await AsyncStorage.setItem('language', event.target.value).then(r => { });
i18n.locale = event.target.value;
setSelectValue(event.target.value);
setCurrentLanguage(formatCurrentLanguage(event.target.value));
eventBus.emit(EVENT.GLOBAL_REFRESH_LANGUAGE, { language: event.target.value });
Loading.hide();
setShowPopup(false);
}
return (
<ViewWrapper>
<HeaderView2 title={i18n.t('MyRoute.anotherSetting.setting')}/>
<NavPaddingGray/>
<ArrowLineStyle
style={{backgroundColor: "white", paddingHorizontal: scaleSize(20.5)}}
title={i18n.t('MyRoute.anotherSetting.switchLang')}
subTitle={currentLanguage}
onPress={() => {
handleChangeLang();
}}
/>
<ArrowLineStyle
style={{backgroundColor: "white", paddingHorizontal: scaleSize(20.5)}}
title={i18n.t('MyRoute.anotherSetting.updateApp')}
subTitle={i18n.t('version') + `:${APP_VERSION_CODE}`}
onPress={() => {
handleUpdateAppVersion();
}}
/>
<AntPopup
title={languageOptions.title}
showPopup={showPopup}
onClose={() => {
setShowPopup(false)
}}
onRequestClose={() => {
setShowPopup(false);
return true;
}}>
<ViewWrapper style={[stl.widthPercent100]}>
<>
<Radio.Group
styles={{checkbox_label: {fontSize: scaleSize(15), color: '#626262'}}}
// options={options}
onChange={onChange}
value={selectValue}>
{languageOptions?.list?.map((option, index) => (
<Radio.RadioItem
key={index}
styles={{
Line: { paddingRight: scaleSize(4) },
Item: { paddingLeft: scaleSize(0) },
Content: { paddingLeft: scaleSize(20) },
}}
value={option.value}
>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
{option.icon ? (
<ImageWrapper
style={{
width: scaleSize(16),
height: scaleSize(16),
}}
source={option.icon}
/>
) : null}
<ViewWrapper style={{width: scaleSize(12)}}/>
<TextWrapper style={[stl.fontSize15, stl.FC626262]}>{option.label}</TextWrapper>
<ViewWrapper style={{width: scaleSize(12)}}/>
<TextWrapper style={[stl.fontSize12, stl.FCB2B2B2]}>{option.desc}</TextWrapper>
</View>
</Radio.RadioItem>
))}
</Radio.Group>
</>
</ViewWrapper>
</AntPopup>
</ViewWrapper>
);
}
export default AnotherSettingScreen;
5.3 新建一个测试页面 mainPage用于验证切换语种后文案刷新
import React, { useEffect, useState } from 'react';
const MainPageScreen = ({route, navigation}) => {
let [fresh, setFresh] = useState(1);
useEffect(() => {
let listener = eventBus.addListener(EVENT.GLOBAL_REFRESH_LANGUAGE, (args) => {
LogUtil.log('MainPageScreen GLOBAL_REFRESH_LANGUAGE args', args);
// 切换了语种,通知页面刷新
setFresh(prevState => prevState + 1);
});
return () => {
listener.remove()
}
}, [])
return (
<View>
<Text>{ i18n.t('appName') }</Text>
<Text>{ i18n.t('Login.login') }</Text>
<Text>{ i18n.t('PaymentWay.AccountLimit', { min_limit: 10, max_limit: 100 }) }</Text>
<Button onPress={()=>{
navigation.push('AnotherSettingScreen');
}}>
跳语种切换页面
</Button>
</View>
);
}