Vue3后台多语言翻译配置模块整理


多语言不只是前端项目里怎么使用 i18n,后台还需要有一套文案维护、导入导出和缺失检查的配置模块

之前做多语言的时候,更多关注的是前端怎么使用。

比如页面上怎么写 $t('xxx'),uni-app 里面怎么切换语言,或者 JSON 文件应该放在哪里。

但是项目做久了以后会发现,真正麻烦的地方不只是使用多语言,而是怎么维护多语言。

如果文案都靠开发手动改 JSON,就会有几个问题:

  1. 运营不能自己维护文案
  2. 翻译缺失不容易发现
  3. 新增语言时要改很多文件
  4. 前端、后端、产品之间很难对齐 key
  5. 导入导出翻译表格很麻烦

所以后台里面单独做一个多语言翻译配置模块,还是很有必要的。

页面结构

这个模块主要是左树右表结构。

左边是文案目录,右边是当前目录下的翻译列表。

大概结构是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div class="page-layout translation-page">
<div class="search-component">
语言筛选、查询、导入、导出
</div>

<div class="Table-page" ref="table">
<div class="translation-layout">
<div class="translation-tree-panel">
文案目录树
</div>

<div class="translation-table-panel">
翻译表格
</div>
</div>
</div>
</div>

左树右表这种结构在后台里面很常见。

比如菜单管理、组织架构、分类管理、权限管理,都可以用类似的布局。

接口统一管理

接口还是统一放在 apis.js 里面。

1
2
3
4
5
6
7
8
9
10
11
12
translation: {
tree: '/support/translate/catalog/list',
list: '/support/translate/list',
addGroup: '/support/translate/catalog/add',
updateGroup: '/support/translate/catalog/update',
deleteGroup: '/support/translate/catalog/del',
addText: '/support/translate/add',
updateText: '/support/translate/update',
deleteText: '/support/translate/del',
importJson: '/support/translate/import',
importXlsx: '/support/translate/importXlsx'
}

页面里面就不要到处写接口地址。

比如请求翻译列表时:

1
2
3
4
5
this.$service.request({
url: this.$apis.operationsConfig.translation.list,
method: 'post',
data: sendMessage
});

这样后面接口地址调整时,只需要改一处。

文案目录树

左侧目录树的数据由后端返回。

常用字段大概是:

1
2
3
4
5
6
7
{
translateCatalogId,
translateKey,
translateDesc,
translateCount,
children
}

前端需要把它整理成 el-tree 好展示的结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
normalizeTreeNode(item, parentFullKey = '') {
const key = item.translateKey;
const fullKey = key ? (parentFullKey ? `${parentFullKey}.${key}` : key) : parentFullKey;
const children = item.children.map((child) => this.normalizeTreeNode(child, fullKey));
const name = item.translateDesc;
const label = key ? `${name} - ${key}` : name;

return {
...item,
label,
fullKey,
count: item.translateCount,
children
};
}

这里的 label 是给页面看的。

真正保存时,还是以后端需要的字段为准。

右键菜单

目录树上我做了右键菜单。

主要操作有:

  1. 添加子文案
  2. 添加翻译
  3. 编辑目录
  4. 删除目录

右键菜单看起来简单,但是实际写的时候要注意位置。

如果用户在页面底部右键,菜单不能直接跑到屏幕外面。

1
2
3
4
5
6
7
8
9
10
getContextMenuPosition(left, top, width = 128, height = 150) {
const safeGap = 8;
const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;

return {
left: Math.max(safeGap, Math.min(left, viewportWidth - width - safeGap)),
top: Math.max(safeGap, Math.min(top, viewportHeight - height - safeGap))
};
}

菜单渲染出来以后,还可以再按真实宽高修正一次。

这样不管是在顶部、底部、左侧还是右侧右键,都不会遮得太奇怪。

翻译列表

后端返回的翻译列表,一般不是直接给表格用的格式。

它可能是一条文案下面带多个语言:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
jsonKey: 'home.title',
belongTranslateCatalogId: 1,
translateList: [
{
languageId: 1,
languageName: '简体中文',
translateContent: '首页'
},
{
languageId: 2,
languageName: 'English',
translateContent: 'Home'
}
]
}

但是表格更适合展示成:

1
key | 中文 | 英文 | 其他语言

所以前端需要整理一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
normalizeTranslationRows(list) {
return list.map((item) => {
const groupId = item.belongTranslateCatalogId;
const jsonKey = item.jsonKey;
const translateList = item.translateList;
const languageValues = {};
let zhHans = '';

translateList.forEach((translateItem) => {
const code = this.getLanguageCodeById(translateItem.languageId);

if (!code) {
return;
}

if (code == 'zh-Hans') {
zhHans = translateItem.translateContent;
} else {
languageValues[code] = translateItem.translateContent;
}
});

return {
...item,
id: `${groupId}-${jsonKey}`,
zhHans,
languageValues
};
});
}

中文单独放到 zhHans

其他语言统一放到 languageValues 里面。

这样后面动态展示语言列就会方便很多。

动态语言列

中文和英文是固定展示的。

其他语言可以通过顶部复选框控制。

1
2
3
4
5
extraLanguageOptions() {
return this.languageList
.filter((item) => !this.isFixedLanguage(item))
.map((item) => this.formatLanguageOption(item));
}

表格里面根据勾选结果动态渲染列:

1
2
3
4
5
6
7
8
9
10
11
12
<el-table-column
v-for="item in languageColumnOptions"
:key="item.value"
align="left"
:label="item.label"
min-width="200"
show-overflow-tooltip
>
<template #default="scope">
{{ getLanguageValue(scope.row, item.value) }}
</template>
</el-table-column>

