前端国际化方案

137 阅读4分钟

支持语言

应对业务需求,未来有出海的打算。现前端需实现国际化切换【简体中文、繁体、英文、日文】,目前暂定这几种,依据后续业务需求变化。

功能点

1、默认语言

方案一:根据用户识别(非中国匹配英文),默认国际化设置。

方案二:默认设置为中文。

方案三:获取浏览器设置。

在前端实现国际化时,若需在未请求或手动切换语言时自动设置默认语言,可通过以下步骤实现,结合浏览器语言检测与国际化库配置:


a、获取浏览器语言偏好

通过JavaScript读取浏览器的语言设置,通常从navigator.languagenavigator.languages获取:

const browserLang = navigator.language || navigator.userLanguage; // 获取浏览器首选语言(如 'zh-CN')

此方法依赖浏览器发送的Accept-Language HTTP头,可覆盖用户系统设置

b、配置国际化库默认语言

vue-i18n为例,在初始化时动态设置locale

import { createI18n } from 'vue-i18n';
import en from './locales/en.json';
import zh from './locales/zh.json';

const i18n = createI18n({
  locale: browserLang.split('-')[0], // 取语言部分(如 'zh-CN''zh'fallbackLocale: 'en', // 回退语言
  messages: { en, zh }
});

// 若检测到语言无资源文件,强制回退
if (!i18n.global.availableLocales.includes(i18n.locale)) {
  i18n.global.locale = 'en';
}

此逻辑确保即使浏览器语言配置异常,应用仍能正常运行。

c、动态加载语言资源

通过异步请求加载对应语言文件,避免阻塞页面渲染:

async function loadLocaleMessages(locale) {
  try {
    const messages = await import(`@/locales/${locale}.json`);
    i18n.global.setLocaleMessage(locale, messages.default);
  } catch (error) {
    console.warn(`语言文件加载失败: ${locale}`);
  }
}

// 初始化时加载
loadLocaleMessages(i18n.global.locale);

此方法支持懒加载,优化首屏加载速度。

d、处理多语言混合场景

对于复杂场景(如用户手动修改系统语言),可结合localStorage持久化用户选择:

// 尝试读取用户历史选择
const savedLang = localStorage.getItem('appLanguage');
if (savedLang) {
  i18n.global.locale = savedLang;
  loadLocaleMessages(savedLang);
}

// 监听语言切换事件
i18n.global.onLocaleChanged((newLocale) => {
  localStorage.setItem('appLanguage', newLocale);
});

此方案兼顾用户体验与数据持久化。

e、SEO优化建议

在HTML中添加lang属性,确保搜索引擎正确识别页面语言:

<html :lang="$i18n.locale">
  <!-- 页面内容 -->
</html>

同时使用hreflang标签声明多语言版本。

2、国际化页面配置生效

为可自己到系统中配置国际化,赋权修改【暂不采用】

缺点:配置即生效国际化,样式不可控、需要做好文案的精简控制

优点:不用发版

3、前端定制国际化

目前方案

缺点:每次修改国际化,需要发版

优点:美观、样式兼容性问题少

待确认:登录页选择后,都是一套语言,还是说在内部登录后也能修改,依据业务需求

4、待办

国际化需求需整改几个问题

问题一:图片上带文字的设计不适配,需要给底图,前端写文字的方案才能做国际化,或者说需要给出对应语言的设计图片。

问题二:第三方插件的国际化存在不适配,需要考虑第三方插件适配。

问题三:后端接口返回的下拉数据如何做国际化,前端或后端都可以做映射。

接入国际化前端步骤

引入国际化语言 vue-i18n 方案。

全局设置 i18n 文件夹,区分中文、英文、日文,繁体可拓展其他;页面国际化统一放入 i18n 目录下,每次添加前先查找有无、再新增。

1、安装

npm install vue-i18n

image.png

2、配置

在src目录下,创建 i18n 文件夹,含 lang 和 pages 文件夹及 index.js

image.png

重点解读 index.js 文件配置


import { createI18n } from 'vue-i18n'
import pinia from '@/store/createPinia'
import { storeToRefs } from 'pinia'
import { useThemeConfig } from '@/store/modules/themeConfig'
// import { info } from '@/api/admin/i18n'; // TODO 请求配置来的国际化

