乐趣区

关于前端:云音乐前端国际化多语言探索实践

本文作者:atie,时浅

本文深入探讨了云音乐海内我的项目在实现多语言反对过程中的摸索和实际,从最后的手动文案治理到倒退出一套全自动化的多语言管理系统——千语平台的演变过程。文章介绍了云音乐海内团队如何通过技术创新和流程优化,无效晋升了多语言我的项目的开发效率,解决了多语言利用开发中遇到的常见问题,包含但不限于代码中的语义清晰性、文案保护的高效率,以及性能优化等挑战。通过这一系列的改良,云音乐海内我的项目可能为寰球用户提供更加晦涩和响应迅速的应用体验,同时也为多语言利用开发提供了贵重的实践经验和启发。

背景

一个国际化的产品,要在不同的国家和地区应用,就必须在设计软件时认真思考如何使产品的文本贴合当地的语种。为每个地区独自开发一个版本当然也是一个抉择,然而这样做势必节约人力,资源。云音乐海内我的项目始终在摸索如何更好更优地渲染不同语种的前端文本,目前得出的一个较优的做法是将软件与特定的语种及地区拆散,使得软件被移植到不同的语种及地区时,其自身不必做外部工程上的扭转或修改就能够将文案,图片等从源码中提取进去,渲染并显示给相应的用户。

本文侧重于分享咱们在开发多语言文案生产端(用户端)时的教训,包含开发效率、我的项目优化的思考与实际。

一些风行的语言多语言库

在介绍云音乐海内的多语言计划之前,咱们先理解下以后一些风行的多语言库以及一些惯例的做法

i18next 及 react-i18next

i18next 是一个用于前端国际化的 JavaScript 库。它提供了一个简略易用的 API,能够帮忙开发人员将应用程序本地化到多种语言。它提供了一种简洁的形式来加载翻译资源,并且反对多种资源格局(如 JSON、PO 等)。同时,它还反对动静加载和缓存翻译资源,以进步性能和用户体验。

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

用法

初始化 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;

vue-i18n

vue-i18n 是一个用于在 Vue.js 应用程序中实现国际化的库。它同样提供了一种简略易用的形式来解决对多语言的反对,使开发人员可能轻松地将应用程序本地化到不同的语言。vue-i18n 反对多种语言切换策略,包含 URL 参数、浏览器语言设置和自定义逻辑。同时它还反对动静加载和异步加载翻译资源,以进步性能和用户体验。

用法

// 筹备翻译的语言环境信息
const messages = {
  en: {
    message: {hello: 'hello world'}
  },
  ja: {
    message: {hello: 'こんにちは、世界'}
  }
}

// 通过选项创立 VueI18n 实例
const i18n = new VueI18n({
  locale: 'ja', // 设置地区
  messages, // 设置地区信息
})

<div id="app">
  <p>{{$t("message.hello") }}</p>
</div>

通过下面的代码,能够看出两个风行库的用法实际上有比拟多的类似点。大体上都是在代码中内置多语种文案,在业务代码中通过调用 i18n 办法,并传入对应文案的 key。编译的时候,会依据以后语种,读取 key 对应的文案并渲染。

一开始,云音乐海内采纳的也是与上述风行库类相似的用法来解决多语言的计划,但用得越久,咱们发现的问题越多,诸如:

  • 1、写法简单,效率低t(‘key’) 的写法须要思考映射内容
  • 2、不合乎语意化,代码中一堆的 key,会产生较强的割裂感
  • 3、回溯艰难,定位问题文案须要先找 key,再通过映射关系找到内容
  • 4、保护艰难,内置的文案,如果须要批改,会须要改代码减少开发人员的心智累赘
  • 5、代码冗余、影响性能,一个模块内的内容被反复援用,引入了不必要的文案
  • 6、我的项目迁徙难度大,一个原先国内的我的项目要接入多语言须要做大量的文本兼容

诸如此类,上述的问题一度困扰了咱们很长一段时间,而通过一年多工夫的积淀,目前海内的多语言计划曾经可能较好地解决上述咱们所面临的各种问题。上面,咱们将会介绍咱们是如何从文案治理到文案录入再到回归国内业务开发习惯(摈弃 t(‘key’) 写法)以及性能优化等一步步造成云音乐海内国际化的计划。

计划的演变

1. 千语治理平台

