目录

41.组件实战国际化语言开发实现

什么是国际化语言开发?

很多业务在全球范围内使用的过程中,不仅仅是针对中国用户,往往会面临着需要进行多语种的方案切换,在这个过程中对,需要考虑到不同地区、语言和文化背景的用户,以确保他们可以无障碍地使用和理解软件。

同理,在低代码平台中,如果有国际化的业务诉求,那么集成国际化方案是非常有必要的。本章节的内容就是来配置在低代码平台当中,如何结合现有方案实现从零到一的语种配置、使用、切换的过程。

国际化方案

目前来说,主流的国际化社区实现的方案主要有以下几个三方包能够满足功能的相关诉求:

  • react-i18next(i18next): react-i18next是一个流行的 React 国际化库,它基于 i18next。主要为React提供一系列的Hook、Hoc和组件的使用方式,能够在项目中更加便捷的使用。
  • react-intl(formatjs): FormatJS 是一个用于国际化的 JavaScript 库的模块化集合,在这个基础上,借用相关的能力实现了提供给React使用的国际化语言切换工具,也就是react-intl。
  • next-i18next(Next.js): nextjs基于i18next提供的国际化解决方案,主要搭配Next.js使用,能够最大程度的获得两者结合的相关开发体验和优化性能。

除此之外也有其他大大小小的社区方案提供选择,在这里就不过多赘述。在小册项目中,会主要使用react-i18next来进行相关的实践,总体的流程大差不差。如果你对其他的工具库有更加深的理解,那么可以根据自身的经验进行选择。

实现原理

在开始之前,先来看看整体的实现脉络。整体实现流程可以分为以下两个阶段:

配置阶段:使用配置组件在工作台和编辑器中进行语种文案的编辑保存。 编辑器阶段:异步加载多语种文案并注入i18n后,将其提供给jsRuntime使用,最终完成页面的渲染。

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/28ae5169f06346ba815bb8053bd25d32~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1684&h=716&s=48844&e=png&b=fffcfc

国际化平台的集成实现的首要的工作就是将其集成在低代码编辑器当中实现。我将其分为两个阶段:

  • 基础实现:实现基本的国际化能力集成,能够进行基本的语言切换和显示。
  • 低代码平台:在基础实现之上,结合低代码平台做数据源管理,版本切换等等不同的功能,最终实现可在线配置的多语言国际化方案。
  • 稳定迭代:随着应用的发展和迭代,可能会有新的文本需要翻译,或者现有的翻译需要更新。确保国际化资源的持续维护,并及时更新和添加新的翻译内容。

基础实现

首先先来实现react-i18next在React工程中的基本场景使用。整体过程如下:

具体也可以参照相关文档链接:https://www.i18next.com/overview/getting-started

安装

在apps/editor文件目录下安装 react-i18nexti18next相关的多语言库,在上面已经提到了目前市面上主流的一些实现方案,

如下代码脚本所示

# 进入编辑器目录下
cd apps/editor

# 安装相关依赖
pnpm add react-i18next i18next

安装成功后就可以在编辑器中开始使用了。

初始化配置

依赖安装完成后,使用i18next进行基础配置的初始化,在utils目录下添加i18next.ts文件,用于实现相关初始化的代码。

如下代码所示:

首先引入i18next依赖,并且执行init方法注册React相关的依赖并加载资源的配置。在这里先将resources的配置相关信息写死,后续这里会换成配置的数据和对应的远程资源数据。

import i18n from "i18next";
import { initReactI18next } from "react-i18next";

i18n
.use(initReactI18next)
.init({
  resources: {
    en: {
      translation: {
        "hello": "Hello",
        "welcome": "Welcome to my app!"
      }
    },
    zh: {
      translation: {
        "hello": "你好",
        "welcome": "欢迎来到我的应用!"
      }
    }
  },
  lng: 'zh',
  fallbackLng: 'zh',
  interpolation: {
    escapeValue: false,
  }
});

![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0485674479094f5bba7f1ab0edb3efbe~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=2690&h=2104&s=321972&e=png&b=fefefe)
export default i18n

Provider

