通用的Web国际化方案

88 阅读9分钟

前端国际化(Web Internationalization)是指在进行Web开发时,使网站或Web应用程序能够支持多种语言和地区文化,以满足全球用户的需求

概述

由于目前前端形态和框架技术各不相同,如果国际化方案需要根据不同框架进行切换,开发成本可能大大增加。有没有一种可以无缝适配多种主流框架的国际化库呢?

i18-fe,通用react、vue、原生HTML等多种Web环境,写法统一且简单,无需额外工作量,便于修改、调试、拓展

特性

⛰️ 适配 ReactVueAngularvanilla js等多种 Web 框架

🛶 多框架写法统一,无切换负担

🏗️ 支持 ts 提示,填词效率飞快

🚝 API 简洁,无学习成本

适用端

web(HTML),PC端、移动端(浏览器环境)、webview。暂不包括原生安卓、iOS和小程序

安装

npm install i18-fe or yarn add i18-fe

API

初始化

使用前需初始化&配置I18,后续都通过i18实例来操作

// src/i18/index.ts

// 引入i18-fe
import I18 from 'i18-fe';  
// 初始化I18
const i18 = new I18();  

// 导出i18实例
export default i18;

配置语言包

需要翻译的内容需要在初始化时配置,传入语言包pack。翻译内容是国际化的重点,也是工作量较大的部分

const pack = {
  // 翻译标识(key)
  你好: {
    // 语言标识(locale)
    zh: '你好',
    en: 'hello',
  },
};

new I18({ pack });

上述代码中,你好翻译标识(key)zh(中文)和en(英文) 是 语言标识(locale)

key能不能用中文?

作为中文开发者,中文更方便开发维护,可以直观的明白页面内文本含义,当然,你也可以不用

语言标识是作为一级key还是二级key好?

  • 翻译标识作为一级key,1是代码量通常会更少,2是突出了翻译重点,整体集中在一起,便于对照和修改

  • 语言标识作为一级key,可以将不同的语言类型拆分开来

默认是第一种格式,如果需要使用第二种格式,可以使用I18.packFmt(pack)进行格式化后再传入

const pack = {
  zh: {
    hello: '你好',
  },
  en: {
    hello: 'hello',
  },
};

new I18({ pack: I18.packFmt(pack) });

pack是不是必填项?

不是。如果不需要使用翻译函数(t)可以不配置pack

pack模块化

语言包配置拆分成各个模块,最后统一导入I18

默认语言

默认:zh

new I18({ defaultLocale: 'en' });

如果翻译时,未从语言包找到当前语言的翻译文本,则会返回默认语言的翻译文本

替换模式

默认:false

用于原生JS DOM替换,用法见下文最佳实践-原生JS

new I18({ replace: true });

翻译(t)

使用i18.t(key)进行翻译

import i18 from '@/i18'

// 会根据语言标识返回 zh: 你好 en: hello
i18.t('你好')

拼接文本翻译(插值)

有时候文本中夹杂需要动态填入的变量,例如:本页于3分钟前更新,其中3分钟需要动态传入

const pack = {
  本页于N时间前更新: {
    zh: '本页于{time}{unit}前更新',
    en: 'This page was updated {time} {unit} ago',
  },
  分钟: {
    zh: '分钟',
    en: 'minutes',
  }
};

{time}{unit}是占位符,翻译时通过t(key, params)第二个参数以对象形式传入

import i18 from '@/i18'
const { t } = i18;

// zh: 本页于3分钟前更新
// en: This page was updated 3 minutes ago
t('本页于N时间前更新', { time: 3, unit: t('分钟') })

英文的单数和复数

t('本页于N时间前更新', { time: 1, unit: t('分钟') })

得到This page was updated 1 minutes agominutes需要修正为单数minute

这里介绍一种方法

const pack = {
  本页于N时间前更新: {
    zh: '本页于{time}{unit}前更新',
    en: 'This page was updated {time} {unit} ago',
  },
  分钟(单): {
    zh: '分钟',
    en: 'minutes',
  },
  分钟(复): {
    zh: '分钟',
    en: 'minute',
  }
};
t('本页于N时间前更新', { time: 1, unit: t(`分钟(单)`) })