// TODO 不使用分包,直接引入本地的国际化
// import enUS from './lang/en'
// import zhCN from './lang/zh-CN'
// import zhTW from './lang/zh-TW'
// import JA from './lang/ja'

/**
 * 说明:
 * 须在 views 下新建文件夹(建议 `要国际化界面目录` 与 `i18n 目录` 相同,方便查找),
 * 注意国际化定义的字段,不要与原有的定义字段相同。
 * 1、/src/i18n/lang 下的 js 为框架的国际化内容
 * 2、/src/i18n/pages 下的 js 为各界面的国际化内容
 */

// element plus 自带国际化
import enLocale from 'element-plus/es/locale/lang/en'
import zhcnLocale from 'element-plus/es/locale/lang/zh-cn'
import zhtwLocale from 'element-plus/es/locale/lang/zh-tw'
import jaLocale from 'element-plus/es/locale/lang/ja'

const element = {
  'en': enLocale,
  'zh-CN': zhcnLocale,
  'zh-TW': zhtwLocale,
  'ja': jaLocale
}

// 定义变量内容
const messages = {}

// 语言包文件分包
const itemize = {
  'en': [], // 英文
  'zh-CN': [], // 简体中文
  'zh-TW': [], // 繁体
  'ja': [] // 日文
}

const modules = import.meta.glob('./**/*.js', { eager: true })
const pages = import.meta.glob('./../../**/**/**/i18n/*.js', { eager: true })

const regexp = /(\S+)/(\S+).js/
// 对自动引入的 modules 进行分类 en、zh-cn、zh-tw
for (const path in modules) {
  const key = path.match(regexp)
  if (itemize[key[2]]) {
    itemize[key[2]].push(modules[path].default)
  } else {
    itemize[key[2]] = modules[path]
  }
}

for (const path in pages) {
  const key = path.match(regexp)
  if (itemize[key[2]]) {
    itemize[key[2]].push(pages[path].default)
  } else {
    itemize[key[2]] = pages[path]
  }
}

// 合并数组对象(非标准数组对象,数组中对象的每项 key、value 都不同)
const mergeArrObj = (list, key) => {
  let obj = {}
  list[key].forEach(item => obj = Object.assign({}, obj, item))
  return obj
}

for (const key in itemize) {
  messages[key] = {
    name: key,
    el: element[key].el,
    ...mergeArrObj(itemize, key)
  }
}

// 读取 pinia 默认语言
const themeConfigStore = useThemeConfig(pinia)
const { themeConfig } = storeToRefs(themeConfigStore)
const locale =  themeConfig.value.globalI18n || 'zh-CN'

// 导出语言国际化
export const i18n = createI18n({
  legacy: false, // 是否使用传统API模式,设为false指定使用Composition API模式(vue3设为false)
  silentTranslationWarn: true, // 为每个组件注入全局属性和函数
  missingWarn: false, // 抑制本地化失败警告
  silentFallbackWarn: true, // 抑制回退失败警告
  fallbackWarn: false, // 回退警告
  locale: locale, // 语言
  fallbackLocale: zhcnLocale.name, // 回退语言
  messages
  // messages: {
  // 	'en': enUS,
  // 	'zh-CN': zhCN,
  // 	'zh-TW': zhTW,
  // 	'ja': JA
  // },// 语言
})

export function useCustomerI18n(app) {
  app.use(i18n)
}

// TODO 获取用户配置的语言包
// await fetchI18n();
// // 远程获取i18n
// async function fetchI18n() {
// 	if (__MOCK_MODE__ != 'mock') {
// 		const infoI18n = await info();
// 		const messageLocal:any = {};
// 		const itemizeLocal = { en: [] as any[], 'zh-cn': [] as any[] };
// 		itemizeLocal['zh-CN'].push(...infoI18n.data.data['zh-CN']);
// 		itemizeLocal.en.push(...infoI18n.data.data.en);
// 		for (const key in itemizeLocal) {
// 			messageLocal[key] = {
// 				name: key,
// 				...mergeArrObj(itemizeLocal, key),
// 			};
// 		}
// 		i18n.global.mergeLocaleMessage('zh-CN', messageLocal['zh-CN']);
// 		i18n.global.mergeLocaleMessage('zh-TW', messageLocal['zh-TW']);
// 		i18n.global.mergeLocaleMessage('en', messageLocal['en']);
// 		i18n.global.mergeLocaleMessage('ja',messageLocal['ja']);
// 		i18n.global.locale.value = themeConfig.value.globalI18n;
// 	}
// }