I18nextProvider是react-i18next提供的Provider组件包裹层,将其包裹编辑器组件并通过i18n属性将i18n实例传递给I18nextProvider,这样,整个应用的组件树都可以通过useTranslation钩子函数或withTranslation高阶组件来访问i18n实例的国际化功能,并且在语言切换时可以自动更新翻译内容。

如下代码所示:

import {I18nextProvider } from 'react-i18next'
import i18n from './utils/i18n'

<I18nextProvider i18n={i18n} >
    {props.children}
</I18nextProvider>

接下来就来试试效果,我给按钮组件的文本内容输入了一个表达式,使用i18n的方式绑定之前注入过的 welcome词条,界面中按钮的显示文案就会变成【欢迎来到我的应用】的显示。

$t(["welcome", "我是默认文案"])

具体效果如下图所示:

可以看到文本内容已经绑定了表达式,利用其特性来渲染多语言。到此,基础实现就完成了,后面就是在低代码平台中的一些结合应用,做一体化的产品功能实现。

https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b8bd52c773b04591866c266cfa891eb5~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=2690&h=2104&s=321972&e=png&b=fefefe

低代码结合

在创建应用时,可以在对应的面板中添加多语种板块来支持导入和导出相关数据的操作,如下图选择新建应用,会自动跳转到应用新增界面。

https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/20124f4da09c4c7097851d3b048f468d~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=2768&h=2112&s=554944&e=png&b=fbfbfb

如下图所示:

在应用新增界面中多语言板块基本的数据编辑器,能够提供自定义语种文案的相关配置方式。将之前写死的resources字段改成从状态更新或者从远程加载,通熟易懂点就是不在init中实现,而是通过异步的形式更新并重新渲染。

[
    {
        "key": "hello",
        "cn": "hello",
        "en": "你好"
    },
    {
        "key": "welcome",
        "cn": "Welcome to my app!",
        "en": "欢迎来到我的应用!"
    }
]

https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b9b0e79221c84f1abbfc267c2b31201c~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=2804&h=2580&s=319938&e=png&b=fafafa

整体编辑界面基本布局如下代码所示:

通过Form新增编辑界面的基本布局,这里不过多赘述了,后面在平台篇会主要的讲述着一块的具体功能点。

export default () => {
  return (
    <div
      className={css({
        maxWidth: 1400,
        width: "100%",
        margin: "0 auto",
      })}
    >
      <ProForm
        submitter={false}
        initialValues={{
          dependencies: {
            js: {
              tpl: '<script defer="defer" src="${link}"></script>',
            },
          },
        }}
      >
        <Flex vertical gap={12}>
          <ProCard bordered title="应用信息" tooltip="当前应用的基本信息配置">
            <ProFormGroup direction="vertical">
              <ProFormUploadButton
                width="xs"
                title="应用图标"
                icon={<FormOutlined />}
                label="应用图标"
                name="icon"
                params={{
                  resolveType: 1
                }}
                action="/gateway/common/rpc/upload"
              />
              <ProFormText
                name="name"
                label="名称"
                width="lg"
                tooltip="最长为 24 位"
                placeholder="请输入名称"
                rules={[{ required: true }]}
              />
              <ProFormTextArea
                fieldProps={{
                  maxLength: 100,
                  showCount: true,
                  autoSize: {
                    minRows: 5,
                    maxRows: 5,
                  },
                }}
                width="lg"
                label="应用描述"
                name="description"
                placeholder="请输入应用描述"
                tooltip="页面的描述,在这里可以将你的应用信息进行详细的描述"
              />
            </ProFormGroup>
          </ProCard>

          {/* 依赖管理 */}
          <ProForm.Item name="depends" >
              <DepensManageCard />
          </ProForm.Iteom>
          

          {/* 多语言管理 */}
          <ProForm.Item name="locale" >
              <LocaleEditTable />
          </ProForm.Iteom>

          {/* 自动化任务 */}
          <ProForm.Item name="automation" >
              <AutomationTaskCard/>
          </ProForm.Iteom>
          
            
          {/* 部署运维 */}
          <ProForm.Item name="deploy" >
              <DeploymentOperationCard/>
          </ProForm.Iteom>
          
        </Flex>
      </ProForm>
    </div>
  );
};

