国际化方案:多环境、多语言、动态加载的完整实践

背景

在开发面向全球用户的网页时,需要:

  • 支持多语言(英文、中文等)
  • 按环境(生产/测试/开发)加载不同翻译资源
  • 语言切换时保持当前页面路径
  • 自动检测用户语言偏好
  • 动态加载翻译资源,避免打包体积过大

技术选型

采用 i18next + react-i18next 生态:

  • i18next:核心库
  • react-i18next:React 集成
  • i18next-http-backend:HTTP 后端加载
  • @18n-language-detect:语言检测

核心实现

1. i18n 初始化配置

typescript 复制代码
// src/i18n/i18n.ts
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from "@i18n-language-detect";//需npm上找替代包
import HttpBackend from "i18next-http-backend";
import Backend from "./backend";
import { isTestnet, isProd } from "env";

// 根据环境动态生成资源路径
const fullPath = () => {
  if (isProd) {
    return "i18n-prod.json";
  } else if (isTestnet) {
    return "i18n-testnet.json";
  } else {
    return "i18n.json";
  }
};

const bundledResources = {};

i18n
  .use(Backend)              // 自定义后端
  .use(LanguageDetector)     // 语言检测
  .use(initReactI18next)      // React 集成
  .init({
    lng: "en",               // 默认语言
    fallbackLng: "en",       // 回退语言
    load: "currentOnly",     // 只加载当前语言
    backend: {
      backends: [HttpBackend],
      bundledResources,
      backendOptions: [{ loadPath: fullPath() }],
    },
    ns: ["example"],            // 命名空间
    defaultNS: "example",
    jsonFormat: "v4",
    interpolation: {
      escapeValue: false,    // 不转义 HTML
    },
  });

export default i18n;

notes:1.通过 fullPath() 按环境返回不同资源路径 2.使用 HTTP 后端,按需加载 3.支持多命名空间管理

2. 自定义 Backend 实现

typescript 复制代码
// src/i18n/backend.ts
class Backend {
  constructor(services, options = {}) {
    this.backends = [];
    this.type = 'backend';
    this.init(services, options);
  }

  init(services, options = {}, i18nextOptions) {
    this.services = services;
    this.options = utils.defaults(options, this.options || {}, getDefaults());

    // 支持多个后端,按顺序尝试加载
    this.options.backends &&
      this.options.backends.forEach((b, i) => {
        this.backends[i] = this.backends[i] || utils.createClassOnDemand(b);
        this.backends[i].init(
          services,
          (this.options.backendOptions && this.options.backendOptions[i]) || {},
          i18nextOptions
        );
      });
  }

  read(language, namespace, callback) {
    // 按顺序尝试从各个后端加载资源
    const loadPosition = (pos) => {
      if (pos >= this.backends.length) {
        return callback(new Error('non of the backend loaded data;', true));
      }
      
      const backend = this.backends[pos];
      if (backend.read) {
        const resolver = (err, data) => {
          if (!err && data && Object.keys(data).length > 0) {
            callback(null, data, pos);
            // 保存到前面的后端(如本地缓存)
            savePosition(pos - 1, data);
          } else {
            // 尝试下一个后端
            loadPosition(pos + 1);
          }
        };
        
        // 处理 Promise 或回调
        const result = backend.read(language, namespace);
        if (result && typeof result.then === 'function') {
          result.then((data) => resolver(null, data)).catch(resolver);
        } else {
          resolver(null, result);
        }
      } else {
        loadPosition(pos + 1);
      }
    };

    loadPosition(0);
  }
}

优势:支持多后端,可配置多个后端,按顺序尝试;一个失败时自动尝试下一个;同时支持将远程资源保存到本地缓存

3. 路由集成多语言路径

typescript 复制代码
// src/App.tsx
import { Routes, Route } from "react-router-dom";
import { routesMap } from "./pages/constants";

function App() {
  return (
    <Routes>
      {/* 默认路由(无语言前缀) */}
      <Route path="/">
        <Route index element={<Home/>} />
        <Route path={routesMap.example1} element={<Example1 />} /> 
        {/* ... */}
      </Route>

      {/* 多语言路由(/:lang/...) */}
      <Route path={`/:lang`} element={<Home />} />
      <Route path={`/:lang/${routesMap.example1}`} element={<Example1 />} /> 
      {/* ... */}
    </Routes>
  );
}

4. 语言切换组件

typescript 复制代码
// src/components/languageChange/index.tsx
import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "react-router-dom";
import { getLang, setLang } from "@/utils/storageData";
import { routesMap } from "@/pages/constants";

