静态翻译解耦,解放开发双手

562 阅读2分钟

CF翻译的前世今生大总结

↑↑↑

上一篇提到,为了支持全站本地化事业,我们搭起翻译平台、打通node网关,初步打通了全链路翻译并实现了平台化。

然而有些流程在具体操作上还是比较僵硬,这里我们着重讲一下针对静态翻译部分所作的优化。

我们所谓静态翻译、即写在各前端项目中的静态文案。例如行动按钮、区域标题等位置的文案,由于相对固定,普遍会直接写死在前端项目里。这些文案通常来源于产品需求,开发所要做的,只是机械性地将文案复制粘贴到对应位置,之后随着项目一起发布。这是最原始的工作流程,而在翻译平台诞生之后,我们将项目里原本写死的文案替换成了翻译平台里的term(词条),引用cf-i18n库来实现项目的翻译功能。

以react项目为例,大体实现流程如下:

// 初始化Intl
import Intl from 'cf-i18n'
Intl.init({
  language: lang, // 设置当前语言
  resources: {
    [lang]: {
      translation: window[`cf_i18n_${lang}`],
    },
    en: {
      translation: window.cf_i18n_en || require('@/locales/en.json'), // 本地英语文件兜底
    },
  },
})

en.json

{
  "app.term1": "这里是一句文案",
  "app.term2": "这里是另一句文案",
}

组件内

<Intl text='app.term1' />
or
Intl.t('app.term2')

相应位置即可实现翻译。


做完这一步,开发就从原本的,拷贝文案到对应位置->发布项目,变为了,在翻译平台更新翻译->拉取到本地->发布项目。

聪明的小朋友应该已经发现,这其实只是给产品提供了一个编辑文案的入口,而开发工具人的本质并没有得到多少改善。

此外还有一个现状依然存在,即有时我只是想更新一个文案,或者翻译里有一个typo需要纠正,却需要发布整个前端项目,而项目代码本身并没有任何改动。

为了一并解决这些问题,我们对每个静态项目提出两个问题

  1. 本地静态文件的引入是否可以使用cdn文件代替?
  2. 前端项目是否可以主动获取最新的翻译内容?

只要解决这两点,我们就可以真正地解放开发的双手,将翻译内容的更新与发布完全与项目本身解耦,并把静态翻译发布的权利交出去,不再只有开发可以操作。

第一个问题:

其实取决于更项目实现翻译的方式,RN项目可以直接引用静态cdn文件来实现翻译。

而使用cf-i18n的各react前端项目,需要在初始化时能获取翻译内容,因此如果cdn静态文件内能将翻译内容挂在window下即可替换。

第二个问题:

目前react项目统一由node-web进行html的渲染,因此可以将拼接cdn链接的工作放到node-web。

RN方面,可以在打开app和下拉刷新页面时,拼接cdn链接并将内容渲染进页面。

cdn链接统一由cookie里的languageCode翻译项目ID项目所需格式版本拼接而成。

目前版本的存储运用了分环境的阿波罗配置系统,项目可以从阿波罗配置中读取不同环境的对应项目的版本hash值,而这个hash值也可以通过在翻译平台点击部署按钮时随机产生并更新到阿波罗配置中。

确认这两个问题可以解决以后,还差最后一个环节,也就是如何让cdn能返回正确的翻译内容?

cdn回源到node-web,因此可以在node-web里对指定router做返回翻译内容的操作,我们将翻译文件的router定为:

/locales/{projectId}/{locale}.{version}.{format}

其中包含了四个变量:

  1. projectId —— 翻译平台中的项目id
  2. locale —— 语言code,统一只取语言不加地区,如en-US和en统一为en,表示英语
  3. version —— 表示版本的hash值
  4. format —— 文件格式,包含翻译平台支持的所有格式

带着项目id、语言code、文件格式这些参数请求tr-api的接口可以获取相应的翻译内容,我们将内容写进缓存,同时可以减少对tr-api的请求压力。而版本,其实就相当于一个更新信号,版本变化意味着需要重新请求tr-api,从而获取最新的翻译内容。

这里贴出具体代码和大家交流交流:

import { Context, Next } from 'koa'
import mime from 'mime-types'
import _ from 'lodash'
import { Cacher, Updater } from 'node-club-native'
import { transaction } from 'decorators'

/**
* 获取翻译文件
*
* @name getLocale
* @function
* @param {Koa.Context} ctx Koa.Context
* @param {Koa.Next} next Koa.Next
*/

@transaction
async getLocale(ctx: Context, next: Next): Promise<void> {
  if (ctx.params.version) {
    ctx.set('Cache-Control', `max-age=${365*60*60*24}`)
  } else {
    // 获取version异常,可能原因:1\. node-config接口挂掉,2\. 没有version配置
    ctx.set('Cache-Control', `max-age=${60*60*24}`)
  }
  ctx.type = mime.lookup(ctx.params.format) || 'plain/text'
  const cached_key = `${ctx.params.project}.${ctx.params.locale}.${ctx.params.version}.${ctx.params.format}`
  const cached = await this.cache.blocked_cached(cached_key)

  cat.info('Locale Files', cached_key)

  // 命中缓存则直接取出
  if (cached.hasOwnProperty('cache')) {
    if ((cached as Cacher).cache.value) {
      ctx.body = (cached as Cacher).cache.value
    } else {
      ctx.status = 500
    }
  // 没有命中就请求tr-api接口
  } else {
    const tr_host = ['pre', 'prod'].includes(app.env) ? 'xxx.xx.xx.xxx:xxxx' : 'xx.xxxxxx.xxx'
    const format_code = _.get(_.find(FORMATS, ['extension', ctx.params.format]), 'code', ctx.params.format)
    await http.get(`http://${tr_host}/api/v1/projects/${ctx.params.project}/exports?locale=${ctx.params.locale}&format=${format_code}&completion=en`, {
      headers: {
        'Authorization': `Bearer xxxxxxx` // token手动加密
      },
      timeout: 0,
    }).then(res => {
      (cached as Updater).update(res.data)
      ctx.body = res.data
    }).catch(e => {
      // 请求异常1分钟后重新请求
      (cached as Updater).update('', 60*1000)
      ctx.status = 500
    })
  }
}

最终整个流程如图所示,目前可以覆盖所有react前端项目和RN项目:


从此以后, 产品想要更改文案或翻译,只需在翻译平台进行修改后,点击部署到对应环境,即可实时看到更改,开发将从此流程中彻底解放双手✿✿ヽ(°▽°)ノ✿

那如果在翻译平台找不到对应翻译呢?

那如果产品根本不知道这是不是静态翻译呢?

那如果是新增词条和翻译呢?

任重而道远……

本文搬运自2020.6.19发布于公司内部论坛的同名博文。