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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值