export default function LanguageChange() {
  const navigate = useNavigate();
  const [t, i18n] = useTranslation("example");
  const { lang } = useParams();
  
  const langList = [
    { name: "EN", key: "en" },
    { name: "中文", key: "zh-MY" },
  ];

  // 切换语言并更新 URL
  const urlLangChange = (paramLang: any) => {
    let currentlang = paramLang;
    
    // 验证语言是否有效
    if (!paramLang || !langList.map((item) => item.key).includes(paramLang)) {
      currentlang = getLang() || "en";
    }
    
    // 提取当前路径(去除语言前缀)
    let pathSegments = window.location.pathname.split("/").slice(1).join("/");
    if (
      window.location.pathname.startsWith("/zh-MY") ||
      window.location.pathname.startsWith("/en")
    ) {
      pathSegments = window.location.pathname.split("/").slice(2).join("/");
    }

    // 验证路径是否在路由表中
    let currentPath = pathSegments;
    const isPathNotInMap = Object.values(routesMap).includes(pathSegments);
    if (!isPathNotInMap) {
      currentPath = "";
    }
    
    // 更新 URL,保持当前路径
    navigate(
      `/${currentlang}${currentPath && `/${currentPath}`}${
        window.location.search && window.location.search
      }`,
      { replace: true }
    );
    
    // 切换 i18n 语言
    handleLangChange(currentlang);
  };

  const handleLangChange = (newLang: string) => {
    i18n.changeLanguage(newLang, () => {
      document?.querySelector("html")?.setAttribute("lang", newLang);
    });
    setLang(newLang); // 保存到本地存储
  };

  // 初始化时根据 URL 参数设置语言
  useEffect(() => {
    urlLangChange(lang);
  }, []);

  return (
    <div className="lang-icon">
      {langList.map((item) => (
        <p
          className={item?.key === getLang() ? "active" : ""}
          onClick={() => urlLangChange(item?.key)}
        >
          {item?.name}
        </p>
      ))}
    </div>
  );
}

要点:

  • URL 同步:切换语言时更新 URL,保持当前路径
  • 路径验证:确保路径在路由表中
  • 本地存储:保存用户语言偏好
  • HTML lang 属性:同步更新 <html lang>

5. 本地存储管理

typescript 复制代码
// src/utils/storageData.ts
import { storage } from "@storage";//获取本地存储的包,需在npm上找替代包

const LANG_KEY = "LANG_KEY";

export function getLang() {
  const lang = storage.get(LANG_KEY) || "en";
  return lang;
}

export function setLang(lang: string) {
  storage.set(LANG_KEY, lang);
}

6. 在组件中使用翻译

typescript 复制代码
// 示例:在组件中使用
import { useTranslation } from "react-i18next";

export default function Banner() {
  const [t] = useTranslation();
  
  const list = [
    {
      id: "one",
      hint: t("exclusiveEventDesc1"),  // 使用翻译
      // ...
    },
  ];
  
  return (
    <div>
      <h1>{t("pageTitle")}</h1>
      {/* ... */}
    </div>
  );
}

技术优势

不仅实现了3种环境的隔离,在性能上,i18n的翻译是按需加载的,减少初始包体积,支持运行时更新翻译资源,而且用户切换语言时保持当前页面

最佳实践

1. 资源文件组织

bash 复制代码
  └── example/
      ├── en.json          # 生产环境英文
      ├── zh-MY.json        # 生产环境中文
      ├── testnet-en.json   # 测试环境英文
      └── testnet-zh-MY.json # 测试环境中文

2. 翻译 Key 命名规范

json 复制代码
{
  "metaTitle": "example",
  "pageTitle": "Welcome to example",
  "chooseLanguage": "Choose Language"
}

完整流程图

markdown 复制代码
用户访问页面
    ↓
语言检测器检测语言(浏览器/URL/本地存储)
    ↓
根据环境加载对应资源路径
    ↓
HTTP Backend 请求翻译资源
    ↓
加载成功 → 渲染页面
    ↓
用户切换语言
    ↓
更新 i18n 语言 + 更新 URL + 保存到本地存储
    ↓
重新加载对应语言资源
    ↓
页面更新
相关推荐
狗哥哥1 小时前
我是如何治理一个混乱的 Pinia 状态管理系统的
前端·vue.js·架构
一 乐1 小时前
物业管理|基于SprinBoot+vue的智慧物业管理系统(源码+数据库+文档)
前端·javascript·数据库·vue.js·spring boot
测试人社区—52722 小时前
你的单元测试真的“单元”吗?
前端·人工智能·git·测试工具·单元测试·自动化·log4j
c骑着乌龟追兔子2 小时前
Day 32 函数专题1:函数定义与参数
开发语言·前端·javascript
fruge2 小时前
前端性能优化实战:首屏加载从 3s 优化到 800ms
前端·性能优化
zlpzlpzyd2 小时前
vue.js 2和vue.js 3的生命周期与对应的钩子函数区别
前端·javascript·vue.js
鸡吃丸子2 小时前
前端需要掌握的关于代理的相关知识
前端
爱敲代码的小冰2 小时前
js 时间的转换
开发语言·前端·javascript
汝生淮南吾在北2 小时前
SpringBoot+Vue游戏攻略网站
前端·vue.js·spring boot·后端·游戏·毕业设计·毕设