1. 首页
  2. React

Vite6+React18+Ts项目-06.集成i18next多语言

Vite6+React18+Ts项目模板-06.集成i18next多语言

01. 安装依赖

首先安装必要的依赖:

npm install i18next react-i18next i18next-http-backend i18next-browser-languagedetector

02. 配置 i18n

创建一个 src\langs\i18n.ts 配置文件:

import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import Backend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';

i18n
  .use(Backend) // 启用从后端加载翻译文件
  .use(LanguageDetector) // 启用浏览器语言检测
  .use(initReactI18next) // 绑定 React
  .init({
    fallbackLng: 'en-GB', // 默认语言,初次加载时,首先会加载默认语言,然后加载当前语言,也就是说初次加载时会加载两种语言
    load: 'currentOnly', // 仅加载当前语言
    interpolation: {
      escapeValue: false, // React 已经处理了 XSS 防护
    },
    backend: {
      loadPath: 'http://demo.com/lang.php?code={{lng}}', // 从后端加载翻译文件的路径
    },
    detection: {
      // 自定义 localStorage 的 key,这里面存的是当前语言类型,不是语言翻译内容
      lookupLocalStorage: 'my_custom_lng_key',
      // 自定义 cookie 的 key,这里面存的是当前语言类型,不是语言翻译内容
      lookupCookie: 'my_custom_lng_cookie_key',
      // 语言检测的选项,当前语言会按照顺序依次从以下选择中查询当前语言
      order: ['localStorage', 'cookie', 'navigator'],
      // 缓存检测到的语言
      caches: ['localStorage', 'cookie'],
      // 语言代码转换,可以将当前语言代码进行转换,比如将 en 转换为 en-GB
      convertDetectedLanguage: (code) => {
        //alert(code);
        return code;
      },
    },
  });

export default i18n;

03. 在 React 应用中使用

在应用的入口文件(main.ts)中引入 i18n:

import ReactDOM from "react-dom/client";

// 引入 i18n 配置
import "@/langs/i18n";

// 引入 App 组件
import App from "@/app.tsx";

ReactDOM.createRoot(document.getElementById("root")!).render(<App />);

04. 创建语言切换组件

创建一个语言切换组件 LanguageSwitcher.js

import { useTranslation } from 'react-i18next';

const LanguageSwitcher = () => {
  const { i18n } = useTranslation();

  const changeLanguage = (lng: string) => {
    i18n.changeLanguage(lng);
  };

  return (
    <div>
      <button onClick={() => changeLanguage('en-GB')}>English</button>
      <button onClick={() => changeLanguage('zh-CN')}>中文简体</button>
    </div>
  );
};

export default LanguageSwitcher;

05. 在组件中使用翻译

在任何组件中使用翻译:

import { useTranslation } from 'react-i18next';

const MyComponent = () => {
  const { t } = useTranslation();

  return (
    <div>
      <h1>{t('welcome')}</h1>
    </div>
  );
};

export default MyComponent;

06. 后端返回的语言文件结构

确保后端提供正确的语言文件结构,例如:

{
  "welcome": "Welcome to our application",
  "Sys.MsgInfo": "You hava {{name}} messages。"
}

07. 命名空间

可以使用命名空间来组织翻译:

后端返回的语言格式为嵌套对象方式

{
  "homepage": {
    "title": "首页",
    "description": "欢迎来到我们的网站"
  },
  "product": {
    "title": "产品",
    "description": "我们的产品列表"
  }
}

在代码中使用React + react-i18next 示例

import { useTranslation } from 'react-i18next';

function Component() {
  const { t } = useTranslation('homepage');
  
  return (
    <div>
      <h1>{t('title')}</h1>
      <p>{t('description')}</p>
    </div>
  );
}

08.插值表达式

可以在翻译文件中使用插值表达式

// 翻译文件中
{
  "greeting": "Hello, {{name}}!"
}

// 组件中
t('greeting', { name: 'John' });

09.复数形式