这样新增一种语言时,不需要改表格模板。

只要语言列表接口返回了,它就可以被勾选展示。

缺失翻译统计

这个模块里面我觉得比较实用的是缺失翻译统计。

比如英文缺失数量:

1
2
3
englishMissingCount() {
return this.tableData.filter((item) => !this.getLanguageValue(item, 'en')).length;
}

已选语言缺失数量:

1
2
3
4
5
6
7
8
9
selectedLanguageMissingCount() {
if (this.selectedLanguageCodes.length == 0) {
return 0;
}

return this.tableData.filter((item) =>
this.selectedLanguageCodes.some((code) => !this.getLanguageValue(item, code))
).length;
}

这个功能对开发来说不复杂,但是对运营很有用。

因为一眼就能知道当前目录下还有多少文案没翻译。

新增和编辑翻译

新增、编辑翻译时,前端保存的数据不要传一堆展示字段。

只传后端需要的字段:

1
2
3
4
5
6
7
8
9
10
{
jsonKey,
belongTranslateCatalogId,
translateList: [
{
languageId,
translateContent
}
]
}

这里要注意:

  1. 保存时用 languageId
  2. 展示时用语言编码,比如 zh-Hansen
  3. 未填写的翻译传空字符串
  4. 编辑时 key 也要能正常回填

不要把语言编码直接传给后端,除非接口就是这么约定的。

导入和导出

多语言模块离不开导入导出。

常见场景是:

  1. 导出 JSON 给前端项目使用
  2. 导出 XLSX 给翻译人员处理
  3. 导入 JSON 回填系统
  4. 导入 XLSX 批量更新翻译

导出时,我觉得最好跟当前页面选择保持一致。

比如页面当前展示了中文、英文、越南语,那导出时就带这些语言:

1
2
3
4
5
6
exportData() {
return {
...this.getSelectedGroupParams(),
languageIdList: this.getVisibleLanguageIdList()
};
}

获取可见语言 ID:

1
2
3
4
5
6
7
getVisibleLanguageIdList() {
const codes = ['zh-Hans', 'en', ...this.selectedLanguageCodes];

return Array.from(new Set(codes))
.map((code) => this.getLanguageIdByCode(code))
.filter((item) => item !== undefined && item !== null);
}

这样导出的内容和页面上看到的语言范围一致,不容易混乱。

为什么不继续用本地JSON

一开始做多语言时,可能会把 zh-Hans.jsonen.json 这些文件直接放在前端项目里。

项目小的时候这样没问题。

但是文案越来越多、语言越来越多以后,继续手动维护 JSON 就不太合适了。

后台配置模块的作用就是把这些事情交给系统处理:

  1. 文案目录由后台维护
  2. 翻译内容由后台维护
  3. 缺失翻译由后台统计
  4. JSON 和 XLSX 由后台导入导出
  5. 前端只负责使用最终产物

这样开发和运营的边界会清楚很多。

uni-app中怎么使用

后台维护好多语言以后,uni-app 里面通常使用后台导出的 JSON。

比如导出的文件是:

1
2
3
4
locale
zh-Hans.json
en.json
vi.json

内容大概是:

1
2
3
4
5
6
7
8
{
"home": {
"title": "首页"
},
"button": {
"confirm": "确定"
}
}

uni-app 项目里可以引入这些语言包:

1
2
3
4
5
6
7
8
9
import zhHans from '@/locale/zh-Hans.json';
import en from '@/locale/en.json';
import vi from '@/locale/vi.json';

const messages = {
'zh-Hans': zhHans,
en,
vi
};

如果项目用了 vue-i18n,可以这样初始化:

1
2
3
4
5
6
7
8
9
10
import { createI18n } from 'vue-i18n';

const i18n = createI18n({
legacy: false,
locale: uni.getStorageSync('locale') || 'zh-Hans',
fallbackLocale: 'en',
messages
});

export default i18n;

main.js 里面挂载:

1
2
3
import i18n from './locale/index.js';

app.use(i18n);

页面中使用:

1
2
3
<template>
<view>{{ $t('home.title') }}</view>
</template>

切换语言时:

1
2
3
4
function changeLocale(locale) {
i18n.global.locale.value = locale;
uni.setStorageSync('locale', locale);
}

这样用户切换后,下次打开应用还能记住当前语言。

uni-app使用时要注意什么

uni-app 多语言使用时,我觉得要注意这几个点:

  1. 语言编码要和后台一致,比如 zh-Hansen
  2. key 不要随便改,改了以后旧页面可能取不到文案
  3. 后台导出的 JSON 最好保持层级结构
  4. 新增语言后,要确认 uni-app 端已经引入对应 JSON
  5. 如果文案来自接口,也要和本地 JSON 的 key 保持一致

如果 App、小程序、H5 都要支持多语言,最好统一一套语言编码。

不要后台叫 zh_CN,前端叫 zh-Hans,uni-app 又叫 zh

这种差异多了以后,后面维护会很累。

总结

多语言配置模块的重点,不是教页面怎么写 $t

它解决的是后台怎么维护文案、怎么管理语言、怎么导入导出、怎么发现缺失翻译。

整体思路就是:

  1. 左侧维护文案目录
  2. 右侧维护翻译列表
  3. 中文英文固定展示
  4. 其他语言动态勾选
  5. 缺失翻译实时统计
  6. 支持 JSON 和 XLSX 导入导出
  7. uni-app 使用导出的语言包

这样后台负责管理,前端项目负责使用,整个多语言流程会清楚很多。

以上就是我对 Vue3 后台多语言翻译配置模块的一些整理,如有错误,欢迎大佬指出。

-------------本文结束感谢您的阅读-------------