博客
React多语言接入方案

React多语言接入方案

多语言说明

在项目中实现多语言功能,其实挺简单的,但是一般需要多语言功能的时候,常常涉及到整个前端工程,甚至公司所有的工程都需要支持多语言功能。

而某一个功能随着需要的地方逐渐变多,往往就需要一种规范,来让多语言规范化,这也是 i18n 国际化概念 (opens in a new tab) 出现的原因。甚至由此多出来了一个 i18next (opens in a new tab) 组织。

i18next (opens in a new tab) 组织为前端的多语言方案提供了一个很受欢迎的库:i18next (opens in a new tab)

i18next (opens in a new tab) 内部实现了一整套规范,文档也非常完善,但如果要在 react 项目中更好的使用,还要做一些处理才行,于是出现了 react-i18next (opens in a new tab)

本篇文章的主要目的并不是介绍如何使用 i18nextreact-i18next ,而是分享一些让开发者使用起来更方便的方法。

正常接入使用

安装:

pnpm add i18next react-i18next

初始化:

// src/i18n/index.ts
import i18next, { Resource, KeyPrefix, ResourceLanguage, Namespace } from "i18next";
import { initReactI18next } from "react-i18next";
 
// 创建 i18n 实例
const i18nextInstance = i18next.createInstance();
// 注入插件,initReactI18next 是一个 3rdParty 类型的插件,也就是把实例给第三方插件,让其完全可以获取 i18n 实例所有信息
i18nextInstance.use(initReactI18next);
 
// 所有翻译资源
const resources = {
    zh: {
        translation: {
            "test": "测试"
        }
    },
    en: {
        translation: {
            "test": "test"
        }
    }
}
 
// i18n 实例初始化内容
i18nextInstance.init(
    {
      lng: "en",
      // 后备语言,如果用户语言的翻译不可用时使用此顺序
      fallbackLng: ["zh", "en"],
      // 如果需要调试可以打开
      // debug: process.env.NODE_ENV === "development",
      ns: ["translation"],
      // 默认值就是 translation,可以进行设置
      defaultNS: "translation",
      resources: resources,
    },
    (err) => {
      if (err) return console.log("something went wrong loading", err);
    }
  );

初始化完成后,就可以到组件中去正常使用翻译功能了,使用 withTranslation 高阶函数 (opens in a new tab)useTranslation hook (opens in a new tab) 都可以获取翻译的 t 方法和 i18n 实例,具体使用方法可点击去官网查看。

扩展使用方案

withTranslation 扩展

正常来说,开发者并不希望每次新增页面或者组件,需要新增翻译都去改动初始化的文件,更希望能够在当前组件在使用的时候自动去注入翻译。

我们可以对 withTranslation 进行重写,其中 ns 代表 namespace。

// src/i18n/index.ts

import i18next, { Resource, KeyPrefix, ResourceLanguage, Namespace } from "i18next";
import {
  initReactI18next,
  WithTranslation,
  withTranslation,
} from "react-i18next";

// 根据 namespace 缓存,避免
const resourcesCache: Record<string, Resource> = {
  translation: {},
};

// 翻译资源集合,有可能初始化的时候在其他组件加载之后注入
const resources: Record<string, Resource> = {};

/**
 * 用于加载namespace资源
 */
export function addI18nResources(ns: string, resourcesObj: Resource) {
  for (const key in resourcesObj) {
    if (Object.prototype.hasOwnProperty.call(resourcesObj, key)) {
      addNSResource(key, ns, resourcesObj[key]);
    }
  }
}

/**
 * 添加资源
 * @param lng 语言
 * @param ns namespace
 * @param resource 语言包
 */
const addNSResource = (lng: string, ns: string, resource: ResourceLanguage) => {
  if (!resources[lng]) {
    resources[lng] = {};
  }
  if (!resources[lng][ns]) {
    resources[lng][ns] = {};
  }
  // 合并之前的数据
  resources[lng][ns] = {
    ...resources[lng][ns],
    ...resource,
  };
  // 如果已经初始化,则直接添加
  if (i18nextInstance.isInitialized) {
    i18nextInstance.addResourceBundle(lng, ns, resources[lng][ns]);
  }
};

