Nuxt3不使用三方i18n插件手搓国际化最佳实践

VueNuxt前端插件

2025年03月13日 15:54:48141

前言:对于国际化应用来讲,我更偏向自己手搓实现,而不是依赖别人封装好的插件,这样能才最大限度的精简代码。

功能实现:

  1. 字典数据抽离在后端,通过指定的key同步/异步获取

  2. 支持保留当前页面数据热切换语言

  3. 支持路由别名

//cdn.shiniest.cn/static/2025/Snipaste_2025-03-13_16-36-56.png

实现思路

1. 多语言字典数据抽离

传统的国际化方案,会把多语言字典放置在前端项目代码内,对于一个大型项目,假设有10种语言,每种语言1万条译文,那么这个字典包的大小可能达到好几兆甚至更大,这意味着每次访问页面会更慢,并且会消耗更多的流量。

我们可以以页面或者组件为单位,把多语言分数据分成一个个集合,并保存在数据库,每个集合有一个唯一的key,前端可以通过这个key加上语言标识(locale)去拉取对应的多语言集合,至于怎么在后端维护这些多语言集合,这里就不做过多阐述了,总之千人千面,方案有很多。

2. 支持保留当前页面的数据热切换语言

这个很好理解,我切换语言的时候不希望刷新页面,只需要显示正确的语言就行了。因为Nuxt3是基于Vue3的,它能很轻松的实现响应式数据更新,配合Nuxt的全局状态管理,可以很简单的实现这个功能。

3. 支持路由别名

这样可以更直观的展示当前的语言,也对SEO更加友好,VueRouter有加别名的功能,但我打算使用Nuxt3的hooks在构建阶段实现。

直接上代码

创建一个全局状态用于控制当前的语言环境

export const useLocale = () => useState<TranslationLocales>('useLocale');

这样在切换语言的时候,Nuxt就能及时得到响应。

封装一个方法用于在页面/组件内初始化i18n

//useMyI18n.ts
import { getUserLanguage } from "~/api";

export type TranslationLocales = 'zh_CN' | 'en_US' | 'th_TH' | 'vi_VN' | 'ar_AR' | 'id_ID';
export interface TranslationLocaleItem {
  [Key: string]: string
}
export type Translations = {
  [Key in TranslationLocales]?: TranslationLocaleItem
}
export type TMethod = (key: string, params?: any[]) => string;

const isDev = process.env.NODE_ENV === 'development';

function log(translations: Translations, locale: TranslationLocales, templateKey: string){
  if(isDev) {
    if(!translations[locale] || !Object.keys(translations[locale]!).length) {
      console.warn('[研发环境警告]: 未找到对应语言的字典\n' + locale + ' - ' + templateKey + '\n该警告只在开发环境触发,上线前务必处理此警告,否则将引起严重生产事故!!!')
      translations[locale] = {};
    } else {
      console.log('[研发环境LOG]: 语言的字典\n' + locale + ' - ' + templateKey + ' 正常')
    }
  } else {
    if(!translations[locale]) throw new Error('未找到对应语言的字典:' + locale + ' - ' + templateKey);
  }
}

/**
 * 获取指定模板、指定语言的翻译数据
 * @param templateKey 模板key
 * @param lang 语言-可选,默认取useLocale().value
 * @returns 多语言翻译方法 t
 */