也可以直接在 views 文件夹下的某个页面中直接配置。

方式为直接创建 i18n 目录,然后新建语言配置文件

image.png

3、使用

在 main.js 中挂载 i18n

image.png

在 App.vue 文件中,增加国际化注入 locale

image.png

4、详细说明

创建的切换国际化语言的组件 image.png

引用组件位置: image.png 使用方式上图可以查看

<template>
  <div class="lang-i18n">
   <el-dropdown placement="top-end">
    <el-icon><Compass /></el-icon>
    <template #dropdown>
      <el-dropdown-menu>
        <el-dropdown-item v-for="item in langOptions" @click="changeLang(item)">{{ item.label }}</el-dropdown-item>
      </el-dropdown-menu>
    </template>
  </el-dropdown>
  {{ langVal }}
  </div>
</template>

<script setup>
import { Compass } from '@element-plus/icons-vue'
import { useI18n } from 'vue-i18n'
import { useThemeConfig } from '@/store/modules/themeConfig'

const { messages, locale } = useI18n()
const useThemeConfigStore = useThemeConfig()

const langVal = ref(locale.value)
const langOptions = ref([
  { key: 'zh-CN', label: '简体中文' },
  { key: 'zh-TW', label: '繁体' },
  { key: 'en', label: '英文 ' },
  { key: 'ja', label: '日文' }
])

const changeLang = (item) => {
  // 设置locale语言
  langVal.value = item.label
  locale.value = item.key
  useThemeConfigStore.SET_LANG(item.key)
}
</script>

<style lang="scss" scoped>
.lang-i18n {
  float: right;
  position: relative;
  top: -2rem;
  right: 2rem;
  height: 3.6875rem;
  line-height: 1rem;
}
</style>

详细使用可参考指南:kazupon.github.io/vue-i18n/zh…

以上网站有说明具体使用及复杂场景。

【提效】扩展插件

vite-auto-i18n-plugin

自动翻译插件。节约前期如果需要人工一条一条去翻译的工作时间。

特点:

  • 一键翻译
  • 无需改动业务代码
  • 支持新增语言自动补全配置
  • 支持翻译多国语言
npm install vite-auto-i18n-plugin --save-dev

配置

vite.config.js

image.png

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vitePluginAutoI18n, { YoudaoTranslator } from 'vite-auto-i18n-plugin'
import path from 'path'

// https://vite.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    vitePluginAutoI18n({
      targetLangList: ['zh-cn', 'zh-TW', 'en', 'ja'],
      translator: new YoudaoTranslator({
        appId: 'xxxxxx', // yangsoso 有道翻译的 appId 和 appKey
        appKey: 'xxxxxx',
      }),
      excludedPath: ['node_modules'], // 排除第三方库
      generate: true, // 生成词条配置文件
      output: true, // 生成翻译文件
    }),
  ],
  resolve: {
    alias: {
      '~': path.resolve(__dirname, './'),
      '@': path.resolve(__dirname, './src'),
    },
    extensions: ['.mjs', '.js', '.mts', '.ts', '.jsx', '.tsx', '.json', '.vue'],
  },
  build: {
    outDir: 'gh-digital-energy-commonui', //输出文件名
    lib: {
      entry: path.resolve(__dirname, './src/components/index.js'), //指定组件编译入口文件
      name: 'index',
      fileName: 'index',
    },
    rollupOptions: {
      // 确保外部化处理那些你不想打包进库的依赖
      external: ['vue'],
      output: {
        // 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
        globals: {
          vue: 'Vue',
        },
      },
    },
  },
})

有道翻译申请的一个appId 和 appKey ai.youdao.com/console/#/a…

翻译器配置(可选)

1. 使用谷歌翻译(默认)
translator: new GoogleTranslator({
  proxyOption: { // 代理配置(可选)
    host: '127.0.0.1',
    port: 8899,
    headers: { 'User-Agent': 'Node' }
  }
})
2. 使用有道翻译(推荐)
translator: new YoudaoTranslator({
  appId: 'your_app_id',
  appKey: 'your_app_key'
})