云音乐海内我的项目启动后,iOS、android、前端和服务端都须要为多语言的切换做筹备。在最开始的阶段海内团队尝试过用 Excel 来对立填写保护文案。然而通过 Excel 会存在如下问题:

  • 复用率低下:传统的开发模式,各端本地寄存国际化语言文本,难以反复利用;
  • 保护老本高:不同开发批改容易导致出错、命名抵触等问题且没有批改记录,无奈追溯,保护老本大;
  • 沟通艰难:产品经营和技术通过邮件、企业通信工具等沟通配合难度大;

所以咱们萌发了以下几个想法,以优化多语言反对的流程和保护:

  1. 建设对立的国际化治理平台:开发一个地方化的国际化(i18n)管理系统,用于存储、更新和检索所有语言的文本。这个平台能够为所有端(iOS、Android、前端、服务端,flutter 等)提供对立的文案资源。
  2. 告诉翻译: 开发者录入完文案之后,能够通过推送,将对应待翻译文案通过企业通信工具推送给翻译同学。
  3. 多语种文案长度比照性能:这一性能反对实时预览同一文案在不同语言下的文案长度,以便翻译人员调整文案,确保各语种版本在长度上尽可能统一,防止不同语种下产生的款式体现问题。
  4. Excel 批量解决性能:平台反对通过 Excel 进行文案的批量导入和导出,以便于高效地治理和更新大量的文本内容。
  5. 集成翻译服务:思考集成业余的翻译服务或机器翻译 API,以进步翻译效率和品质。
  6. 版本控制:应用版本控制来治理国际化文本,确保更改的可追溯性。
  7. 角色和权限治理:在国际化治理平台中实现角色和权限治理,确保产品经营、翻译人员和开发人员可能在适当的权限下进行工作。

上述的这些计划与想法最终汇合成了云音乐海内多语言文案治理平台——千语 千语 的落地,极大地提高了多语言我的项目的效率和品质,同时升高保护老本和沟通难度。

应用流程

  1. 创立利用(每个工程,或某个 App 都可创立一个利用)
  2. 创立模块(每个利用下,能够创立多个模块,个别咱们把每个独立页面,或者某一个玩法流动归笼到某一个模块下)
  3. 创立文案
  4. 公布(公布到 CDN)

对于多语言文案生产端的设计与实现,本文不做具体探讨。市面上曾经有一些对外提供服务的多语言治理平台产品,大家能够参考他们的设计与实现。

2. 千语自动化

背景

一开始云音乐海内 C 端多语言计划是应用的 i18nextreact-i18next 这两个库实现的。

该技术计划与下面介绍的 i18nextreact-i18next 库的用法统一,区别在于一个是咱们文案不是写死在代码中,而是通过 CDN 来获取文案内容,二是为了项目管理不便,咱们的“key”是由我的项目模块 module(module 能够了解为一个命名空间,不同的页面能够独自定一个 module,不同的利用也能够定一个 module)以及惟一键 key(key 能够了解为一个文案的惟一标识) 组成,具体计划大抵如下:

  1. 千语平台公布前端文案到 CDN 上
  2. 前端申请 CDN 获取多语言文案(由 key 跟文本组成的 JSON),并用 i18next 初始化
  3. 业务代码中应用 react-i18nextuseTranslation,文案通过编写 t('module:key'),也即 react-i18nextt('key') 来获取对应模块下的文本映射
  4. 最终渲染页面

咱们开发流程大抵如下:

  1. 千语平台上录入文案
  2. 告诉翻译同学翻译文案
  3. 公布文案到 CDN,更新 CDN 版本
  4. 批改代码中的 CDN 版本号,这样咱们的文案能力申请到指定版本的文案
  5. 前端代码中文案通过书写 t('module:key')

2.1 千语自动化 1.0

在经验屡次需要迭代后,咱们发现以后的多语言计划效率不佳。工作流程中须要频繁切换平台和 IDE,并且波及批改 CDN 资源的版本号来确保获取最新的 CDN 资源。另外,代码中应用的 t('module:key') 短少清晰的语义表白,这升高了其易了解性和维护性。因而,咱们开始思考施行多语言文案的自动化策略,以晋升效率和代码品质。

梳理可自动化流程