// withTranslation 扩展方法
export function withTranslationExtend<N extends string, TKPrefix extends KeyPrefix<N> = undefined>(
    ns: N,
    resourcesObj?: Record<string, Resource>,
    options?: {
        withRef?: boolean;
        keyPrefix?: TKPrefix;
    }
) {
    // 注入新的 namespace 翻译内容
    if (resourcesObj) {
        addI18nResources(ns, resourcesObj);
    }
    return withTranslation(ns, options);
}

扩展完成后,withTranslationExtend 就可以动态加载翻译资源:

import { withTranslationExtend, WithTranslation } from "../i18n";

type Props = WithTranslation;

function I18nTest(props: Props) {
  const t = props.t;
  return (
    <>
      <div>
        <h1>i18n 测试</h1>
        <p>{t("你好")}</p>
      </div>
    </>
  );
}

export default withTranslationExtend("test", {
    en: {
        "你好": "hi!"
    },
    zh: {
        "你好": "你好!"
    },
})(I18nTest);

现在这样是不是方便了一些?特别是对按需加载的组件,可以让翻译资源也按需加载。

改善 useTranslation 使用体验

待完善,

withTranslationExtend 给被包裹的组件传入的 tFunction 默认作用 ns 就是传入的 ns,但 useTranslation 并不是,还是需要传入需要使用的 ns,或者使用 t 函数的时候添加前缀,比如:

const { t } = useTranslation("test");
t("你好")
// 或者
const { t } = useTranslation();
t("test:你好")

还有些其他方式,但是并不觉得好用,有时候在深层次的组件中,我只想使用当前代码所在上下文最近的上文 ns ,这样可以方便一个组件被不同的组件使用,上层的翻译内容是啥,就使用啥,因为可能不同场景翻译也可能不一样,但key可以保存一致。

下面我们就来使用 React.createContext 来实现这个能力:

const I18nCustomContext = React.createContext({ namespace: "translation" });
 
/**
 * 只用于加载资源时使用
 */
export function withTranslationExtend<N extends string, TKPrefix extends KeyPrefix<N> = undefined>(
  ns: N,
  resourcesObj?: Record<string, Resource>,
  options?: {
    withRef?: boolean;
    keyPrefix?: TKPrefix;
  }
) {
  if (ns && resourcesObj) {
    addI18nResources(ns, resourcesObj);
  }
  const withFunc = withTranslation(ns, options);
  return function wrap<
    C extends React.ComponentType<React.ComponentProps<any>>,
    Props extends React.ComponentProps<any>
  >(Component: C) {
    // 只是为了注入 I18nCustomContext
    function WithCustomI18nContext(props: Props & WithTranslation, ref: React.ForwardedRef<unknown>) {
      return (
        <I18nCustomContext.Provider
          value={useMemo(
            () => ({
              namespace: ns,
            }),
            []
          )}
        >
          <Component {...(props as any)} ref={ref} />
        </I18nCustomContext.Provider>
      );
    }
    
    // 定义 displayName,便于更好的定位错误
    WithCustomI18nContext.displayName = `WithCustomI18nContext(${
      Component.displayName || Component.name || "Component"
    })`;
 
    // 转移 Component 函数中的静态方法
    hoistNonReactStatics(WithCustomI18nContext, Component);
    // 处理 ref 问题
    return withFunc(React.forwardRef(WithCustomI18nContext));
  };
}
 
// useTranslation 扩展
export function useTranslationExtend<N extends Namespace = "translation", TKPrefix extends KeyPrefix<N> = undefined>(
  namespace?: N,
  options?: UseTranslationOptions<TKPrefix>
) {
  const i18nCtx = React.useContext(I18nCustomContext);
  // 未提供 namespace 时,使用最近的一个上下文中指定的 namespace
  return useTranslation(namespace || i18nCtx.namespace, options);
}

现在 使用 useTranslationExtend 替代 useTranslation 后,就可以读取最近上文的翻译,都没有则读取默认的namespace,使用方法跟 useTranslation 一致。

结语

总结一下,文本主要介绍了 React 工程常用的多语言方案,并扩展了 withTranslationExtenduseTranslationExtend 函数来增强开发体验。

上面有部分 ts类型 可能并不完善,因为时间原因,也没有考虑 Next.js 等服务端渲染框架使用的情况,大家如果采用此方案的话,需要注意下一下。