t('本页于N时间前更新', { time: 9, unit: t(`分钟(复)`) })
t('本页于N时间前更新', { time: 0, unit: t(`分钟(复)`) })

获取语言(getLocale)

i18.getLocale(): string

可以在页面任何地方获取当前的语言标识(locale),在一些复杂的场景会需要用到它

I18初始化时,会从依次从url querylocalStoragenavigator.language获取locale值,再将其赋值到i18对象上

切换浏览器语言,getLocale()得到的locale也会随之改变

image.png

日期格式

比如中国的日期格式是YYYY-MM-DD,美国的日期格式是MM/DD/YYYY

可以使用i18.getLocale()获取当前语言进行判断

const dateFormat = i18.getLocale() === 'zh' ? 'YYYY-MM-DD': 'MM/DD/YYYY'

moment().format(dateFormat)

图片

很多图片都带有文字,也需要进行国际化处理

<img src={`/static/img/logo-${i18.getLocale()}.png`} />

动态数据请求

很多时候文案是由服务端返回,可以和后端开发者约定,比如在请求头里加上locale参数,由后端进行翻译。前端一般有统一的请求入口,可使用请求拦截器进行统一处理

接口文案的处理工作量也是比较大,很多数据的文字是不好改的

image.png

从华为云管理端可以看到,有些数据也是没办法翻译成英文的

这里我列举几个常见的,被翻译优先级比较高的:

状态码,比较重要且翻译工作量不大,一般会返回statusCodestatusName,前端可以根据statusCode进行匹配翻译

image.png

切换语言(setLocale)

i18.setLocale('en')

一般在页面固定位置可以进行语言切换,具体看设计

image.png

切换语言时会将url querylocalStorage中的locale修改为指定语言,并自动刷新页面

你也可以不调用语言切换,因为i18-fe会自动获取浏览器默认的语言

最佳实践

包含在多种web框架下的使用方法

react(+ts)

首先在src下建一个i18目录

配置语言包

// src/i18/pack.ts
export default {
  线索单据漏斗图: {
    zh: '线索单据漏斗图',
    en: 'Lead document funnel diagram',
  },
  订单金额: {
    zh: '订单金额',
    en: 'Order amount',
  },
  SKU成交转化率: {
    zh: 'SKU成交转化率',
    en: 'SKU conversion rate',
  },
};

初始化

具体初始化和语言包的配置,请查阅API-初始化

// src/i18/index.ts
import I18 from 'i18-fe';  
import pack from './pack'

const i18 = new I18({ pack });  

export default i18;

翻译

import i18 from '@/i18'
const { t } = i18;

//...
t('线索单据漏斗图')
//...
<div>{t('SKU成交转化率')}</div>

支持ts联想,引导填入key值,方便开发

image.png

具体用法见API-翻译

react组件

组件一般与当前开发的工程分离,如何将组件国际化呢?

这里有几个方案:

  1. 从外部传递语言标识,组件内部自行翻译
  2. 组件内部自行获取语言标识,自行翻译
  3. 从外部传递翻译内容,组件接收进行填写

3种方式灵活性 3>1>2,操作难度也是3>1>2

通用组件文案基本固定且较少,如果组件只对内部使用,且都使用的是i18-fe,可以使用方案2,分工明确,便于操作。如果像ant designMaterial-UI那样开放给全球用户使用,因为语言标识获取的不统一, 可以使用方案1

业务组件文案其实也变化不大,可选择方案2或3

翻译内容需不需要传入?

查询组件SearchButton,中文叫查询,那英文可以叫Search,应该没有其他叫法了,所以内部维护比较好

无数据组件Nodata,英文可以叫no data,中文叫暂无数据,还可以叫未查找到数据真的没有数据了,这种情况已经脱离了翻译的范畴,应该把文案当做props传入组件

内部获取语言标识,内部翻译

操作方式和上面react类似,确保组件内和使用组件的工程统一使用i18-fe

import React from 'react';
import I18 from 'i18-fe';

const i18 = new I18({
  pack: {
    查询: {
      zh: '查询',
      en: 'Search',
    },
    重置: {
      zh: '重置',
      en: 'Reset',
    },
  },
});

const SearchButton = () => {
  return (
    <div>
      <div>{i18.t('查询')}</div>
      <div>{i18.t('重置')}</div>
    </div>
  );
};

export default SearchButton;

