思路
语言导入逻辑
初始化
在 src / locales / setupI18n 内的根语言文件中 import(`./lang/${locale}.ts`)
import type { App } from 'vue';
import type { I18n, I18nOptions } from 'vue-i18n';
import { createI18n } from 'vue-i18n';
import { loadLocalePool, setHtmlPageLang, setLoadLocalePool } from './helper';
import { localeSetting } from '/@/settings/localeSetting';
import { useLocaleStoreWithOut } from '/@/store/modules/locale';
const { fallback, availableLocales } = localeSetting;
export let i18n: ReturnType<typeof createI18n>;
async function createI18nOptions(): Promise<I18nOptions> {
// 使用pinia中的defineStore创建store
const localeStore = useLocaleStoreWithOut();
// 获取当前语言环境locale
const locale = localeStore.getLocale;
// 导入对应文件的语言包,此文件会导入对应语言下的所有文件。然后提取其中的message
const defaultLocal = await import(`./lang/${locale}.ts`);
const message = defaultLocal.default?.message ?? {};
// 将html文件的lang设置为所选择的语言
setHtmlPageLang(locale);
// 设置loadLocalPool为对应的语言:'zh_CN' | 'en' | 'ru' | 'ja' | 'ko'
setLoadLocalePool((loadLocalePool) => {
loadLocalePool.push(locale);
});
return {
legacy: false,
locale,
fallbackLocale: fallback,
messages: {
[locale]: message,
},
availableLocales: availableLocales,
sync: true, //If you don’t want to inherit locale from global scope, you need to set sync of i18n component option to false.
silentTranslationWarn: true, // true - warning off
missingWarn: false,
silentFallbackWarn: true,
};
}
// setup i18n instance with glob
export async function setupI18n(app: App) {
const options = await createI18nOptions();
i18n = createI18n(options) as I18n;
app.use(i18n);
}
会异步导入 src / locales / lang / ${locale}.ts 文件语言包。
举例说明,当import zh-CN.ts 文件时,最终export的对象中的message为一个对象,包含了自定义的字段和antd库中的字段。zh-CN.ts文件内代码如下:
import { genMessage } from '../helper';
import antdLocale from 'ant-design-vue/es/locale/zh_CN';
// import.meta.glob函数从文件中导入多个模块,匹配到的文件都是懒加载的,通过动态导入实现,并会在构建时分离为独立的chunk
// 如果倾向于直接引入所有的模块(例如依赖于这些模块中的副作用首先被应用),可以传入{eager:true}作为第二个参数
// import.meta.glob返回一个对象 {./zh-CN/sys.ts, ./zh-CN/commom.ts......}
const modules = import.meta.glob('./zh-CN/**/*.ts', { eager: true });
// 输出所有自定义和antd库中的对应语言字段
export default {
message: {
...genMessage(modules as Recordable<Recordable>, 'zh-CN'),
antdLocale,
},
};
此时 src / locales / lang / zh-CN 文件夹下的所有ts文件内容被转化为一个对象输出,因此在使用时直接使用 t('sys.login.signInFormTitle') 进行获取
我们来看一下lang文件夹的文件目录,这样做的好处是更容易管理大型项目的多语言,如果不需要分模块划分,可以直接自己手动导入即可:
|-- lang
|-- en.ts
|-- zh_CN.ts
|-- en
| |-- common.ts
| |-- component.ts
| |-- layout.ts
| |-- sys.ts
| |-- routes
| |-- basic.ts
| |-- dashboard.ts
| |-- demo.ts
|-- zh-CN
|-- common.ts
|-- component.ts # 组件相关
|-- layout.ts # 布局相关
|-- sys.ts # 系统页面相关
|-- antdLocale
| |-- DatePicker.ts
|-- routes # 路由菜单相关
|-- basic.ts
|-- dashboard.ts
|-- demo.ts
具体文件内容转化为对象的过程通过 src / locales / lang / helper.ts 模块实现,代码如下:
import type { LocaleType } from '/#/config';
import { set } from 'lodash-es';
export const loadLocalePool: LocaleType[] = [];
export function setHtmlPageLang(locale: LocaleType) {
document.querySelector('html')?.setAttribute('lang', locale);
}
export function setLoadLocalePool(cb: (loadLocalePool: LocaleType[]) => void) {
cb(loadLocalePool);
}
// 将对应语言文件下的ts的内容转换为对象形式
export function genMessage(langs: Record<string, Record<string, any>>, prefix = 'lang') {
const obj: Recordable = {};
Object.keys(langs).forEach((key) => {
// 各ts文件中的内容,对象形式
const langFileModule = langs[key].default;
// 获取文件名,不要后一个replace效果一样
// let fileName = key.replace(`./${prefix}/`, '').replace(/^\.\//, '');
let fileName = key.replace(`./${prefix}/`, '');
// 去掉.ts后缀
const lastIndex = fileName.lastIndexOf('.');
fileName = fileName.substring(0, lastIndex);
// 获取文件夹名和文件名
const keyList = fileName.split('/');
const moduleName = keyList.shift();
const objKey = keyList.join('.');
// 将文件内容赋给对象
if (moduleName) {
if (objKey) {
// 若存在objKey,则该ts文件是文件夹下的文件,则需要嵌套赋值
// 首先设置obj中的moduleName,如果已经存在,则obj.moduleName=已经存在的obj[moduleName],若不存在,则赋为空对象
// 然后将对应文件夹下的文件内容赋给obj.moduleName.objKey
set(obj, moduleName, obj[moduleName] || {});
set(obj[moduleName], objKey, langFileModule);
} else {
// 若不存在objKey,则该ts文件是第一层文件夹下的文件,直接将langFileModule赋给moduleName即可
set(obj, moduleName, langFileModule || {});
}
}
});
return obj;
}
从 src / locales / lang / ${locale}.ts 文件可以看到,最终export的对象中的message为一个对象,包含了自定义的字段和antd库中的字段。
src / locales 文件夹:封装了有关语言设置的各种定义及操作
|-- locales
|-- helper.ts # setHtmlPagePool / setLoadLocalePool / genMessage
|-- setupI18n.ts # createI18nOptions / setupI18n
|-- useLocale.ts # setI18nLanguage / useLocale / changeLocale / getLocale..
|-- en文件夹 # 各展示变量对应的英文
|-- zh_CN文件夹 # 各展示变量对应的中文
如何进行使用
项目将vue-i18n进行了二次封装。
在需要使用的组件中引入项目自带的 useI18n
注意不要引入 vue-i18n 的 useI18n
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
const title = t('components.modal.title');
useI18n的代码如下,export了二次封装的 t 函数 和 i18n 中的一些methods,所以就可以直接使用 t('components.modal.title') 来获取想要的字段:
import { i18n } from '/@/locales/setupI18n';
type I18nGlobalTranslation = {
(key: string): string;
(key: string, locale: string): string;
(key: string, locale: string, list: unknown[]): string;
(key: string, locale: string, named: Record<string, unknown>): string;
(key: string, list: unknown[]): string;
(key: string, named: Record<string, unknown>): string;
};
type I18nTranslationRestParameters = [string, any];
function getKey(namespace: string | undefined, key: string) {
if (!namespace) {
return key;
}
if (key.startsWith(namespace)) {
return key;
}
return `${namespace}.${key}`;
}
export function useI18n(namespace?: string): {
t: I18nGlobalTranslation;
} {
const normalFn = {
t: (key: string) => {
return getKey(namespace, key);
},
};
if (!i18n) {
return normalFn;
}
// 这里的t为一个函数
// function t(...args) {
// return wrapWithDeps(context => Reflect.apply(coreBase.translate, null, [context, ...args]), () => coreBase.parseTranslateArgs(...args), 'translate', root => Reflect.apply(root.t, root, [...args]), key => key, val => shared.isString(val));
// }
// methods为i18n中的一些属性和方法?
const { t, ...methods } = i18n.global;
const tFn: I18nGlobalTranslation = (key: string, ...arg: any[]) => {
if (!key) return '';
if (!key.includes('.') && !namespace) return key;
return t(getKey(namespace, key), ...(arg as I18nTranslationRestParameters));
};
return {
...methods,
t: tFn,
};
}
// Why write this function?
// Mainly to configure the vscode i18nn ally plugin. This function is only used for routing and menus. Please use useI18n for other places
// 为什么要编写此函数?
// 主要用于配合vscode i18nn ally插件。此功能仅用于路由和菜单。请在其他地方使用useI18n
export const t = (key: string) => key;
如何切换语言
在 src / locales / useLocale.ts 中定义了多个语言设置函数
/**
* Multi-language related operations
*/
import type { LocaleType } from '/#/config';
import { i18n } from './setupI18n';
import { useLocaleStoreWithOut } from '/@/store/modules/locale';
import { unref, computed } from 'vue';
import { loadLocalePool, setHtmlPageLang } from './helper';
interface LangModule {
message: Recordable;
dateLocale: Recordable;
dateLocaleName: string;
}
function setI18nLanguage(locale: LocaleType) {
const localeStore = useLocaleStoreWithOut();
if (i18n.mode === 'legacy') {
i18n.global.locale = locale;
} else {
(i18n.global.locale as any).value = locale;
}
localeStore.setLocaleInfo({ locale });
setHtmlPageLang(locale);
}
export function useLocale() {
const localeStore = useLocaleStoreWithOut();
const getLocale = computed(() => localeStore.getLocale);
const getShowLocalePicker = computed(() => localeStore.getShowPicker);
const getAntdLocale = computed((): any => {
return i18n.global.getLocaleMessage(unref(getLocale))?.antdLocale ?? {};
});
// Switching the language will change the locale of useI18n
// And submit to configuration modification
async function changeLocale(locale: LocaleType) {
const globalI18n = i18n.global;
const currentLocale = unref(globalI18n.locale);
if (currentLocale === locale) {
return locale;
}
if (loadLocalePool.includes(locale)) {
setI18nLanguage(locale);
return locale;
}
const langModule = ((await import(`./lang/${locale}.ts`)) as any).default as LangModule;
if (!langModule) return;
const { message } = langModule;
globalI18n.setLocaleMessage(locale, message);
loadLocalePool.push(locale);
setI18nLanguage(locale);
return locale;
}
return {
getLocale,
getShowLocalePicker,
changeLocale,
getAntdLocale,
};
}
其中 changeLocale 函数就时进行语言切换的,分三种情况:
①所选择的语言和当前语言相同
②所选择的语言和当前语言不同,且在loadLocalePool中存在
③所选择的语言和当前语言不同,且在loadLocalePool中不存在
使用 changeLocale 函数切换语言。
import { useLocale } from '/@/locales/useLocale';
const { changeLocale } = useLocale();
changeLocale('en');
新增
语言文件
在 src/locales/lang/ 增加对应语言的文件即可
新增语言
目前项目自带的语言只有 zh_CN
和 en
两种
如果需要新增,按以下操作即可
- 在 src/locales/lang/ 下新增相应的语言目录及语言文件并引入 引入 ant-design-vue 和 moment 对应的语言包
- 在 types/config.d.ts 内加上预览类型定义
- 在 src/settings/localeSetting.ts 修改语言配置
实际页面中如何进行切换
项目在 src / components / Application / src 下定义了 AppLocalePicker 组件
AppLocalePicker 组件中使用了 antd 库的 DropDown 组件
并且传入了localeList进行展示,给 item 绑定的 click 事件为 handleMenuEvent,在handleMenuEvent 中调用了changeLocale函数实现语言的切换。
<template>
<Dropdown
placement="bottom"
:trigger="['click']"
:dropMenuList="localeList"
:selectedKeys="selectedKeys"
@menu-event="handleMenuEvent"
overlayClassName="app-locale-picker-overlay"
>
<span class="cursor-pointer flex items-center">
<Icon icon="ion:language" />
<span v-if="showText" class="ml-1">{{ getLocaleText }}</span>
</span>
</Dropdown>
</template>
<script lang="ts" setup>
import type { LocaleType } from '/#/config';
import type { DropMenu } from '/@/components/Dropdown';
import { ref, watchEffect, unref, computed } from 'vue';
import { Dropdown } from '/@/components/Dropdown';
import Icon from '@/components/Icon/Icon.vue';
import { useLocale } from '/@/locales/useLocale';
import { localeList } from '/@/settings/localeSetting';
const props = defineProps({
/**
* Whether to display text
*/
showText: { type: Boolean, default: true },
/**
* Whether to refresh the interface when changing
*/
reload: { type: Boolean },
});
const selectedKeys = ref<string[]>([]);
const { changeLocale, getLocale } = useLocale();
const getLocaleText = computed(() => {
const key = selectedKeys.value[0];
if (!key) {
return '';
}
// localeList:DropMenu类型数据
// [{text: '简体中文',event: LOCALE.ZH_CN,},{text: 'English',event: LOCALE.EN_US,}]
return localeList.find((item) => item.event === key)?.text;
});
watchEffect(() => {
selectedKeys.value = [unref(getLocale)];
});
async function toggleLocale(lang: LocaleType | string) {
await changeLocale(lang as LocaleType);
selectedKeys.value = [lang as string];
props.reload && location.reload();
}
function handleMenuEvent(menu: DropMenu) {
if (unref(getLocale) === menu.event) {
return;
}
toggleLocale(menu.event as string);
}
</script>