为了进步云音乐海内我的项目的工作流程效率,通过深刻探讨,咱们决定对现有流程进行以下优化:

  1. 简化代码书写:不再应用传统的指定 modulekey 的办法编写国际化代码,改为间接应用 $i18n('中文') 进行书写,简化开发过程并进步代码的可读性。
  2. 自动化文案治理:开发人员无需手动在千语平台的文案治理页面创立录入文案。千语自动化插件将主动提取代码中的待翻译中文文案并自行创立惟一键 key 并上传,缩小人工操作和潜在的谬误。
  3. 主动公布文案:一旦文案上传实现,零碎将主动触发公布流程,将文案推送至 CDN,无需开发人员手动染指,进步公布效率。
  4. 自动化版本治理:勾销手动批改 CDN 版本号的步骤,通过读取缓存中的版本号,确保流程的连贯性和准确性。

通过这些流程的优化,开发人员在编码时只需简略地应用 $i18n() 包裹中文文案,残余的翻译上传、公布到 CDN 以及版本治理等流程均由自动化工具实现。这样不仅极大地晋升了开发效率,也保障了流程的一致性和准确性,让团队可能更专一于外围开发工作。

实现计划

架构图

为了晋升工作效率并实现国际化文案的自动化治理,咱们设计了一个两阶段的自动化计划:

第一阶段:文案主动替换

  • 技术实现:利用自开发的 babel 插件,这个插件通过剖析形象语法树(AST),辨认出代码中的 $i18n('你好') 表达式。同时插件会以以后我的项目设定的模块 module 主动查问多语言平台,找到对应的 module 下“你好”这个文本的 key,而后将原始的 AST 节点 $i18n('你好') 替换成 t('module:key') 格局。
  • 迭代更新 :在后续的版本迭代中,咱们减少了对间接应用中文文案的反对(也即摒弃了$i18n() 办法包裹的模式,通过 babel 插件间接辨认代码中的中文文案,如“你好”),进一步简化了开发过程。

第二阶段:文案主动提取与上传

  • 过程形容:在代码提交前,通过 commit 钩子扫描批改过的代码。该过程与之前在文案主动替换阶段创立的缓存文件进行比照,以确定新的或批改过的文案。而后,将这些文案主动上传到多语言治理平台。
  • 主动触发公布:文案上传后,主动触发平台的公布流程,次要更新文案版本号。这确保了在代码的热更新过程中,如果文案发生变化,文案主动替换阶段可能辨认并拉取最新的文案资源。

通过这个计划,咱们极大地简化了国际化文案的治理流程,从手动操作转向自动化解决,显著晋升了开发效率并缩小了人为谬误,使得团队可能更加专一于产品的外围性能开发。

重点局部

资源缓存

工具包会缓存版本号跟文案资源到包中。初始化的时候,会先比照版本号是否统一,如果不统一,拉取平台最新文案,并缓存到本地,供前面 babel-plugin 文案替换应用。

技术计划中比较复杂的局部波及到 AST,一个是 babel-plugin,一个是 commit 的时候的执行的 node 脚本。上面我将提供阉割过的代码,带大家理解下 AST 局部的实现。

babel-plugin

{
  return {
    visitor: {
      Program: {enter(programPath, { filename}) {
          programPath.traverse({
            // 拦挡纯中文的节点
            StringLiteral(path) {visitorCallback(path, filename);
            },
            // 拦挡纯中文的节点
            JSXText(path) {visitorCallback(path, filename);
            },
            // 拦挡 $i18n() 的节点
            CallExpression(path) {ExpressionCallback(path, filename);
            },
          });
        },
      },
    },
  };
}

下面三个节点,别离对应咱们代码中的五种写法。

  • 纯中文写法
  • $I18n() 写法(万能写法,反对很多性能)

    • $i18n('纯中文')
    • 文案中带有变量$I18n('你好!%1', { 1: name}),%1 会被替换 name 对应的值
    • $i18n({module: 'shop', key: 'dress'}),反对 module key 的写法
    • $i18n({text: '你好!<1>%1</1>', components: { 1: <span>}, values: {1: name}})多语言组件写法,例子最终会被替换为 你好 <span>{name}</span>。比方 name 须要通过标签来批改他的款式。

visitorCallback

纯中文节点解决逻辑

function visitorCallback(path, filename) {const CNValue = path.node.value.trim();
  // 先判断是否中文 [yes] 已验证匹配到了所有中文
  if (!(isChinese(CNValue) && !isIgnoreNode(path))) return;
  // 第一种状况是打包时携带对应的语种进来
  const languageModules = DefaultLangObj;
  // 找到匹配到对应模块的 module:key
  const currentModuleName = getModuleNameByRelativePath(Path.relative(i18nConfig.rootPath, filename),
  );
  const currentCNObj = LOCAL_DOC?.["zh-CN"]?.[currentModuleName] || {};
  const textKey = Object.keys(currentCNObj).find((key) => currentCNObj[key] === CNValue,
  );
  // 替换原来的中文文案节点为以后语种对应的文案节点
  const languageText =
    languageModules?.[currentModuleName]?.[textKey] || CNValue;
  path.replaceWith(t.stringLiteral(languageText));
}
  1. 通过拦挡的中文,找到对应中文在千语平台上的 module 和 key
  2. 在对应语种文案汇合中通过 module 和 key 找到对应的文案
  3. 文案替换