export default function (templateKey: string, lang?: TranslationLocales): Promise<TMethod> {
  let locale = lang || useLocale().value;
  return new Promise(async (resolve, reject) => {
    try {
      const translations = reactive<Translations>({[locale]: {}});
      const localeStringData = localStorage.getItem(`${templateKey}[${locale}]`);
      // 同步实现
      if(localeStringData) {
        Object.assign(translations, {[locale]: JSON.parse(localeStringData)});
      } else {
        const res = await getUserLanguage({
          templateKey,
          languageCode: locale,
          fieldKey: ''
        });
        Object.assign(translations, res.data.value.data);
        localStorage.setItem(`${templateKey}[${locale}]`, JSON.stringify(translations[locale]));
      }
      log(translations, locale, templateKey);

      // 异步实现
      // if(localeStringData) {
      //   Object.assign(translations, {[locale]: JSON.parse(localeStringData)});
      //   log(translations, locale, templateKey);
      // } else {
      //   getUserLanguage({
      //     templateKey,
      //     languageCode: locale,
      //     fieldKey: ''
      //   }).then(res => {
      //     Object.assign(translations, res.data.value.data);
      //     localStorage.setItem(`${templateKey}[${locale}]`, JSON.stringify(translations[locale]));
      //     log(translations, locale, templateKey);
      //   })
      // }
      /**
       * 翻译方法
       * @param key 文案key
       * @param params 可选:文案参数
       * @returns 翻译后的文案
      */
      function t(key: string, params?: any[]): string {
        let result = isDev ? (translations[locale]![key] || key).replace(/===/g, '') : (translations[locale]![key] || '');
        if(useOriginalI18nKey().value) return templateKey;
        if (params && params.length) {
          return result.replace(/\{\d+\}/g, (value: string) => {
            let index = Number(value.replace(/[^\d]/g, ''));
            return params[index] ?? '';
          })
        } else {
          return result;
        }
      }
      resolve(t);
    } catch (error) {
      reject(error);
    }
  })
}

getUserLanguage 是用来获取多语言字典数据的接口,由后端开发实现。我在这里进行了一点优化,在客户端渲染阶段,如果本地已经缓存了对应的多语言字典,那么会直接使用本地的多语言字典,这样可以减少很多请求。当然,有缓存就需要配套的缓存清理逻辑,我是在一个类似心跳检查的代码块内检查有没有更新的。由于多语言字典是使用reactive创建的响应式对象,所以,当切换语言的时候,只需重新执行一遍初始化多语言的方法就可以了。

路由别名实现

为了更好的理解工作原理,假设en_US不用别名,直接通过原链接访问。

Nuxt的hooks提供了一个叫pages:extend的生命周期钩子,pages:extend 发生在 路由解析之前,这意味着你在这个钩子里操作时,Nuxt 还没有完全解析和生成所有的页面信息。因此,它适用于 初始化 阶段,允许你向路由列表中添加新的页面或修改页面配置。

//nuxt.config.ts
export default defineNuxtConfig({
  hooks: {
    'pages:resolved': (files) => {
      const locales: TranslationLocales[] = ['zh_CN', 'th_TH', 'vi_VN', 'ar_AR', 'id_ID'];
      const extendRouters = [];
      locales.forEach(locale => {
        extendRouters.push(...files.map(page => {
          return {
            ...page,
            name: locale + page.name,
            path: '/' + locale + page.path
          }
        }))
      });
      files.push(...extendRouters);
    }
  }
})

在页面中使用

<script setup lang="ts">
import { showDialog } from 'vant';
onMounted(() => {
  showDialog({
    theme: 'round-button',
    confirmButtonText: t('pqvx_1734'),
    title: t('iCkV_8907'),
    message: t('kjyc_3307')
  })
})
</script>

到这一步其实已经实现的差不多了,但是为了更好的开发体验,可以在项目内使用node开发一些脚本,用于自动化译文替换,比如在开发过程中按照某种规则,进行母语开发,开发完毕后,使用脚本自动化将母语替换为对应的多语言key。

赞 6
收藏
分享

本作品系 原创,作者:你不熟悉的x先生

原文链接:https://shiniest.cn/blog/article/165

文本版权:文本版权归作者所有

转载需著名并注明出处(禁止商业使用)

评论和回复

0/500
    没有更多啦~
    怎么一条数据都没有呢?
简介
Nuxt3不使用第三方插件实现国际化,并且支持路由别名(在路由地址加上语言前缀)和热切换。
目录
推荐阅读

D&D By x先生 With