main.js

image.png

import '../lang/index' // 必须放在首位
import { createApp } from 'vue'
import App from './App.vue'
// 样式文件导入
import './assets/style/index.scss'

import ElementComponent from './components/index.js' //导入各个组件

import { useCustomerI18n } from '@/i18n/index'
import { useElementPlus } from '@/plugins/element-plus'

import './assets/style/global.scss' // 引入全局样式
//
// 渲染页面
function renderApp() {
  const app = createApp(App)
  ElementComponent(app)
  useElementPlus(app)
  useCustomerI18n(app)
  app.mount('#app')
}

renderApp()

配置项

参数类型必选默认值描述
translateKeystring$t插件转换后切换语言的默认函数
excludedCallstring[]["$i8n", "require", "$$i8n", "console.log", "$t"]标记不会翻译的调用函数
excludedPatternRegExp[][/.\w+$/]标记不会翻译的字符串
excludedPathRegExp[][]不翻译指定目录下文件
includePathRegExp[][/src//]翻译指定目录下文件
globalPathstring./lang翻译配置文件生成位置
distPathstring''打包后生成文件的位置 比如 ./dist/assets(用于将翻译配置注入打包文件
distKeystring''打包后生成文件的主文件名称,比如index.xxx 默认是index(用于将翻译配置注入打包文件
namespacestring''线上区分当前项目间的翻译配置
originLangstring'zh-cn'源语言(基于该语言翻译成其他语言)
targetLangListstring[]['en']目标语言(原始语言将被翻译成的语言类型,接受一个数组,支持多种语言)支持语言类型(langFile
buildToDistBooleanfalse是否将翻译配置打包到主包中

解释buildToDist:

在vite环境中执行插件后会生成翻译配置文件。但是如果您直接构建它,项目会先生成翻译配置文件。但翻译配置文件不会立即打包到主包中,您可能需要再次打包。

因此,提供了buildToDist选项,当创建翻译配置文件时,它将主动将翻译配置文件打包进主包,缺陷是您的打包文件可能有两份翻译配置文件

翻译文件生成

  • 插件会自动生成 lang/index.js 和对应语言的 JSON 文件(如 zh-CN.json),需先执行 npm run build 完整构建
  • 检查生成的 JSON 文件是否包含所有翻译键值对,键名应为哈希值(如 "0"),确保相同文本生成相同键

image.png

查看生产的翻译结果,进入lang\index.json,支持重新设置翻译结果,以设置后的为准。 image.png

看起来有些翻译,还是有一些偏差。看项目业务是否需要很专业的翻译,个人觉得需要高精度的翻译,可以利用插件首次给出结构,但是后期再校验一遍,节约前期投入能力,针对想快速给国际化的项目比较友好。

验证翻译

在切换语言的地方切换语言验证。

// 切换语言
const changeLanguage = (lang) => {
  locale.value = lang
  localStorage.setItem('lang', lang) // 保存语言偏好
  window.location.reload()
}

image.png

查看效果,成功。 image.png

deepseek还给出了一些专门的测试插件。可自行安装验证。

推荐测试工具

工具适用场景优势
Cypress端到端测试支持多语言切换自动化验证
BrowserSync实时刷新与多设备同步测试自动刷新页面,提升测试效率
i18n-check翻译文件完整性检查自动检测缺失键值对和重复键

总结

项目业务是否需要很专业的翻译,个人觉得需要高精度的翻译,可以利用插件首次给出结构,但是后期再校验一遍,节约前期投入能力,针对想快速给国际化的项目比较友好。

  1. 首次构建白屏

    • 原因:插件解析代码生成翻译文件时阻塞构建流程。
    • 解决方案:先执行 npm run build 生成完整文件,再启动开发服务器
  2. 翻译键冲突

    • 现象:不同文本生成相同哈希键导致内容错乱。
    • 解决方案:手动修改 JSON 文件中的键名,确保唯一性
  3. 第三方组件翻译缺失

    • 现象:UI 库(如 Element Plus)的文本未被翻译。

    • 解决方案:

      • 在 vite.config.js 中配置 excludedPath: ['node_modules'] 排除第三方库
      • 手动提取组件文本并添加到翻译文件。

该文档仅用于学习分享。不涉及商业用途。