LocaleEditTable 组件就是多语言编辑表格的实现,如下代码所示:

LocaleDataRecordType中定义了各种语类的资源声明,通过 EditableProTable 组件将其进行渲染,在LocaleEditTableProps声明value和onChange方法提供给Antd的组件使用,以此来实现一个自定义的Form组件。

export type LocaleDataRecordType = {
  id: React.Key;
  key?: string;
  cn?: string;
  eu?: string;
  jp?: string;
  kr?: string;
  fe?: string;
};

export interface LocaleEditTableProps {
  value?: LocaleDataRecordType[];
  onCahnge?: (newData: readonly LocaleDataRecordType[]) => void;
}

export const LocaleEditTable: React.FC<LocaleEditTableProps> = (props) => {
  const [editableKeys, setEditableRowKeys] = useState<React.Key[]>(() => []);

  const columns: ProColumns<LocaleDataRecordType>[] = [
    {
      title: "键值",
      dataIndex: "key",
      tooltip: "必须填写,否则使用时无法命中",

      formItemProps: {
        rules: [
          {
            required: true,
            message: "请输入语种键值",
          },
        ],
      },
    },
    {
      title: "简体中文",
      dataIndex: "cn",
      formItemProps: {
        rules: [
          {
            required: true,
            message: "请输入默认中文名称",
          },
        ],
      },
    },
    {
      title: "英文",
      dataIndex: "e",
    },
    {
      title: "日文",
      dataIndex: "jp",
    },
    {
      title: "韩文",
      dataIndex: "kr",
    },
    {
      title: "法语",
      dataIndex: "fe",
    },
    {
      title: "操作",
      valueType: "option",
      fixed: "right",
      align: "left",
      width: 50,
      render: () => {
        return null;
      },
    },
  ];

  const setDataSource = props.onCahnge!

  return (
    <EditableProTable<LocaleDataRecordType>
      bordered
      rowKey="id"
      headerTitle="多语言"
      tooltip="国际化语种配置"
      columns={columns}
      scroll={{
        x: "100%",
        y: 400,
      }}
      value={props.value}
      onChange={setDataSource}
      recordCreatorProps={{
        newRecordType: "dataSource",
        record: () => ({
          id: Date.now(),
        }),
      }}
      toolBarRender={() => {
        return [
          // todo
          <Typography.Link key="download">下载语言模版</Typography.Link>,

          // todo
          <Button key="import" type="primary" ghost>
            导入元数据
          </Button>,
        ];
      }}
      editable={{
        type: "multiple",
        editableKeys,
        actionRender: (_, __, defaultDoms) => {
          return [defaultDoms.delete];
        },
        onValuesChange: (_, recordList) => {
          setDataSource(recordList);
        },
        onChange: setEditableRowKeys,
      }}
    />
  );
};

以上就是配置界面的作用和布局的实现,具体细节可以根据自身的想法和需求进行搭配,最终本质是导出一份多语种的文案列表,然后提供给编辑器使用。

异步更新语言资源

在工作台应用创建和编辑器编辑的时候可以动态的配置语种文案数据源,那么如何异步更新语言资源并且实现页面文案切换呢?

这个时候就需要用到i18n的相关API了,具体可以浏览:https://www.i18next.com/overview/api

打开文档后,使用搜索功能搜索 addResource 方法,会发现其提供了相关资源添加的钩子,基于此就可以来实现文案的添加。

https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4a57684fff1b4056903112b1bd637011~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=2612&h=1290&s=382331&e=png&b=ffffff

首先,先来实现 convertLocaleData方法,用于将列表的数据转换为当前i18n的资源。

显示效果如下所示:

转换前:

[
    {
        "key": "hello",
        "cn": "hello",
        "en": "你好"
    },
    {
        "key": "welcome",
        "cn": "Welcome to my app!",
        "en": "欢迎来到我的应用!"
    }
]

转换后:

{
    en: {
      translation: {
        "hello": "Hello",
        "welcome": "Welcome to my app!"
      }
    },
    cn: {
      translation: {
        "hello": "你好",
        "welcome": "欢迎来到我的应用!"
      }
    }
  }

实现方法如下代码所示:

locales 遍历循环,根据 languages 将需要的语言包进行转换。

 // 这一步其实是在服务端去预处理的
    const convertLocaleData = (
      locales: LocaleDataRecordType[],
      languages: string[]
    ) => {
      const outputData: {
        [language: string]: {
          translation: {
            [key: string]: string;
          };
        };
      } = {};

      languages.forEach((language) => {
        outputData[language] = {
          translation: {},
        };
      });

      locales.forEach((item: Record<string, any>) => {
        const key = item.key;

        languages.forEach((language) => {
          if (language && item && key) {
            outputData[language].translation[key] = item[language];
          }
        });
      });

      return outputData;
    };

完整的代码实现如下:

i18nlocales发生改变的时候,useEffect会重新执行i18n.addResourceBundle注册相关的语言包资源,从而通过I18nextProvider触发整个页面视图状态的改变。

 React.useEffect(() => {
    // 这一步其实是在服务端去预处理的
    const convertLocaleData = (
      locales: LocaleDataRecordType[],
      languages: string[]
    ) => {
      const outputData: {
        [language: string]: {
          translation: {
            [key: string]: string;
          };
        };
      } = {};

      languages.forEach((language) => {
        outputData[language] = {
          translation: {},
        };
      });

      locales.forEach((item: Record<string, any>) => {
        const key = item.key;

        languages.forEach((language) => {
          if (language && item && key) {
            outputData[language].translation[key] = item[language];
          }
        });
      });

      return outputData;
    };

    const resources = convertLocaleData(locales, ["cn", "en"])

    // 监听资源更新并将其设置到i18n实例
    i18n.addResourceBundle('en', 'translation', resources.en.translation, true, true);
    i18n.addResourceBundle('cn', 'translation', resources.cn.translation, true, true);

    // 存在语言文案数据的时候
    if (locales.length > 0) {
    }
  }, [i18n, locales]);

基于此套流程,后续只需要更新locales字段即可重新加载相关的国际化资源。

需要注意的是,往往国际化资源会非常大,能预处理和预加载的都提前处理掉, 避免额外的运行时计算开销带来的加载性能问题。

表达式配置

上述流程当中已经完成了国际化资源的配置、更新、加载。本节主要是将其与表达式结合,通过表达式代码的方式进行配置使用。

由于在此之前已经完成了表达式的属性绑定,编辑器也已经加载了I18nProvider, 因此我们只需要在对useParseBinding进行简单的改造即可让表达式支持 $t 相关的使用。

react-i18next中,提供了给用户使用多语言文案的hook,也就是useTranslation,通过useTranslation暴露出的t方法可以根据传递key的方式将对应语种的文案进行返回。

如下代码所示:

通过useTranslation获取方法,将其重命名为 $t 传递到 jsRuntime.execute 中,以此表达式的上下文就能够通过 $t 的方式将页面显示的文案返回到页面当中。

import React from 'react'
import _ from 'lodash'
import { jsRuntime } from '../runtime'
+ import { useTranslation } from 'react-i18next'

export const useParseBinding = (props: Record<string, any>, id?: string) => {

+  const { t } = useTranslation()

  const customizer = (value: any) => {
    if (_.isPlainObject(value) && _.has(value, '$$jsx')) {
      return jsRuntime.execute(value.$$jsx, {
        ...props,
+        $t: t
      })?.value
    }
  }

  const memoizedProps = React.useMemo(() => {
    const data = _.cloneDeepWith(props, customizer)
    return data
  }, [props])

  return memoizedProps
}

通过以上的改造,useParseBinding就已经可以支持多语种相关的文案渲染了。

总结

以上就是基于react-i18next如何实现多语种方案的基础内容,后续就是将相关的配置平台和编辑器进行完善。在后续工作台内容相关的章节实现完成后,会将其整个模块进行串联,实现一站式的多语种文案管理。

目前是通过jsRuntime来实现相关多语种文案的绑定使用,虽然这种方式能够解决我们的问题,但是随着业务复杂,在这基础之上理应针对多语种开辟一个新的空间和设置器来完成绑定的工作,不仅仅可以在语种提示上,还是在显示交互上都能有很大的优化空间。

资源