复数形式是国际化(i18n)中处理不同数量下文本变化的重要特性,因为不同语言对数量的表达方式差异很大。是指根据数量不同而变化的文本表达。例如:

  • 英语: "1 apple" vs "2 apples"

  • 中文: "1个苹果" vs "2个苹果" (中文通常不需要复数变化)

  • 俄语: 有更复杂的复数规则

常见实现方式

1. 简单键值对

{
  "apple": "apple | apples",
  "banana": "banana | bananas"
}

2. 结构化形式

{
  "apple": {
    "one": "apple",
    "other": "apples"
  }
}

3.在代码中使用

const { t } = useTranslation();

// 简单形式
t('apple', { count: 1 }); // "apple"
t('apple', { count: 3 }); // "apples"

// 复杂形式
t('apple', { count: 1, plural: 'one' }); // "apple"
t('apple', { count: 3, plural: 'other' }); // "apples"

10.将i18n的语言保存到状态管理

大多数场景不建议将i18n的语言保存到状态管理,因为 i18n 自身已有管理状态,避免重复,微前端架构可能需要保存到状态管理。

1.创建用于保存状态的store文件 src\stores\lang\lang.store.ts

import { create } from 'zustand';
import { createJSONStorage, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';

// type passport
interface Lang {
  code: string;
}

// define the initial state
const init: Lang = {
  code: 'zh-CN',
};

// 存储store的key
const storeKey = 'store-lang';

// create your store
const useLangStore = create<Lang>()(
  immer(
    persist(
      () => ({
        ...init,
      }),
      {
        // name of the item in the storage (must be unique)
        name: storeKey,
        // (optional) by default, 'localStorage' is used
        storage: createJSONStorage(() => localStorage),
      }
    )
  )
);

export default useLangStore;

// 设置语言
export const setLang = (state: Lang) => {
  useLangStore.setState(state);
};

// 重置语言
export const resetLang = () => {
  useLangStore.setState(init);
};

// 监听 localStorage 变化
window.addEventListener('storage', (event) => {
  if (event.key === storeKey && event.newValue !== null) {
    const val = JSON.parse(event.newValue);
    // 这里判断用户状态,如果用户变化了,则进入登录页面或进入首页,根据具体业务自行处理
    if (JSON.stringify(useLangStore.getState()) != JSON.stringify(val.state)) {
      useLangStore.setState(val.state);
    }
  }
});

2.在初始化i18n后,将语言代码保存到状态管理里

// src/i18n.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import Backend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';

import { setLang } from '@/stores/lang/lang.store';

i18n
  .use(Backend) // 启用从后端加载翻译文件
  .use(LanguageDetector) // 启用浏览器语言检测
  .use(initReactI18next) // 绑定 React
  .init({
    fallbackLng: 'en-GB', // 默认语言,初次加载时,首先会加载默认语言,然后加载当前语言,也就是说初次加载时会加载两种语言
    load: 'currentOnly', // 仅加载当前语言
    interpolation: {
      escapeValue: false, // React 已经处理了 XSS 防护
    },
    backend: {
      loadPath: 'http://demo.com/lang.php?code={{lng}}', // 从后端加载翻译文件的路径
    },
    detection: {
      // 自定义 localStorage 的 key,这里面存的是当前语言类型,不是语言翻译内容
      lookupLocalStorage: 'my_custom_lng_key',
      // 自定义 cookie 的 key,这里面存的是当前语言类型,不是语言翻译内容
      lookupCookie: 'my_custom_lng_cookie_key',
      // 语言检测的选项,当前语言会按照顺序依次从以下选择中查询当前语言
      order: ['localStorage', 'cookie', 'navigator'],
      // 缓存检测到的语言
      caches: ['localStorage', 'cookie'],
      // 语言代码转换,可以将当前语言代码进行转换,比如将 en 转换为 en-GB
      convertDetectedLanguage: (code) => {
        //alert(code);
        return code;
      },
    },
  });

// 大多数场景不建议将i18n的语言保存到状态管理,因为 i18n 自身已有管理状态,避免重复,微前端架构可能需要保存到状态管理,也看具体情况
i18n.on('initialized', () => {
  setLang({
    code: i18n.language,
  });
});

export default i18n;

3.封装修改语言的hook函数,在修改语言的同时,也需要修改状态管理里的语言代码

import { useTranslation } from 'react-i18next';
import { setLang } from '@/stores/lang/lang.store';

export const useChangeLanguage = () => {
  const { i18n } = useTranslation();
  const changeLanguage = async (lng: string) => {
    await i18n.changeLanguage(lng);
    setLang({
      code: lng,
    });
  };
  return {
    changeLanguage,
  };
};

4.在组件中使用

function LanguageSwitcher() {
  const { changeLanguage } = useChangeLanguage();
  return (
    <div>
      <button onClick={() => changeLanguage('en-GB')}>English</button>
      <button onClick={() => changeLanguage('zh-CN')}>中文简体</button>
    </div>
  );
}

11.使用常量及注释

通常情况下,我们可以定义需要翻译的内容为常量,在需要翻译的地方调用常量,而不是字符串,同时,不同模块也可以建立不同的文件夹管理

模块sys下的翻译 src\langs\sys\index.ts

const Sys = {
  /**
   * 菜单是必填的
   */
  MenuIsRequire: 'Sys.MenuIsRequire',
  
  /**
   * 消息提示
   */
  MsgInfo: 'Sys.MsgInfo'
};

export default Sys;

将常量导出 src\langs\index.ts

import Sys from "./sys";

const Lang = {
  Sys: Sys
}

export default Lang;

在组件中使用

import Lang from '@/langs';
import { useTranslation } from 'react-i18next';

export default function Demo() {
  const { t } = useTranslation();
  return (
    <div>
      <h1>{t(Lang.Sys.MenuIsRequire)}</h1>
      <h1>{t(Lang.Sys.MsgInfo, { name: name })}</h1>
    </div>
  );
}

鼠标悬浮在常量上, 可以轻松看到注释信息

image.png

12.编写脚本,从后端同步常量文件

通常情况下,我们需要翻译的文本都是后端统一管理,故常量一般不需要我们手动编写,可以直接从后端同步过来,比如下面的node脚本就可以实现这一点

后端文件返回内容

{
  // index.ts 的内容,内容经过base64编码
  "main": "aW5kZXggY29udGVudA==",
  // 各个模块的内容
  "list": [
      {
          "name": "sys",
          // 模块文件内容,内容经过base64编码
          "content": "c3lzIGNvbnRlbnQ="
      }
  ]
}

脚本文件路径: run\sync.js

// 控制台终端运行代码: node run/sync.js 即可同步服务器配置的语言翻译

import fs from 'fs';
import path from 'path';
import axios from 'axios';
import { Buffer } from 'buffer';
import { fileURLToPath } from 'url';

// 请求URL
const url = 'http://demo.com/lang-const.php';

// 目标目录
const dir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../lang');

// 写入文件函数
function writeFile(filePath, content) {
  const s = path.dirname(filePath);
  if (!fs.existsSync(s)) {
    fs.mkdirSync(s, { recursive: true });
  }
  fs.writeFileSync(filePath, content);
}

// 主函数
async function run() {
  try {
    // 请求数据
    const response = await axios.get(url);
    const data = response.data;

    // 写入 index 内容
    writeFile(path.join(dir, 'index.ts'), Buffer.from(data.main, 'base64').toString('utf-8'));

    // 遍历 list 并写入各模块内容
    data.list.forEach(item => {
      writeFile(path.join(path.join(dir, item.name), 'index.ts'), Buffer.from(item.content, 'base64').toString('utf-8'));
    });

    console.log('Sync Language Successfully!');
  } catch (error) {
    console.error('Sync Language Failed:', error);
  }
}

// 执行主函数
run();

TOP