外部传入语言标识,内部翻译

外部传入最好通过Provider形式注入,避免每次使用组件都传入

// 组件
import React from 'react';

export const Context = React.createContext({} as any);
// 提供全局Provider
export const ConfigProvider = ({
  /** 语言标识 */
  locale,
  children,
}: {
  locale: string;
  children: React.ReactNode;
}) => <Context.Provider value={{ locale }}>{children}</Context.Provider>;

const pack = {
  查询: {
    zh: '查询',
    en: 'Search',
  },
  重置: {
    zh: '重置',
    en: 'Reset',
  },
};

type GetKey<T> = T extends Record<string, infer R>
  ? R extends Record<infer L, string>
    ? L
    : never
  : never;

const TestCom = () => {
  const { locale }: { locale: GetKey<typeof pack> } = React.useContext(Context);

  return (
    <div>
      <div>{pack['查询'][locale]}</div>
      <div>{pack['重置'][locale]}</div>
    </div>
  );
};

export default TestCom;

外部传入翻译内容,组件接收

同样以Provider形式将翻译的内容传入组件,组件获取后负责展示

// 组件
import React from 'react';

export const Context = React.createContext({} as any);
// 提供全局Provider
export const ConfigProvider = ({
  locale,
  children,
}: {
  locale: Record<string, string>;
  children: React.ReactNode;
}) => <Context.Provider value={{ locale }}>{children}</Context.Provider>;


const TestCom = () => {
  const { locale } = React.useContext(Context);

  return (
    <div>
      <div>{pack['查询']}</div>
      <div>{pack['重置']}</div>
    </div>
  );
};

export default TestCom;
// 使用组件

const pack = {
  查询: {
    zh: '查询',
    en: 'Search',
  },
  重置: {
    zh: '重置',
    en: 'Reset',
  },
};

const i18 = new I18({ pack });

<ConfigProvider locale={{
    查询: i18.t('查询'),
    查询: i18.t('查询'),
}}>
    <TestCom />
    <TestCom />
    <TestCom />
</ConfigProvider>

react(iframe/微前端/不同工程)

原生JS (freemarker、jQuery等)

如果未使用React、vue等框架,写的是原生DOM,如何国际化处理呢?

<body>
    <!-- 测试 → test -->
    <div>测试</div>
    
    <!-- 输入框的 请输入 → please input -->
    <input type="text" placeholder="请输入" />

    <div>
      <!-- 中文的百度logo换成英文logo -->
      <img src="./img/zh-baidu.png" alt="" />
    </div>
</body>

我们常规修改DOM元素一般是给元素绑定idclass,再通过document.getElementById等方法获取到DOM元素,再进行修改

设置replace模式

开启replace模式后会对页面DOM进行筛选,并修改匹配i18前缀的DOM

 <script src="/node_modules/i18-fe/dist/index.js"></script>

    <script>
      const i18 = new I18({
        replace: true,
        pack: {
          测试: {
            zh: "测试",
            en: "test",
          },
          请输入: {
            zh: "请输入",
            en: "please input",
          },
        },
      });
</script>

加上i18-属性

i18-children匹配修改元素的html内容(类似于react中的children)

i18-placeholder匹配修改元素的placeholder属性

i18-src匹配修改元素的src属性

上述匹配机制,如果内容中包含{locale}字样,会将{locale}替换为语言标识,如zhen

<div i18-children="测试"></div>
<input type="text" i18-placeholder="请输入" />

<div>
    <img i18-src="./img/{locale}-baidu.png" alt="" />
</div>

完整代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div i18-children="测试"></div>
    <input type="text" i18-placeholder="请输入" />

    <div>
      <img i18-src="./img/{locale}-baidu.png" alt="" />
    </div>

    <div>
      <button onclick="changeLocale('zh')">中文</button>
      <button onclick="changeLocale('en')">English</button>
    </div>
    <script src="/node_modules/i18-fe/dist/index.js"></script>

    <script>
      const i18 = new I18({
        replace: true,
        pack: {
          测试: {
            zh: '测试',
            en: 'test',
          },
          请输入: {
            zh: '请输入',
            en: 'please input',
          },
        },
      });

      function changeLocale(locale) {
        i18.setLocale(locale);
      }
    </script>
  </body>
</html>

低代码

vue