发布于 2026-01-06 1 阅读
0

使用 i18next 实现 React Native 国际化 DEV 的全球展示挑战赛,由 Mux 呈现:展示你的项目!

使用 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


Enter fullscreen mode Exit fullscreen mode

现在我们将为我们的应用程序添加对多种语言的支持。

内容

1. 安装并配置 react-i18next 库

首先,我们需要运行以下命令将react-i18next添加到我们的项目中:



npm i react-i18next i18next


Enter fullscreen mode Exit fullscreen mode

这将安装 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;


Enter fullscreen mode Exit fullscreen mode

所有配置选项的列表可在文档中找到。

然后只需将此文件导入到项目的入口点:对于 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>
  );
}


Enter fullscreen mode Exit fullscreen mode

2. 添加翻译(Google 表格 + 自动脚本)

我们将把所有翻译添加到一个名为“translations ”的单独文件夹中,每个受支持的语言对应一个单独的 JSON 文件。



//translations folder structure

├── translations/
│   ├── be.json
│   ├── de.json
│   ├── en.json
│   ├── es.json
│   ├── fr.json


Enter fullscreen mode Exit fullscreen mode

通常情况下,应用程序的翻译工作由其他团队成员(如果您的团队是国际化的)、聘请的翻译人员或使用专门的翻译工具完成。最便捷的方法之一是将所有翻译存储在Google 表格中,然后自动生成 JSON 文件并将其上传到项目源代码的 translations 文件夹中。

创建一个符合以下结构的 Google 表格:

Google 表格结构

A 列将包含翻译键(HELLO、PRESS 等)。这些值将用作包含翻译的 JSON 文件中的键。BF 列将包含翻译本身,第一行是支持的语言名称(en - 英语,es - 西班牙语,fr - 法语,依此类推)。

添加所有翻译后,Google 表格应如下所示:

谷歌表格翻译

现在让我们进入有趣的部分——编写一个剧本:

  • 将读取 Google 表格中的翻译。
  • 会将它们直接写入项目的 translations 文件夹中,每种语言的翻译都会写入各自的 JSON 文件,并进行正确的格式设置。

为了从 Google 表格读取数据,我们将使用google-spreadsheet库。让我们把它添加到我们的项目中:



npm i google-spreadsheet


Enter fullscreen mode Exit fullscreen mode

接下来我们需要处理的是 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"
}


Enter fullscreen mode Exit fullscreen mode

在你的 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,
  });
};


Enter fullscreen mode Exit fullscreen mode

您可以在 Google 表格网址中找到电子表格 ID:



https://docs.google.com/spreadsheets/d/spreadsheetId/edit#gid=0


Enter fullscreen mode Exit fullscreen mode

我的电子表格 ID 如下所示:



1hDB6qlijcU5iovtSAisKqkcXhdVboFd1lg__maKwvDI


Enter fullscreen mode Exit fullscreen mode

现在编写一个函数,从我们的 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;
};


Enter fullscreen mode Exit fullscreen mode

如果你运行这个脚本



cd utils && node script.js


Enter fullscreen mode Exit fullscreen mode

并将结果对象打印出来(在 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: 'Прэс' }
}


Enter fullscreen mode Exit fullscreen mode

接下来,我们需要将此结果对象写入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);
        }
      }
    );
  });
};


Enter fullscreen mode Exit fullscreen mode

所以,就是这样:

  • 我们将读取函数的结果对象作为参数传递给它。
  • 遍历此对象的键
  • 使用 Node.js 文件系统模块 (fs) 将结果对象(例如 translations)的键值写入 JSON 文件,并使用 JSON.stringify() 方法进行格式化。

最后,将以上所有异步方法串联起来:



//utils/script.js
...

init()
  .then(() => read())
  .then((data) => write(data))
  .catch((err) => console.log("ERROR!!!!", err));


Enter fullscreen mode Exit fullscreen mode

现在,如果您再次运行脚本:



node script.js


Enter fullscreen mode Exit fullscreen mode

所有翻译内容都应该以单独的 JSON 文件形式保存在translations文件夹中,每种语言对应一个 JSON 文件。

翻译文件夹

为了能够在 React Native 项目中使用这些翻译,我们需要:

  1. 从翻译文件夹中导出这些 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";


Enter fullscreen mode Exit fullscreen mode
  1. 更新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,
  },
};

...



Enter fullscreen mode Exit fullscreen mode

现在我们可以借助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


Enter fullscreen mode Exit fullscreen mode

要在应用程序中切换支持的语言,请构建语言选择器组件:



//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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

现在让我们来看看它是如何运作的:

演示

3. 自定义插件,用于将所选语言存储在本地存储中

国际化功能运行正常,但如果能存储用户的语言选择,以便在用户打开应用程序后默认使用其先前选择的语言,岂不是更好?

i18next 提供了多个 React Native插件来增强现有功能。但让我们尝试从头开始编写一个自定义插件:

  • 将用户的语言选择存储在异步存储中
  • 应用启动时从异步存储中获取已保存的语言。
  • 如果异步存储中没有任何内容,则检测设备语言。如果设备不支持该语言,则使用备用语言。

如何创建自定义插件在 i18next文档的单独章节中有详细介绍。就我们的用例而言,我们需要一个languageDetector 插件

让我们动手实践吧!

安装 @react-native-async-storage/async-storage 库

  • 博览会应用程序


expo install @react-native-async-storage/async-storage


Enter fullscreen mode Exit fullscreen mode
  • 适用于 React Native CLI 或 Expo 裸 React Native 应用


npm i @react-native-async-storage/async-storage


Enter fullscreen mode Exit fullscreen mode

iOS 额外步骤(Expo 项目不需要):



npx pod-install


Enter fullscreen mode Exit fullscreen mode

安装 expo-localization 库



expo install expo-localization


Enter fullscreen mode Exit fullscreen mode

对于 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 };


Enter fullscreen mode Exit fullscreen mode

- 更新 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
    },
  });

...



Enter fullscreen mode Exit fullscreen mode

就这样!我们已经为 React Native 应用添加了国际化功能!

完整的源代码可以在这个仓库中找到。

文章来源:https://dev.to/ramonak/react-native-internationalization-with-i18next-568n