使用 i18next 实现 React Native 国际化
由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!
原文发表于我的个人博客
我们要建造什么?
我们将构建一个 React Native 应用,该应用:
- 支持多种语言(使用react-i18next库)
- 从 Google 表格获取翻译并直接写入应用程序(使用google-spreadsheet库)
- 根据用户设备区域设置设置默认语言
- 将用户的语言选择存储在异步存储中
- 包含一个语言选择器组件
引言
假设我们有一个基本的 React Native 项目。在本博客文章中,我将使用Expo项目,但对于使用 React Native CLI 初始化的项目,步骤也相同。
该应用程序只有一个屏幕,上面显示文本“Hello!”和一个标题为“Press”的按钮。
源代码:
//App.tsx
import { StatusBar } from "expo-status-bar";
import React from "react";
import { StyleSheet, Text, View, Button } from "react-native";
export default function App() {
return (
<View style={styles.container}>
<Text style={styles.text}>Hello!</Text>
<Button title="Press" onPress={() => Alert.alert("HELLO")} />
<StatusBar style="auto" />
</View>
);
}
//styles omitted
现在我们将为我们的应用程序添加对多种语言的支持。
内容
1. 安装并配置 react-i18next 库
首先,我们需要运行以下命令将react-i18next添加到我们的项目中:
npm i react-i18next i18next
这将安装 i18next 框架及其 React 封装器。
接下来,我们需要通过创建一个新文件来配置它,比如在项目根目录下创建一个i18n.config.ts文件(或者你喜欢的任何其他名称):
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
//empty for now
const resources = {};
i18n.use(initReactI18next).init({
resources,
//language to use if translations in user language are not available
fallbackLng: "en",
interpolation: {
escapeValue: false, // not needed for react!!
},
});
export default i18n;
所有配置选项的列表可在文档中找到。
然后只需将此文件导入到项目的入口点:对于 Expo 项目,导入 App.tsx;对于 React Native CLI 项目,导入 index.ts/index.js。
//App.tsx
import { StatusBar } from "expo-status-bar";
import React from "react";
import { StyleSheet, Text, View, Button } from "react-native";
import "./i18n.config"; // <-- this line added
export default function App() {
return (
<View style={styles.container}>
<Text style={styles.text}>Hello!</Text>
<Button title="Press" onPress={() => Alert.alert("HELLO")} />
<StatusBar style="auto" />
</View>
);
}
2. 添加翻译(Google 表格 + 自动脚本)
我们将把所有翻译添加到一个名为“translations ”的单独文件夹中,每个受支持的语言对应一个单独的 JSON 文件。
//translations folder structure
├── translations/
│ ├── be.json
│ ├── de.json
│ ├── en.json
│ ├── es.json
│ ├── fr.json
通常情况下,应用程序的翻译工作由其他团队成员(如果您的团队是国际化的)、聘请的翻译人员或使用专门的翻译工具完成。最便捷的方法之一是将所有翻译存储在Google 表格中,然后自动生成 JSON 文件并将其上传到项目源代码的 translations 文件夹中。
创建一个符合以下结构的 Google 表格:
A 列将包含翻译键(HELLO、PRESS 等)。这些值将用作包含翻译的 JSON 文件中的键。BF 列将包含翻译本身,第一行是支持的语言名称(en - 英语,es - 西班牙语,fr - 法语,依此类推)。
添加所有翻译后,Google 表格应如下所示:
现在让我们进入有趣的部分——编写一个剧本:
- 将读取 Google 表格中的翻译。
- 会将它们直接写入项目的 translations 文件夹中,每种语言的翻译都会写入各自的 JSON 文件,并进行正确的格式设置。
为了从 Google 表格读取数据,我们将使用google-spreadsheet库。让我们把它添加到我们的项目中:
npm i google-spreadsheet
接下来我们需要处理的是 v4 版 Google Sheets API 的身份验证。您可以在 google-sheet 库的文档中找到相关信息。在本篇博文中,我将使用服务帐户选项。
按照文档中的步骤操作后,您应该会得到一个包含以下键的 JSON 文件:
{
"type": "service_account",
"project_id": "XXXXXXXXXXXXXXX",
"private_key_id": "XXXXXXXXXXXXXXX",
"private_key": "XXXXXXXXXXXXXXX",
"client_email": "service-account-google-sheet-a@XXXXXXXXXXXX.iam.gserviceaccount.com",
"client_id": "XXXXXXXXXXXXXXX",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/service-account-google-sheet-XXXXXXXXXXXXX.iam.gserviceaccount.com"
}
在你的 React Native 项目中创建另一个文件夹(我将其命名为utils),并将此 JSON 文件放入其中。别忘了将其添加到 .gitignore 文件中!
现在在 React Native 项目中初始化一个 Google 表格实例。
//utils/script.js
const { GoogleSpreadsheet } = require("google-spreadsheet");
const secret = require("./secret.json");
// Initialize the sheet
const doc = new GoogleSpreadsheet("<YOUR_GOOGLE_SHEET_ID");
// Initialize Auth
const init = async () => {
await doc.useServiceAccountAuth({
client_email: secret.client_email, //don't forget to share the Google sheet with your service account using your client_email value
private_key: secret.private_key,
});
};
您可以在 Google 表格网址中找到电子表格 ID:
https://docs.google.com/spreadsheets/d/spreadsheetId/edit#gid=0
我的电子表格 ID 如下所示:
1hDB6qlijcU5iovtSAisKqkcXhdVboFd1lg__maKwvDI
现在编写一个函数,从我们的 Google 表格中读取带有翻译的数据。
//utils/script.js
...
const read = async () => {
await doc.loadInfo(); // loads document properties and worksheets
const sheet = doc.sheetsByTitle.Sheet1; //get the sheet by title, I left the default title name. If you changed it, then you should use the name of your sheet
await sheet.loadHeaderRow(); //Loads the header row (first row) of the sheet
const colTitles = sheet.headerValues; //array of strings from cell values in the first row
const rows = await sheet.getRows({ limit: sheet.rowCount }); //fetch rows from the sheet (limited to row count)
let result = {};
//map rows values and create an object with keys as columns titles starting from the second column (languages names) and values as an object with key value pairs, where the key is a key of translation, and value is a translation in a respective language
rows.map((row) => {
colTitles.slice(1).forEach((title) => {
result[title] = result[title] || [];
const key = row[colTitles[0]];
result = {
...result,
[title]: {
...result[title],
[key]: row[title] !== "" ? row[title] : undefined,
},
};
});
});
return result;
};
如果你运行这个脚本
cd utils && node script.js
并将结果对象打印出来(在 return 语句前添加 console.log(result)),你应该会得到以下结果:
{
en: { HELLO: 'Hello', PRESS: 'Press' },
fr: { HELLO: 'Bonjour', PRESS: 'Presse' },
es: { HELLO: 'Hola', PRESS: 'Prensa' },
de: { HELLO: 'Hallo', PRESS: 'Drücken Sie' },
be: { HELLO: 'Прывітанне', PRESS: 'Прэс' }
}
接下来,我们需要将此结果对象写入translations文件夹,每个键对应一个文件。
//utils/script.js
...
const fs = require("fs");
...
const write = (data) => {
Object.keys(data).forEach((key) => {
fs.writeFile(
`../translations/${key}.json`,
JSON.stringify(data[key], null, 2),
(err) => {
if (err) {
console.error(err);
}
}
);
});
};
所以,就是这样:
- 我们将读取函数的结果对象作为参数传递给它。
- 遍历此对象的键
- 使用 Node.js 文件系统模块 (fs) 将结果对象(例如 translations)的键值写入 JSON 文件,并使用 JSON.stringify() 方法进行格式化。
最后,将以上所有异步方法串联起来:
//utils/script.js
...
init()
.then(() => read())
.then((data) => write(data))
.catch((err) => console.log("ERROR!!!!", err));
现在,如果您再次运行脚本:
node script.js
所有翻译内容都应该以单独的 JSON 文件形式保存在translations文件夹中,每种语言对应一个 JSON 文件。
为了能够在 React Native 项目中使用这些翻译,我们需要:
- 从翻译文件夹中导出这些 JSON 文件
//utils/index.js
export { default as be } from "./be.json";
export { default as en } from "./en.json";
export { default as de } from "./de.json";
export { default as es } from "./es.json";
export { default as fr } from "./fr.json";
- 更新i18n.config.ts文件:
//i18n.config.ts
...
import { en, be, fr, de, es } from "./translations";
const resources = {
en: {
translation: en,
},
de: {
translation: de,
},
es: {
translation: es,
},
be: {
translation: be,
},
fr: {
translation: fr,
},
};
...
现在我们可以借助react-i18next 库提供的useTranslation hook 来翻译应用程序的内容。
//App.tsx
...
import { useTranslation } from "react-i18next";
export default function App() {
const { t } = useTranslation();
return (
<View style={styles.container}>
<Text style={styles.text}>{`${t("HELLO")}!`}</Text>
<Button title={t("PRESS")} onPress={() => Alert.alert(t("HELLO"))} />
<StatusBar style="auto" />
</View>
);
}
//styles omitted
要在应用程序中切换支持的语言,请构建语言选择器组件:
//LanguagePicker.tsx
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Modal, View, Text, Pressable, StyleSheet } from "react-native";
const LanguagePicker = () => {
const [modalVisible, setModalVisible] = useState(false);
const { i18n } = useTranslation(); //i18n instance
//array with all supported languages
const languages = [
{ name: "de", label: "Deutsch" },
{ name: "en", label: "English" },
{ name: "fr", label: "Français" },
{ name: "be", label: "Беларуская" },
{ name: "es", label: "Español" },
];
const LanguageItem = ({ name, label }: { name: string; label: string }) => (
<Pressable
style={styles.button}
onPress={() => {
i18n.changeLanguage(name); //changes the app language
setModalVisible(!modalVisible);
}}
>
<Text style={styles.textStyle}>{label}</Text>
</Pressable>
);
return (
<View>
<Modal
animationType="slide"
transparent={true}
visible={modalVisible}
onRequestClose={() => {
setModalVisible(!modalVisible);
}}
>
<View style={styles.centeredView}>
<View style={styles.modalView}>
{languages.map((lang) => (
<LanguageItem {...lang} key={lang.name} />
))}
</View>
</View>
</Modal>
<Pressable
style={[styles.button, styles.buttonOpen]}
onPress={() => setModalVisible(true)}
>
//displays the current app language
<Text style={styles.textStyle}>{i18n.language}</Text>
</Pressable>
</View>
);
};
export default LanguagePicker;
//styles omitted
在 App.tsx 文件中添加语言选择器组件:
//App.tsx
import { StatusBar } from "expo-status-bar";
import React from "react";
import { StyleSheet, Text, View, Button } from "react-native";
import "./i18n.config";
import { useTranslation } from "react-i18next";
import LanguagePicker from "./LanguagePicker";
export default function App() {
const { t } = useTranslation();
return (
<View style={styles.container}>
<LanguagePicker />
<Text style={styles.text}>{`${t("HELLO")}!`}</Text>
<Button title={t("PRESS")} onPress={() => Alert.alert(t("HELLO"))} />
<StatusBar style="auto" />
</View>
);
}
现在让我们来看看它是如何运作的:
3. 自定义插件,用于将所选语言存储在本地存储中
国际化功能运行正常,但如果能存储用户的语言选择,以便在用户打开应用程序后默认使用其先前选择的语言,岂不是更好?
i18next 提供了多个 React Native插件来增强现有功能。但让我们尝试从头开始编写一个自定义插件:
- 将用户的语言选择存储在异步存储中
- 应用启动时从异步存储中获取已保存的语言。
- 如果异步存储中没有任何内容,则检测设备语言。如果设备不支持该语言,则使用备用语言。
如何创建自定义插件在 i18next文档的单独章节中有详细介绍。就我们的用例而言,我们需要一个languageDetector 插件。
让我们动手实践吧!
安装 @react-native-async-storage/async-storage 库
- 博览会应用程序
expo install @react-native-async-storage/async-storage
- 适用于 React Native CLI 或 Expo 裸 React Native 应用
npm i @react-native-async-storage/async-storage
iOS 额外步骤(Expo 项目不需要):
npx pod-install
安装 expo-localization 库
expo install expo-localization
对于 React Native CLI 或 expo 裸 React Native 应用,也请遵循以下附加安装说明。
- 编写语言检测器插件
//utils/languageDetectorPlugin.ts
import AsyncStorage from "@react-native-async-storage/async-storage";
import * as Localization from "expo-localization";
const STORE_LANGUAGE_KEY = "settings.lang";
const languageDetectorPlugin = {
type: "languageDetector",
async: true,
init: () => {},
detect: async function (callback: (lang: string) => void) {
try {
//get stored language from Async storage
await AsyncStorage.getItem(STORE_LANGUAGE_KEY).then((language) => {
if (language) {
//if language was stored before, use this language in the app
return callback(language);
} else {
//if language was not stored yet, use device's locale
return callback(Localization.locale);
}
});
} catch (error) {
console.log("Error reading language", error);
}
},
cacheUserLanguage: async function (language: string) {
try {
//save a user's language choice in Async storage
await AsyncStorage.setItem(STORE_LANGUAGE_KEY, language);
} catch (error) {}
},
};
module.exports = { languageDetectorPlugin };
- 更新 i18n 配置文件
//i18n.config.ts
...
const { languageDetectorPlugin } = require("./utils/languageDetectorPlugin");
...
i18n
.use(initReactI18next)
.use(languageDetectorPlugin)
.init({
resources,
//language to use if translations in user language are not available
fallbackLng: "en",
interpolation: {
escapeValue: false, // not needed for react!!
},
react: {
useSuspense: false, //in case you have any suspense related errors
},
});
...
就这样!我们已经为 React Native 应用添加了国际化功能!
完整的源代码可以在这个仓库中找到。
文章来源:https://dev.to/ramonak/react-native-internationalization-with-i18next-568n