ExpressionCallback

$i18n() 写法解决逻辑

function ExpressionCallback(path, filename) {// 如果外面是对象 对应 $i18n({})
  if (t.isObjectExpression(node?.arguments[0])) {// 没有 components 属性,代表是 $i18n({ module, key}) 写法
    if (!hasComponentAttr && keyFind && moduleFind) {
      const languageModules = DefaultLangObj;
      const key = keyFind.value.value;
      const module = moduleFind.value.value;
      // 找到匹配到对应模块的 module:key
      const languageText = languageModules?.[module]?.[key];

      const valuesProps = findProperty(properties, VALUES);
      // 有本地文件的解决形式

      const newLiteral = t.stringLiteral(languageText);
      // ... 一堆代码逻辑
      // 通过下面的 module key 从缓存文件中找到对应语种的文案,并替换
      path.replaceWith(newLiteral);
      path.skip();}
    // 如果外面有 components 属性,代表是多语言组件写法
    if (hasComponentAttr) {const CNAttr = findProperty(properties, TEXT);
      const valuesProp = findProperty(properties, VALUES);
      // ... 一堆代码
      // 封装成一个 react 组件返回
    }
  }
  // 如果外面是文本
  if (t.isLiteral(node?.arguments[0])) {// 主逻辑大抵同下面纯文本 visitorCallback 的逻辑,只是多了一些逻辑的判断,兜底语种等性能}
}
  1. 通过拦挡的中文,找到对应中文在千语平台上的 module 和 key
  2. 在对应语种文案汇合中通过 module 和 key 找到对应的文案
  3. 判断不同的写法类型,转化成相应的内容

接入指南

const {I18nPlugin} = require("@music/i18n");

webpackChain: (chain) => {chain.plugin("i18n").use(I18nPlugin, [{ id: 190}]); // id 对应千语多语言平台的利用 id
};

使用指南

对于那些好奇如何在文案中嵌入变量或从接口动静获取数据的同学,这里提供了几种次要的应用形式来适应不同的场景:

  1. 间接应用中文:当文案中不蕴含变量时,书写纯中文即可。
<p> 你好 </p>
  1. 嵌入变量的文案:应用 $i18n('我有一个 %1', { 1: apple}) 的格局来插入变量。例如,$i18n('%1 world', { 1: 'hello'}) 容许你将 hello 作为变量动静插入到文案中。
  2. 应用已有文案的援用:通过 $i18n({key, module, fallbackText}) 格局援用千语零碎中已存在的文案。其中,fallbackText 作为未胜利匹配文案时的备选内容。
  3. 组件中的简单文案

    $i18n({
      text: "价格 <1>%1</1> 商品名 <2>%2</2>",
      components: {1: <p style={{ margin: "0 5px", color: "#FDE020"}} />,
        2: <p style={{color: "#FDE020"}} />,
      },
      values: {
        1: price || "",
        2: name || "",
      },
    });

    这种办法容许在文案中嵌入 React 组件,并通过 values 传递变量。

咱们也在一直摸索更优的用法来进一步晋升开发体验。近期,咱们打算引入基于字符串模板的变量嵌入形式,如通过 ${hello} world 的模式来实现。这将使得带变量的文案书写更加直观和便捷,为开发者带来更佳的开发体验。

2.2 千语自动化 2.0:性能优化计划

我的项目性能同样是海内我的项目的一个重要的考量因素。尽管基于 i18nextreact-i18next 实现的自动化计划无效晋升了开发效率,解决了一系列的效率问题,但它并未充沛解决由多语言反对引入的各种性能挑战:

  1. 多语言资源加载:我的项目须要从 CDN 预加载多语言资源,或将所有语种文案打包进我的项目中,这减少了首屏加载工夫。
  2. 库依赖:引入 i18nextreact-i18next 两个库,导致我的项目体积减少。
  3. 渲染提早:我的项目必须期待多语言库初始化实现后,能力进行最终渲染,影响用户体验。
  4. 动态站点生成(SSG)不敌对:以后计划不反对 SSG 预构建,无奈为不同语种国家提供同一份预构建的产品(因为不同国家的语言不同)。

2.2.1 解决方案摸索🤔️

为了克服这些性能问题,咱们决定跳呈现有自动化计划的限度,采纳一种新的思路:为每个语种创立独立的构建包。这个构建包将仅蕴含所需的语种文案,无需携带多余的语种信息或依赖 i18nextreact-i18next 库。这样,咱们能够针对不同的语种提供精简且高效的构建产物,防止不必要的资源加载和库依赖,同时解决 SSG 预构建的问题。

通过这种多构建产物计划,咱们旨在显著进步我的项目的加载速度和运行效率,同时维持开发过程的自动化和高效性,为用户提供更加晦涩和响应疾速的体验。

2.2.2 技术计划

为了晋升我的项目性能并解决多语言反对带来的挑战,咱们对原有的自动化计划进行了屡次优化和调整:

2.2.3 生产产物的优化

编译阶段的改良

  • 引入了 I18N_LANGUAGE 环境变量,在构建过程中指定以后构建指标的语种。
  • 利用自定义的 babel 插件,在 AST 分析阶段将代码中的纯中文或通过 i18n() 办法包裹的文案,间接替换为以后构建语种对应的文案。这一步骤实现了在源代码层面的语言特定优化。

    • 前一阶段能够简略了解为 中文 /$i18n(‘ 中文 ’)** 通过 babel 转成 **$i18n(‘module:key’) ===> 对应语种文案
    • 现阶段间接越过了两头阶段,间接将中文文案编译成对应语种文案

例子

平台文案

{
  'zh-CN': {hello: '你好'},
  'en-US': {hello: 'hello'}
}

源代码

import React from "react";

const Main = () => {return <div> 你好 </div>;};

如果构建的时候,指定了英语语种,源代码会被转换成

import React from "react";

const Main = () => {return <div>hello</div>;};

构建产物理论是编译过的代码,下面的代码只是为了阐明文案原地替换

产物输入阶段的调整

  • 调整了构建产物的 publicPath 设置为 dist/${I18N_LANGUAGE},确保每个语种的构建产物被搁置在独立的目录中。这样,dist 目录下将组织有针对不同语种的构建包,使得资源管理更为清晰和高效。

构建进去的 dist 目录如下

.
├── en-US
├── id-ID
├── tr-TR
└── zh-CN
...

这样不同语种的门路如 /heatup/en-US/pageA,就会指向到 en-US 构建产物中的 pageA 页面。

2.2.4 生产产物的变更

拜访门路的调整

  • 咱们从原先间接拜访如 /pageA 的形式,转变为拜访指定语种的门路,例如 /${language}/pageA。这意味着,客户端在加载某个 WebView 页面时,会依据 APP 以后抉择的语种,主动将链接调整为对应的语种版本,如拜访 /en-US/pageA
  • 通过这种形式,资源申请间接指向 dist/en-US 下的构建包,从而实现了语种特定的资源加载,缩小了不必要的资源申请和加载工夫,晋升了页面响应速度和用户体验。

通过上述改变,咱们不仅晋升了我的项目的运行效率,缩小了不必要的资源累赘,也实现了更加灵便和高效的多语言反对计划。这些优化确保了我的项目在寰球多语种环境下的性能体现同时保障了海内的用户体验。

总结

只管本文未能笼罩所有细节,但已概述了云音乐海内我的项目在多语言上的摸索实际以及目前云音乐海内多语言自动化最终计划的核心理念。与晚期手动解决相比,目前该计划显著进步了开发效率,解决了多个长期存在的问题比方频繁手动输出文案的繁琐、代码中文案不足清晰语义以及文案反复输出等问题。此外,它还克服了传统办法导致的我的项目体积收缩,以及随之而来的性能挑战。

通过自动化解决流程的引入和优化,云音乐海内我的项目不仅晋升了工作流的效率,还确保了我的项目的轻量化和高性能运行,从而为海内用户提供了更加晦涩和响应迅速的体验。云音乐海内多语言计划使得团队可能更专一于翻新和晋升产品质量,同时为用户带来更优质的服务。而于此同时咱们也面临着更多的挑战,对多语言我的项目的优化、晋升,仍是云音乐海内项目组须要一直思考与摸索的课题。

最初

更多岗位,可进入网易招聘官网查看 https://hr.163.com/

退出移动版