前端国际化多语言探索实践

934 阅读11分钟

本文深入探讨了企业级项目在实现多语言支持过程中的探索与实践。从早期的手动文案管理,逐步演变为一套自动化的多语言管理系统。在这一过程中,通过技术创新和流程优化,不仅提升了多语言项目的开发效率,还解决了代码语义化、文案管理和性能优化等常见问题。这一系列改进使得应用能够为全球用户提供更流畅的使用体验。

背景

由于公司业务发展,需要将产品推广到香港、英美、非洲等地。因为要在不同的国家和地区使用,首先就要让文本能够根据国家地区切换为当地语言,也就是实现多语言。

基本概念与技术选型

  1. 多语言的基本概念

在前端项目中,多语言通常涉及两个核心概念:国际化(i18n)本地化(l10n),它们共同构成了多语言支持的基础。

  • 国际化(i18n)

    国际化是指在产品设计和开发阶段,为适配不同语言、地区和文化差异做出技术准备的过程。它侧重于在代码结构、文案管理、日期格式、货币格式等方面提供灵活性,确保产品能够被轻松地“翻译”成各种语言。

  • 本地化(l10n)

    本地化是指将产品内容翻译成特定地区或语言,使其符合该地区的文化和语言习惯的过程。它包括实际的翻译、文案调整、图标和图形修改、颜色调整等。

  1. 常见的多语言库和框架选型
  • i18nextreact-i18next

    i18next 是一个用于前端国际化的 JavaScript 库。它提供了一个简单易用的 API,可以帮助开发人员将应用程序本地化到多种语言。它提供了一种简洁的方式来加载翻译资源,并且支持多种资源格式(如 JSON、PO 等)。同时,它还支持动态加载和缓存翻译资源,以提高性能和用户体验。

    react-i18next 则是基于 i18next 的一个 React 绑定库,提供了一套用于在 React 应用程序中实现国际化的组件和高阶组件。它能够无缝集成到 React 应用程序中,并且提供了方便的 API 来处理语言切换、翻译文本和处理复数等国际化相关任务。

  • polyglot

    Polyglot.js是一个轻量级的国际化库,适用于小型项目。它功能相对简单,支持基本的多语言功能和插值,但不支持异步加载等高级特性。

  • vue-i18n

    vue-i18n 是一个用于在 Vue.js 应用程序中实现国际化的库。它支持多种语言切换策略,包括 URL 参数、浏览器语言设置和自定义逻辑。同时它还支持动态加载和异步加载翻译资源,以提高性能和用户体验。

这些库的用法实际上有比较多的相似点。大体上都是在代码中内置多语种文案,在业务代码中通过调用 i18n 方法,并传入对应文案的 key。编译的时候,会根据当前语种,读取 key 对应的文案并渲染。

  1. 本项目的技术选型和理由

    为了应用于大型的 React 项目并满足 C 端产品对性能的高要求,我们选择了 react-i18next。它作为专为 React 优化的多语言库可以直接集成到项目中,并且它支持动态加载语言文件,能够有效减少初始加载量并提升性能。

    用法:初始化 i18next,并在入口文件引入

// i18n.js
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";

i18n
  .use(LanguageDetector)
  // 注入 react-i18next 实例
  .use(initReactI18next)
  // 初始化 i18next
  .init({
    debug: true,
    fallbackLng: "en",
    interpolation: {
      escapeValue: false,
    },
    resources: {
      en: {
        translation: {
          // 这里是我们的翻译文本
          welcome: "Welcome to my website",
        },
      },
      zh: {
        translation: {
          // 这里是我们的翻译文本
          welcome: "欢迎来到我的网站",
        },
      },
    },
  });

export default i18n;

// app.js
import { useTranslation, Trans } from "react-i18next";

function App() {
  const { t } = useTranslation();
  return (
    <div>
      <main>
        <p>{t("welcome")}</p>
      </main>
    </div>
  );
}

export default App;

