场景

吾辈有一些项目需要使用 i18next 来处理国际化,但是使用 typescript 需要有类型定义,所以之前在 joplin-utils 项目中维护和使用。昨天做了很多重构,现在已经分离出来并作为公共 npm 包发布。如果有人感兴趣,可以尝试一下。

English, 简体中文

简介

i18next 的 typescript 类型定义生成器,可以从多个语言翻译 json 文件中生成类型定义,支持嵌套对象与参数。

使用

这个 cli 本身国际化配置的类型定义生成也是由 cli 完成的(自举)
i18next-dts-gen --dirs src/i18n # 扫描这个目录下的 json 文件并生成 index.d.ts 类型定义

详情

$ i18next-dts-gen -h
Usage: bin [options]

根据 json 生成 .d.ts 类型定义

Options:
  -i, --dirs <dirs...>  包含一或多个翻译文件的目录
  -w, --watch             是否使用监视模式
  -h, --help              display help for command

代码

下面是在 nodejs 中使用

// src/util/I18nextUtil.ts
import i18next from 'i18next';

export enum LanguageEnum {
  En = 'en',
  ZhCN = 'zhCN',
}

export class I18nextUtil<
  T extends Record<
    string,
    {
      params: [key: string] | [key: string, params: object];
      value: string;
    }
  >
> {
  constructor() {}

  async changeLang(lang: LanguageEnum) {
    await i18next.changeLanguage(lang);
  }

  /**
   * 加载国际化
   */
  async init(resources: Record<LanguageEnum, object>, language: LanguageEnum) {
    await i18next.init({
      lng: language,
      resources: Object.entries(resources).reduce((res, [k, v]) => {
        Reflect.set(res, k, {
          translation: v,
        });
        return res;
      }, {}),
      keySeparator: false,
    });
  }

  /**
   * 根据 key 获取翻译的文本
   * @param args
   */
  t<K extends keyof T>(...args: T[K]['params']): T[K]['value'] {
    // @ts-ignore
    return i18next.t(args[0], args[1]);
  }
}
// src/constants/i18n.ts
import { TranslateType } from '../i18n';
import osLocale from 'os-locale';
import { I18nextUtil, LanguageEnum } from '../util/I18nextUtil';

export async function getLanguage(): Promise<LanguageEnum> {
  const language = await osLocale();
  /**
   * os-locale => i18next 的语言类型字符串映射
   */
  const map: Record<string, LanguageEnum> = {
    'zh-CN': LanguageEnum.ZhCN,
    'en-US': LanguageEnum.En,
  };
  return map[language] || LanguageEnum.En;
}

export const i18n = new I18nextUtil<TranslateType>();
// src/bin.ts
async function main() {
  await i18n.init({ en, zhCN }, await getLanguage());
  console.log(i18n.t('hello', { name: 'liuli' }));
}

或者在浏览器中

// App.tsx
function getLanguage() {
  return navigator.language.startsWith('zh') ? LanguageEnum.ZhCN : LanguageEnum.En;
}

export const App: React.FC<AppProps> = () => {
  useMount(async () => {
    await i18n.init({ en, zhCN }, getLanguage());
    // 然后再做其它的事情,例如加载路由
  });

  return <div />;
};

当然,如果你需要在其他环境中使用,应该仅需实现对应的 getLanguage 函数即可。

技巧

提示

prompt.gif

导航

navigation.gif

搜索和替换
searchAndReplace.gif

动机

为什么已经有了很多第三方的类型定义生成器,甚至最新版 i18next 官方已经推出了 typescript 解决方案,吾辈还要写这个呢?

简而言之,都不完善。

先从 i18next 官方的解决方案说起,它是将 json 文件替换为 ts 文件,但不能支持参数和嵌套对象。

注:最新版似乎利用了 typescript 4.2 的递归类型和模板字符串类型来保证类型安全,但这实际上是不怎么好用的。另外只有 react-i18next 是可用的。

再来说 i18next-typescript 这个第三方库,几乎能满足吾辈的需求了,除了一点:支持对象参数。还有像是 Jack 菊苣的 i18n-codegen,代码设计上非常优雅,但同样的,不支持 react 之外的生态。

另外,就吾辈而言,认为使用生成器生成简单的类型要比从类型系统上支持这种功能更加容易,也更加合理。

设计

架构图
流程图

FAQ

是否支持 i18next 的全部特性?

否,这里支持的仅为 i18next 的一个子集。

  • [x] 为多个本地化 json 配置文件生成类型定义
  • [x] 支持包含参数

    • [ ] 不支持嵌套参数
  • [ ] 不支持嵌套的 key -- 我们认为使用 . 分割就足够了,而且这样也更容易全局查找和替换
  • [ ] 不支持配置命名空间、嵌套的分割字符串,我们认为约定大于配置
  • [ ] 不支持 json 之外的配置文件,我们认为 json 文件对于非开发者都更友好,而且在需要时开发者更容易处理。它还是跨语言的,包括 golang/rust 的 i18n 框架均支持
  • [ ] 不支持 i18next 命名空间,即将翻译文件分割 -- 大部分时候翻译的内容并没有那么多,不超过 1000 个我都不太愿意分割

    可以通过不同的 i18n 对象

rxliuli
709 声望26 粉丝

算了,还是不在这儿玩了,国内什么论坛大了都会变成泥潭。。。