news 2026/11/2 4:04:33

SkyWalking UI 国际化实战:语言包配置、组件库联动与 key 校验

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SkyWalking UI 国际化实战:语言包配置、组件库联动与 key 校验

1. 先搞清楚 SkyWalking UI 的语言包到底管哪些文案

SkyWalking UI 的国际化配置,说白了就是把前端里所有硬编码的英文文案抽成 key,再按语言文件喂回去。但真正动手之前得先明白一件事:$t能解决的只是四类文案里的一类,剩下三类漏掉任何一个,切换语言后界面就会出现“半中文半英文”的尴尬。这套分层认知决定了后面所有配置和排错思路。

1.1 界面文案的四种来源,只有一类走语言包

我在本地把整个 UI 的文案来源捋过一遍,按可控程度排下来大概是这样:

  • 静态文案:菜单名、表头、按钮、表单 label、空状态提示。这类全在src/assets/lang/*.ts里,是$t的主战场,占比最大,也是最容易做干净的部分。
  • 组件库内置文案:分页器的“条/页”、日期选择器的月份名、表格无数据时的“暂无数据”、上传组件的提示。这些藏在组件库自己的 locale 包里,跟你写的语言文件毫无关系,必须单独设置,否则中文界面里永远夹着几处英文。
  • 后端返回的枚举和状态:告警规则的触发类型、服务状态、Span 类型这类的值,接口直接返回英文枚举串。前端要做的是“值 → i18n key”的映射,而不是翻译返回值本身。
  • 用户自定义数据:服务名、端点名、标签键值、实例名。这类内容永远不翻译,翻译了反而是事故。

分层清楚之后再看语言文件,你就知道哪些 key 该写、哪些根本不该出现在语言包里。很多新手把后端返回的枚举也硬塞进语言文件里做全量映射,结果接口一加新枚举,界面就直接显示空字符串,这是典型的边界没划清。

1.2 为什么选 vue-i18n 而不是自己撸一个字典

SkyWalking UI 用的是 Vue 技术栈,国际化沿用社区主流的 vue-i18n。有人可能觉得,不就是个字典查表,自己写个getText(key)不香吗?实际操作下来,自研方案会在四个地方翻车:

  • 占位符插值:{name}这类动态参数,自研方案要自己写正则替换,还得处理参数缺失的空值。vue-i18n 原生支持,一行$t('key', { name })搞定。
  • 响应式:语言切换后视图要自动刷新。vue-i18n 挂在 Vue 实例上,locale一改,所有$t自动重算。自研方案要么强制刷新页面,要么手写全局事件,成本反而更高。
  • 回退机制:某个 key 在新语种里没翻译,fallbackLocale会兜底到英文,界面不至于出现service.list.title这种裸 key。这个能力在小语种渐进翻译的场景里非常关键。
  • 复数与格式化:虽然 SkyWalking 界面里复数场景不多,但像“共 N 个实例”这类句子,用$tc配合管道符|写起来更省事。

需要说明的是,SkyWalking UI 某些版本分支上用的是 Vue 2 + Element UI,另一些新版本走的是 Vue 3 + Element Plus 的路线。两者的 i18n 库和组件库 locale 引入路径都不一样,动手前先看本地package.json确认版本,别照着旧文章抄代码,这是我踩过的第一个坑。

2. 语言包的结构设计与注册链路

语言文件长什么样,决定了后面好不好维护。我见过不少项目的语言文件是几百行扁平 key 堆在一起,a1、b2这种命名都有,改一处文案要在几千行里翻半天。SkyWalking UI 的键结构相对规整,理解它的划分逻辑,新增语种时才能保持结构一致。

2.1 从 en.ts 出发看键的三层划分

以本地这份代码为例,语言文件默认导出一个大对象,顶层大致分成三块:

export default { // 第一层:通用词汇,跨页面复用 common: { auto: 'Auto', operation: 'Operation', search: 'Search', cancel: 'Cancel', confirm: 'Confirm', noData: 'No Data', }, // 第二层:菜单与导航 menu: { dashboard: 'Dashboard', topology: 'Topology', trace: 'Trace', alarm: 'Alarm', settings: 'Settings', }, // 第三层:页面级命名空间 service: { title: 'Services', instance: 'Instance', endpoint: 'Endpoint', cpm: 'CPM', }, };

这个三层划分的意义在于:common是复用池,任何页面都能取;menu跟路由配置强绑定,改 key 要同步改路由;页面级命名空间则是隔离区,页面下线时整块删掉,不会误伤别处。

命名上有几个约定值得坚持:一律小驼峰,不用下划线和短横线;key 要表达语义,title、placeholder、emptyTip这种后缀比text1、text2强一百倍;避免用缩写,svc、ep这种过半年自己都想不起来。

我在维护语言包时还有一个习惯:key 的顺序保持和英文文件完全一致。翻译时对着英文文件一行行往下推,新增 key 也插在对应位置,这样 diff 出来的改动是干净的,review 时一眼就能看出谁多加了谁漏了。

2.2 从文件到$t的注册链路

理解注册链路,是为了出问题时知道该在哪一层排查。整个链路大概是:

语言文件 (assets/lang/en.ts) ↓ import i18n 实例 (i18n/index.ts) ↓ Vue.use(VueI18n) + new VueI18n({ messages }) 根实例注入 (new Vue({ i18n })) ↓ 组件内 this.$t('menu.dashboard')

对应的核心代码:

import Vue from 'vue'; import VueI18n from 'vue-i18n'; import en from '@/assets/lang/en'; import zhCN from '@/assets/lang/zh-CN'; Vue.use(VueI18n); const i18n = new VueI18n({ locale: 'en', fallbackLocale: 'en', silentTranslationWarn: true, // 生产环境关掉缺 key 的噪音告警 messages: { en, 'zh-CN': zhCN, }, }); export default i18n;

fallbackLocale这一行千万别省。我试过把某个语种的 key 漏了一大片,因为没设回退,界面上直接显示service.labels.cpm这种裸字符串,看着比不翻译还难受。设了回退之后,漏翻的地方退化成英文,用户至少能看懂,也不会显得界面崩了。

silentTranslationWarn则是一个取舍:开发阶段建议关掉,让缺 key 的告警刷屏,逼着自己补齐;提测前再打开,避免控制台噪音干扰排查其它问题。

3. 新增一门语言的完整实操

理论讲完,下面是我实际加一门语言时走的完整流程。整个动作拆成六步,每步都有容易翻车的地方。

3.1 第一步到第三步:复制、翻译、注册

第一步,复制模板。直接从en.ts复制成目标语言文件,比如ja.ts。注意文件名要和语言代码一致,后缀是.ts不是.js,因为项目里语言文件是 TS 模块导出的。

cp src/assets/lang/en.ts src/assets/lang/ja.ts

第二步,逐行翻译 value,key 一个字都不能动。这一步是最耗时的,也是最容易出错的。翻译时的优先级是:菜单和导航 > 按钮和表单 > 表格表头 > 提示和错误信息 > 帮助说明文字。前三类能覆盖 80% 的视觉面积,先把它们做完,界面看起来就“像个日语界面”了。

第三步,注册到 messages。

import ja from '@/assets/lang/ja'; const i18n = new VueI18n({ locale: detectLocale(), fallbackLocale: 'en', messages: { en, 'zh-CN': zhCN, ja }, });

语言代码必须遵循 BCP 47 规范,中文是zh-CN不是zh_CN,带地区的语种写成pt-BR、es-419这种形式。locale匹配是精确字符串比对,后缀写成下划线会导致匹配失败、静默回退到英文,而且不报错,非常隐蔽。

3.2 第四步:语言切换器加选项并持久化

SkyWalking UI 的右上角通常有个语言下拉,选项列表往往也是硬编码的数组。新增语种时必须同步加进去:

export const LANGUAGES = [ { label: 'English', value: 'en' }, { label: '简体中文', value: 'zh-CN' }, { label: '日本語', value: 'ja' }, ];

注意 label 用的是各语种的自称,不是当前界面语言的翻译。日语选项在任何界面语言下都写「日本語」,中文选项写「简体中文」,这样用户永远能找到自己的语言。

切换动作本身很简单,但有两件事必须一起做:

changeLang(lang: string) { this.$i18n.locale = lang; localStorage.setItem('skywalking-locale', lang); document.documentElement.setAttribute('lang', lang); ElementLocale.use(elementLocaleMap[lang]); }

localStorage是持久化偏好,否则用户刷新一次就退回默认语言,体验很差。写入html的lang属性看起来可有可无,实际上影响屏幕阅读器的发音、浏览器自动翻译的触发,以及 CSS 里:lang()选择器的匹配,是个小成本高收益的动作。

初次进入时的语言探测建议按“本地存储 > 浏览器语言 > 默认英文”的顺序:

function detectLocale(): string { const saved = localStorage.getItem('skywalking-locale'); if (saved && SUPPORTED_LOCALES.includes(saved)) return saved; const nav = (navigator.language || 'en').toLowerCase(); if (nav.startsWith('zh')) return 'zh-CN'; if (nav.startsWith('ja')) return 'ja'; return 'en'; }

这里有个细节:navigator.language可能是zh-TW、zh-HK,直接拿它当 locale 用会匹配不上zh-CN,所以要用前缀判断做一次归并。我一开始偷懒直接赋值,结果繁体环境的用户看到的还是英文,查了半天才反应过来。

3.3 第五步:组件库和日期库的联动切换

这是最容易漏的一步,也是我在实际项目里被反馈最多的问题。语言切换之后,分页器还是“10/page”,日期选择器还是英文月份,表格空数据还是“No Data”。原因是 Element UI 有自己独立的 locale 体系,和你自己的 i18n 完全不联动。

Element UI 的官方做法是:

import ElementLocale from 'element-ui/lib/locale'; import elementEn from 'element-ui/lib/locale/lang/en'; import elementZhCN from 'element-ui/lib/locale/lang/zh-CN'; import elementJa from 'element-ui/lib/locale/lang/ja'; export const elementLocaleMap = { en: elementEn, 'zh-CN': elementZhCN, ja: elementJa, }; // 初始化 ElementLocale.use(elementLocaleMap[currentLocale]);

注意ElementLocale.use()可以在运行时调用,不需要重启应用,切换语言时调一次就能让分页器、日期面板、上传组件的文案立刻跟上。

如果项目用的是 Element Plus,路径变成element-plus/es/locale/lang/zh-cn,而且导出形式是import zhCn from 'element-plus/es/locale/lang/zh-cn',use()的调用方式也改成了app.use(ElementPlus, { locale })或配合 ConfigProvider 动态切换。版本不同写法差异很大,抄代码之前先确认自己用的是哪一套。

日期库同理。项目里如果用 dayjs 做时间轴格式化,要同步dayjs.locale('zh-cn');用 moment 就是moment.locale('zh-cn')。这一块不处理,时间轴上的“Jan”“Feb”会一直杵在中文界面里。

提醒一句:Element UI 的 locale 包只覆盖组件内置文案,你在<el-button>搜索</el-button>里硬编码的中文它是不会管的。这类硬编码必须逐个换成$t,否则就会出现“组件库切了、自己的文案没切”的错位。我一般用正则扫一遍模板,把>纯中文<的写法全部打出来人工过一遍。

3.4 第六步:本地跑通再提 PR

做完上面五步,跑一遍npm run serve,重点验证这几个场景:

验证场景期望结果常见异常
首次访问、无本地存储按浏览器语言选中始终英文
切换后刷新页面保持上次选择退回英文
切换后打开分页器组件库文案跟随仍是英文
切换后打开日期选择器月份名跟随仍是英文
打开任意弹窗、抽屉内部文案全部翻译局部残留英文
打开浏览器控制台无缺 key 告警出现Cannot translate

这个表我每次新增语种都会过一遍,能挡掉八成低级的遗漏。

4. 键一致性和翻译质量的自动化校验

人工对几百个 key 是低效且不可靠的。语言文件本质是结构化数据,完全可以用脚本把一致性检查自动化,这一步做完能省掉大量 review 时的来回。

4.1 写一个 key diff 脚本

核心思路:把语言对象递归拍平成a.b.c形式的路径集合,然后和基准语言(英文)做差集。

// scripts/check-i18n.js require('esbuild-register/dist/node').register(); const fs = require('fs'); const path = require('path'); const LANG_DIR = path.resolve(__dirname, '../src/assets/lang'); const BASE_LOCALE = 'en'; function flatten(obj, prefix = '') { const result = {}; for (const key of Object.keys(obj)) { const value = obj[key]; const fullKey = prefix ? `${prefix}.${key}` : key; if (value && typeof value === 'object' && !Array.isArray(value)) { Object.assign(result, flatten(value, fullKey)); } else { result[fullKey] = value; } } return result; } const base = flatten(require(path.join(LANG_DIR, `${BASE_LOCALE}.ts`)).default); const files = fs.readdirSync(LANG_DIR).filter( (f) => f.endsWith('.ts') && f !== `${BASE_LOCALE}.ts` ); let failed = false; for (const file of files) { const locale = path.basename(file, '.ts'); const target = flatten(require(path.join(LANG_DIR, file)).default); const missing = Object.keys(base).filter((k) => !(k in target)); const extra = Object.keys(target).filter((k) => !(k in base)); const empty = Object.keys(target).filter( (k) => typeof target[k] === 'string' && target[k].trim() === '' ); if (missing.length || extra.length || empty.length) { failed = true; console.error(`\n[${locale}] missing=${missing.length} extra=${extra.length} empty=${empty.length}`); missing.slice(0, 10).forEach((k) => console.error(` - missing: ${k}`)); extra.slice(0, 10).forEach((k) => console.error(` + extra: ${k}`)); } else { console.log(`[${locale}] OK, ${Object.keys(target).length} keys`); } } process.exit(failed ? 1 : 0);

esbuild-register的作用是让 Node 能直接requireTS 文件。如果你不想引入额外依赖,也可以用ts-node注册,或者干脆在构建前先tsc编译语言目录再做比对。三种方式我都试过,esbuild-register启动最快,适合挂在pre-commit上。

配套的package.json脚本:

{ "scripts": { "check:i18n": "node scripts/check-i18n.js" } }

在 CI 里跑一次,缺 key 直接让流水线红掉,比人工肉眼扫靠谱得多。extra那一项同样重要,英文文件里已经删掉的 key 如果残留在其它语种里,会让后面维护的人以为它还有用。

4.2 占位符一致性必须单独校验

key 对齐了不代表内容对。这是另一个隐蔽的坑:英文写的是Showing {count} results,翻译时把{count}弄丢了,运行时这句就变成“共条结果”,而且不会报错。

const placeholderOf = (str) => (String(str).match(/\{[a-zA-Z0-9_]+\}/g) || []).sort().join(','); for (const key of Object.keys(base)) { const expect = placeholderOf(base[key]); const actual = placeholderOf(target[key]); if (expect !== actual) { console.error(`[placeholder mismatch] ${key}: "${expect}" -> "${actual}"`); failed = true; } }

注意排序一下再比,{a}{b}和{b}{a}顺序不同但含义一致,不该报错。另外 vue-i18n 里还有@:key这种引用语法和|复数分隔符,如果文案里用到了,同时也要做占位符检查,别让译者把管道符当成普通竖线删掉。

4.3 术语表比逐句翻译更重要

界面国际化最难的不是“翻得准”,而是“翻得一致”。同一个Instance,一会儿译成“实例”,一会儿译成“节点”,用户会以为是两个不同的东西。所以开工前先定一张术语表,所有语种都按这张表走:

英文原文建议译法备注
Service服务不要译成“服务端”
Service Instance服务实例与“服务”区分
Endpoint端点也可用“接口”,全库必须统一
Trace追踪链路追踪语境
Span跨度首次出现可加注原文
Trace Segment追踪段
Topology拓扑顶部菜单用“拓扑图”更直观
Metrics指标不要与“度量”混用
Dashboard仪表盘不用“看板”
Alarm告警不用“警报”“报警”
Heatmap热力图
Profiling性能剖析
OAP / CPM / SLA / P99保持原文专有名词不译

这张表也是我 review 时的依据。有人觉得术语表是形式主义,实际上它把“翻译”从主观判断变成了机械比对,审校效率至少翻倍。特别是多人协作时,没有这张表,同一个文件里出现三种译法几乎是必然的。

另外还有几个细节约定值得写进规范里:中文文案里不要中英文混排时加空格(除非是专有名词与中文之间);标点一律用目标语言的全角符号;句子结尾不加句号,短语保持简洁;按钮文案控制在 6 个字以内,超过会挤爆布局。

5. 常见问题与排查实录

做了几轮之后,我把遇到过的坑整理成了一份速查表,新同事上手时基本能靠它自查。

5.1 切换语言后部分文案不生效

这是出现频率最高的反馈。排查顺序建议固定下来:

现象大概率原因处理方式
整块区域没变化该区域文案是硬编码,没接$t全局搜索中英文关键词定位
只有组件库部分没变Element 的 locale 没切换检查ElementLocale.use()是否被调用
单独几个 key 显示裸字符串该 key 在目标语种缺失跑check:i18n脚本
切换后刷新又变回去偏好没写入 localStorage检查切换函数的写入逻辑
首屏是目标语言,路由跳转后变回英文路由懒加载的组件提前缓存了$t结果不要在 data 里存this.$t(...)的返回值

最后一条特别值得展开。有些老代码习惯在data()里初始化表头数组:

data() { return { columns: [ { label: this.$t('service.name'), prop: 'name' }, ], }; }

data()只执行一次,$t的结果被固化在内存里,i18n.locale改了也不会重算。正确做法是把 label 存成 key,在模板里实时$t:

data() { return { columns: [{ labelKey: 'service.name', prop: 'name' }], }; }
<el-table-column v-for="col in columns" :key="col.prop" :label="$t(col.labelKey)" :prop="col.prop" />

这类问题靠肉眼很难发现,因为它在切换时“大部分是对的”,只有个别表头不对劲。我一般的排查手段是:切到目标语言后,在控制台执行document.body.innerText抓取整页文本,再用正则匹配纯 ASCII 的连续单词,把疑似残留英文的位置打印出来,逐一核对。

5.2 长文案引发的布局问题

不同语种的长度差异远超想象。同一句话,中文字符数通常是最短的,德语、俄语往往要长出 30% 到 50%。按钮、标签页、表格列头这些固定宽度的地方,翻译完直接溢出或者被省略号截断。

几个实用的处理方式:

  • 按钮容器不要写死width,改用min-width+padding,让内容自己撑开。
  • 表格列头开启show-overflow-tooltip,让长文案可以被悬停查看。
  • 顶栏菜单项加white-space: nowrap,避免换行导致高度跳动。
  • 关键操作按钮在目标语种下单独截图确认,别只看默认语言。

我吃过一次亏:某个语言下“确认删除”这一串文字把对话框底部的按钮挤出容器,用户点不到取消按钮。这种问题在英文界面下完全无法复现,只能靠在每个语种下走一遍关键路径。

5.3 构建、缓存和体积方面的三个经验

语言包会被全量打进 bundle。如果只加了中英两种,影响可以忽略;但如果语种到五六个,主包体积会明显变大。这时候可以考虑把语言文件改成动态import(),按需加载:

async function loadLocale(lang: string) { const module = await import(`@/assets/lang/${lang}.ts`); i18n.setLocaleMessage(lang, module.default); i18n.locale = lang; }

注意动态 import 的路径里带变量时,打包工具会生成一个上下文模块把所有匹配文件都打进去,不会真正减包。要真正按需加载,得显式写成映射表的形式,让打包工具能静态分析出依赖边界。

浏览器缓存会骗人。改完语言文件后如果发现界面没变化,先确认不是旧 JS 缓存。开发阶段用硬刷新加禁用缓存,或者直接重启 dev server,别在这上面浪费半小时。

后端返回的枚举要建映射表。前面提过,接口返回的ALARM、NORMAL这类值是英文枚举,不能直接展示。做法是建一张{ value: 'ALARM', i18nKey: 'status.alarm' }的映射表,模板里统一走$t(映射后的key)。当接口新增枚举而映射表没跟上时,加一层兜底:查不到就原样展示,至少不会显示空白。

6. 我个人在长期维护语言包时的几个习惯

语言包这东西的特点是“一次做完,长期维护”。新增一个语种可能就两三天,但接下来每次功能迭代都要同步更新所有语种,这才是真正的成本所在。所以我在实际操作中养成了几个习惯。

第一,每次改功能先改英文文件,英文是基准,其它语种靠check:i18n脚本兜着。CI 里挂了这条检查之后,漏翻基本不会带到主干。

第二,新增 key 时顺手写上简短的上下文注释。比如alarm.duration: 'Duration'这种词,脱离上下文根本不知道是“告警持续时长”还是“告警规则窗口”,翻译人员只能猜。加一行注释就能避免误译,成本极低。

第三,不要追求 100% 覆盖再发版。小语种允许阶段性缺失,靠fallbackLocale退化成英文,先让主路径可见。等主要页面翻完了再逐步补全提示类、帮助类的长文案,比憋一个大 PR 效率高得多。

第四,验收必须真机截图。我在本地开三个不同语言的窗口并排对比过关键页面,中文、英文、日文各一份,截图存档。后面谁改坏了样式,一看截图就知道差在哪。这个小动作省下了无数“在我这儿是好的”的扯皮。

最后分享一个排查小技巧:如果怀疑某个文案没接$t,可以在控制台临时把$t蒙一层,让所有翻译结果都加上标记,比如统一前缀__。刷新之后,页面上带标记的是走 i18n 的,不带的就是硬编码的,一眼分辨,比全局搜索快得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/2 4:03:32

Kadane算法解析:高效解决最大子数组和问题

1. 最大子数组和问题解析最大子数组和问题&#xff08;Maximum Subarray Problem&#xff09;是算法领域的一个经典问题&#xff0c;给定一个整数数组&#xff0c;我们需要找到一个连续子数组&#xff0c;使得该子数组的和是所有可能子数组中最大的。这个问题看似简单&#xff…

作者头像 李华
网站建设 2026/11/2 4:03:25

C语言二级试题:一张定位编程思维盲区的体检图

简介&#xff1a;本资源是一份面向全国计算机等级考试二级C语言科目的高质量备考资料&#xff0c;专为考生系统梳理核心知识点、突破选择题与填空题难点而设计。内容覆盖C语言特点、程序结构、函数机制、数据类型、标识符规则、运算符特性、算法基础等高频考点&#xff0c;每道…

作者头像 李华
网站建设 2026/11/2 4:03:05

现代软件测试体系:从金字塔到持续集成实践

1. 项目概述"测试文章3.7"这个标题看似简单&#xff0c;实则蕴含着一个专业测试人员多年积累的实战经验。作为从业十年的测试工程师&#xff0c;我深知版本号背后往往代表着重要的技术迭代和知识沉淀。3.7这个版本号暗示着至少经历了37次以上的重要更新&#xff0c;每…

作者头像 李华
网站建设 2026/11/2 4:03:00

GPT 5.4 + Image 1.5 像素角色动画生成实战:从提示词到精灵图集

像素风游戏角色动画一直是独立游戏开发里最耗时间、也最考验美术功底的环节之一。传统流程里&#xff0c;一个能跑能跳能攻击的角色&#xff0c;从概念草图到逐帧动画&#xff0c;熟练的美术也得花上两三天&#xff0c;更别提还要保证每一帧的像素对齐、调色板统一、动作连贯。…

作者头像 李华
网站建设 2026/11/2 4:02:42

别被“AI写作工具榜单”带偏:论文开题到定稿的误区清单与选购思路

很多本科生、研究生第一次写毕业论文&#xff0c;最容易做的事就是搜一张“主流 AI 写作工具榜单”&#xff0c;然后照着排名挨个试。但论文不是一篇普通文章&#xff0c;你最终要交付的是选题、开题报告/任务书、文献综述、研究设计、正文、参考文献、修改稿这一整套东西。不同…

作者头像 李华
网站建设 2026/11/2 4:02:41

软件工程系统框架设计:三层架构落地与高内聚低耦合实践

简介&#xff1a;本资源是一份面向软件工程专业学生、初级开发工程师及系统架构入门学习者的系统化教学课件&#xff0c;聚焦软件工程核心方法论与系统框架/结构设计实践&#xff0c;解决学习者对SDLC模型选择、架构原则落地、模块化分层设计及SOA/微服务演进路径理解模糊的问题…

作者头像 李华