方案的演变

  1. 手动阶段(0~0.9)

    在这个阶段,我们实现了多语言管理平台,并修改了项目结构、前后端交互、业务组件库,也重新规范了开发流程,这一系列措施,让项目初步达到了“国际化”的标准。

    多语言管理平台

    通过平台,我们可以完成最基本的文案管理流程:创建多语言项目 => 新建文案key => 编辑文案 => 发布

    另外,将项目接入平台也很简单,因为多语言项目与前后端项目一一对应,只需要在平台创建一个多语言项目,并在项目中配置projectId即可,对于前端项目,react-i18next 会根据projectId动态拼接语言包的json资源地址,流程大致如下👇

    未命名文件.png

    项目结构

    有些业务流程在各个国家地区间的差异性不大,仅需要在代码里添加一些基于站点的Map或者判断👇

    const TIPS_MAP = {
        'HK': '',
        'US': '',
        'UK': '',
    }
    const { curSite, isHK } = useSite()
    console.log(TIP_MAP[curSite])
    console.log(isHK ? '' : '')
    

    而有些业务模块,在不同的国家地区会有完全不同的业务流程,对于这些模块,要在组织目录结构时区分站点👇

    ├── Refund                      # 退款业务
    │   ├── components              # 公共组件
    │   ├── utils                   # 工具函数
    │   ├── types                   # 类型文件
    │   ├── HK                      # 香港站
    │   ├── US                      # 美国站
    │   └── Mainland                # 大陆站
    └── ...
    

    前后端交互

    为了确保接口返回内容符合用户选择的语言,通常会在请求中传递语言信息。以下是常见的几种方案:

    1. 通过 Accept-Language 或其他自定义请求头传递语言 (已采纳)
    2. 通过查询参数传递语言
    3. 通过 URL 路径传递语言 (阿里云采用的此方案)

    选择 Accept-Language 请求头传递语言,主要是因为它属于HTTP 规范推荐的传递方式,并且可以在请求库(如 Axios)中统一设置,简化代码维护。

    拦截器代码如下:

    import axios from 'axios';
    import i18next from 'i18next';
    
    // 创建 Axios 实例
    const apiClient = axios.create({
      baseURL: 'https://your-api.com',
    });
    
    // 请求拦截器
    apiClient.interceptors.request.use(config => {
      // 从 i18next 中获取当前语言
      const currentLanguage = i18next.language;
    
      // 在 headers 中添加语言信息
      config.headers['Accept-Language'] = currentLanguage;
    
      return config;
    });
    
    export default apiClient;
    
    

    PS:大多数后端框架直接支持对 Accept-Language 的解析,适用性广泛,且在需要的情况下,可以更灵活地与其他国际化方案结合。

    业务组件

    在设计国际化项目的业务组件时,需要额外考虑以下几个方面:

    1. 多语言内容的动态适配
    2. 日期、时间、数字格式
    3. 布局适配(使用css 逻辑属性)

    开发流程 & 测试流程

    开发流程:按设计稿还原UI => 文本替换为多语言key的形式 => 生成Excel => 翻译 => 上传到多语言平台 => 发布

    测试流程:除了基本的业务case外,还需要验证多语言是否缺失、翻译准确性

    可见,国际化改造带来了一定的心智负担,尤其是业务开发,人工参与了太多了流程。

  2. 半自动化阶段(0.9~1)

    这个阶段就是着手解决在「上一阶段」暴露出来的问题,主要是通过效率工具让流程尽可能的自动化,以保证流程正确、提升效率。

    有了初期阶段的准备工作,我们先在小范围内进行了多语言改造,过程中暴露了几个明显的问题:

    1. 写法复杂,效率低t('key')  的写法需要思考映射内容
    2. 不符合语意化,代码中一堆的 key,会产生较强的割裂感,
    3. 回溯困难,定位问题文案需要先找 key,再通过映射关系找到内容
    4. 编辑Excel太耗时,对于增量key较多的页面,人工编辑Excel效率太低
    5. 翻译低效有误差,翻译同学人力有限,研发翻译还需要借助翻译App

    1~3点属于多语言语法带来的问题,4~5属于流程繁琐带来的问题。在多次分享与实践后,我们实现了多语言效率工具 X-i18n-utils,它将帮开发人员解决「文本替换」「机器翻译」「导出上传」等工作。能够在「文本替换」过程中,通过注释很好地解决了语义化低与回溯困难的问题。

    接下来举例说明X-i18n-utils解决了哪些具体问题👇

    假设要对以下这段代码做「文本替换」操作:

    import React from 'react'
    import { Card, message } from 'antd'
    import { BasicPageWrapper } from '@/components'
    
    const App: React.FC = () => {
      const year = '2024'
      const month = '11'
      const day = '12'
    
      const str = `今天是${year}${month}${day}日`
      const text = '你好'
    
      const handleClick = () => {
        message.info('早上好')
      }
    
      return (
        <BasicPageWrapper title={'标题'} desc='描述'>
          <Card onClick={handleClick}>{str}</Card>
          <Card>仪表盘</Card>
        </BasicPageWrapper>
      )
    }
    export default App
    

    可以发现,里面有一些节点需要转换:

     // 需要替换的节点
     const str = `今天是${year}${month}${day}日`
     const text = '你好'
     message.info('早上好')
     <BasicPageWrapper title={'标题'} desc='描述' />
     <Card>仪表盘</Card>
    

    按照react-i18next语法,需要将他们转换为:

    const str = t('key_str', { year, month, day })
    const text = t('key_text')
    message.info(t('key_info'))
    <BasicPageWrapper title={t('key_title')} desc={t('key_desc')}>
    <Card>{t('key_dashboard')}</Card>
    

    现在应该能看出上述所说的问题了,key的命名麻烦语义化差劲回溯困难。假如客户发了个截图让前端帮忙修复线上问题,想定位到具体是哪行代码都很难😣。

    但是如果转换结果是这样呢?

    const str = t('594cec45' /* 今天是{{year}}年{{month}}月{{day}}日 */, { year, month, day })
    const text = t('7a4e7e47' /* 你好 */)
    message.info(t('770d4127' /* 早上好 */))
    <BasicPageWrapper title={t('825dcb9e' /* 标题 */)} desc={t('c1ecbf16' /* 描述 */)}>
    <Card>{t('c253eadc' /* 仪表盘 */)}</Card>
    

    如果是这样转换,问题是不是解决了?

    1. key采用hash,不用关注命名
    2. 每个key后面跟着原文注释,解决了语义化和回溯的问题(对于中文来说)

    明确了需求,就可以实现脚本了,当然还要考虑一些边界场景:

    • 要排除的文件或目录,例如类型文件目录、工具函数目录 ['types', 'utils']
    • 要忽略的函数调用 例如日志打印、监控埋点 ['console.log', 'sentry']

    具体实现过程就不做展开了,主要是基于@babel/generator@babel/parser@babel/traverse这三个babel库实现如下流程:

     将代码解析为AST => 遍历AST替换更新指定节点 => 将AST还原为代码。
    

    解决了「文本替换」,后续工作就很容易了,因为在替换过程中可以拿到所有要做多语言的文本,对此进行「机器翻译」「导出上传」,可进一步提升效率,尤其对于翻译同学是相当友好!

    PS: 机器翻译基于腾讯云API实现

    有了X-i18n-utils后,国际化方案终于算是完整了,虽然还没有实现完全自动化,但达到了对项目进行全量替换的标准,这时候,团队可以正式开始各领域的国际化改造了。

  3. 性能优化阶段

    随着需求迭代,语言包体积逐渐增大,免不了会遇到性能问题(潜在的😅)。

    下面是当前已使用的一些性能优化策略:

    • 动态加载语言文件:应用只加载当前语言资源,避免一次性加载所有语言
    • 按模块拆分多语言:每个子应用对应一份多语言,与同上同理
    • CDN缓存策略:浏览器缓存语言包,以减少重复请求

    前两点的核心思想就是拆分后再做按需加载,react-i18next的命名空间也是基于这个思想。

    除了以上的方式,还可以定期清理不再使用的翻译键,删除冗余内容

    理论上流程应该是这样子(因为我没参与😄):

     全量扫描项目 => 生成多语言文件 => 与线上多语言文件做diff => 找出冗余key并删除
    

总结

尽管本文未能覆盖所有细节,但已概述了项目在多语言上的探索实践以及自动化最终方案的核心理念。与早期手动处理相比,目前该方案显著提高了开发效率,解决了多语言长期存在的几个问题比如写法效率低、缺乏语义化、代码回溯困难等问题。此外,它还克服了传统方法导致的项目体积膨胀,以及随之而来的性能挑战。