多语言不只是前端项目里怎么使用 i18n,后台还需要有一套文案维护、导入导出和缺失检查的配置模块
之前做多语言的时候,更多关注的是前端怎么使用。
比如页面上怎么写 $t('xxx'),uni-app 里面怎么切换语言,或者 JSON 文件应该放在哪里。
但是项目做久了以后会发现,真正麻烦的地方不只是使用多语言,而是怎么维护多语言。
如果文案都靠开发手动改 JSON,就会有几个问题:
- 运营不能自己维护文案
- 翻译缺失不容易发现
- 新增语言时要改很多文件
- 前端、后端、产品之间很难对齐 key
- 导入导出翻译表格很麻烦
所以后台里面单独做一个多语言翻译配置模块,还是很有必要的。
页面结构
这个模块主要是左树右表结构。
左边是文案目录,右边是当前目录下的翻译列表。
大概结构是这样:
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 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 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 } ] }
|
这里要注意:
- 保存时用
languageId
- 展示时用语言编码,比如
zh-Hans、en
- 未填写的翻译传空字符串
- 编辑时 key 也要能正常回填
不要把语言编码直接传给后端,除非接口就是这么约定的。
导入和导出
多语言模块离不开导入导出。
常见场景是:
- 导出 JSON 给前端项目使用
- 导出 XLSX 给翻译人员处理
- 导入 JSON 回填系统
- 导入 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.json、en.json 这些文件直接放在前端项目里。
项目小的时候这样没问题。
但是文案越来越多、语言越来越多以后,继续手动维护 JSON 就不太合适了。
后台配置模块的作用就是把这些事情交给系统处理:
- 文案目录由后台维护
- 翻译内容由后台维护
- 缺失翻译由后台统计
- JSON 和 XLSX 由后台导入导出
- 前端只负责使用最终产物
这样开发和运营的边界会清楚很多。
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 多语言使用时,我觉得要注意这几个点:
- 语言编码要和后台一致,比如
zh-Hans、en
- key 不要随便改,改了以后旧页面可能取不到文案
- 后台导出的 JSON 最好保持层级结构
- 新增语言后,要确认 uni-app 端已经引入对应 JSON
- 如果文案来自接口,也要和本地 JSON 的 key 保持一致
如果 App、小程序、H5 都要支持多语言,最好统一一套语言编码。
不要后台叫 zh_CN,前端叫 zh-Hans,uni-app 又叫 zh。
这种差异多了以后,后面维护会很累。
总结
多语言配置模块的重点,不是教页面怎么写 $t。
它解决的是后台怎么维护文案、怎么管理语言、怎么导入导出、怎么发现缺失翻译。
整体思路就是:
- 左侧维护文案目录
- 右侧维护翻译列表
- 中文英文固定展示
- 其他语言动态勾选
- 缺失翻译实时统计
- 支持 JSON 和 XLSX 导入导出
- uni-app 使用导出的语言包
这样后台负责管理,前端项目负责使用,整个多语言流程会清楚很多。
以上就是我对 Vue3 后台多语言翻译配置模块的一些整理,如有错误,欢迎大佬指出。