首先,我们项目是通过vben admin搭建的基本框架,然后它内部集成了vue-i18n
等国际化库,我们可以直接使用。但是国际化内容是产品将一些翻译输出到excel发给我,这样我就需要将excel内容一个一个的粘贴复制到项目国际化语言包文件中,这样就很恶心了。
国际化实现以前写过一篇文章,切换国际化语言菜单时,最好刷新一下页面location.load()
然后就想着通过excel解析库,去解析内容并输出到对应的文件中,这样就可以很快完成这种枯燥的功能了。
还有就是当我们使用国际化时,我们的语言包也应该有对应模块的区分,不然卸载一个文件夹下,会很臃肿,所以我们要动态去生成语言包对象。
读取excel内容,快速设置语言包
由于我们的文件是加密的,所以需要先解密。
// 上传文件,获取解密code
const uploadChange = async (e) => {
const file = e.target.files[0];
const fd = new FormData();
fd.append("file", file);
const res = await fetch("解密url", {
method: "post",
body: fd,
headers: {
"Content-Type": "multipart/form-data",
},
}).then((res) => res.json());
// 获取解密code
getData(res.data.code);
};
获取完 code 进行解密,并对文件进行分析处理。
createObjectURL(File)
: 根据File类型对象创建一个url,定位资源。revokeObjectURL(objectURL)
: 释放createObjectURL创建的资源,可以让浏览器知道不需要在保存他的内存。
const getData = (code) => {
fetch(`解密url?id=${code}`, {
method: "get",
responseType: "blob",
})
.then((res) => {
return res.blob();
})
.then((blob) => {
let bl = new Blob([blob], { type: "Application/vnd.ms-excel" });
let fileName = Date.parse(new Date()) + ".xlsx";
var link = document.createElement("a");
link.href = window.URL.createObjectURL(blob);
link.download = fileName;
link.click();
window.URL.revokeObjectURL(link.href);
// 直接传递给xlsx处理
transformData(blob);
});
};
不需要解密我们可以直接获取上传的文件对象,进行解析就可以了。
const uploadChange = async (e) => {
const file = e.target.files[0];
// 直接解析文件对象
transformData(file);
};
数据解析,借助xlsx
库。
- 首先创建一个文件对象,用于读取文件内容。
- 将读取的内容通过
XLSX.read
进行解析。 - 将解析的内容转化为json格式。
- 处理数据输出文件,这里我们生成中英文两个 json 文件,方便我们后续操作。
// xlsx处理数据
function transformData(file) {
// 通过FileReader对象读取文件
const fileReader = new FileReader();
// readAsArrayBuffer之后才会启动onload事件
fileReader.onload = (event) => {
const data = new Uint8Array(event.target.result);
const { SheetNames, Sheets } = XLSX.read(data, { type: "array" });
// 这里只取第一个sheet
const workSheets = Sheets[SheetNames[0]];
const sheetRows = XLSX.utils.sheet_to_json(workSheets);
console.log(sheetRows, "转换的excel");
// 中文
const obj = {};
// 英文
const enObj = {};
sheetRows.forEach((item) => {
// 对英文进行转换,输出属性字段。小驼峰
const en =
(item.en + "").replace(/\s/g, "").slice(0, 1).toLowerCase() +
(item.en + "").replace(/\s/g, "").slice(1);
obj[en] = item.zh;
enObj[en] = item.en;
});
exportRaw(JSON.stringify(obj), "./zh.json");
exportRaw(JSON.stringify(enObj), "./en.json");
// console.log("obj", obj, enObj);
};
// uploadFile是上传文件的文件流
fileReader.readAsArrayBuffer(file);
}
输出并下载文件。
Blob
可以通过给定数据,生成一个文件对象。
// 下载转化后的文件
function exportRaw(data, name) {
let urlObject = window.URL || window.webkitURL || window;
let export_blob = new Blob([data]);
let save_link = document.createElementNS("http://www.w3.org/1999/xhtml", "a");
save_link.href = urlObject.createObjectURL(export_blob);
save_link.download = name;
save_link.click();
}
这样一顿操作下来,解放了我的双手,又可以来两把 QQ Fly Car了。
封装useI18n hook
文件结构
注册i18n
- 获取当前语言状态,放在全局状态管理中,做响应式。
- 通过
import()
动态导入语言包处理文件, 获取语言包语言对象。 - 返回i18n配置
- 最后导出一个注册函数,在
main.ts
文件中经行注册。
import type { App } from 'vue';
import type { I18n, I18nOptions } from 'vue-i18n';
import { createI18n } from 'vue-i18n';
export let i18n: ReturnType<typeof createI18n>;
export const LOCALE: { [key: string]: 'zh_CN' | 'en'} = {
ZH_CN: 'zh_CN',
EN_US: 'en',
};
async function createI18nOptions(): Promise<I18nOptions> {
// TODO: 获取本地缓存的localeLang, 放在全局状态管理中,做响应式
const locale = localStorage.getItem("localeLang") || LOCALE.ZH_CN
// 获取语言包,即语言包处理文件
const defaultLocal = await import(`./lang/${locale}.ts`);
// 获取语言对象
const message = defaultLocal.default?.message ?? {};
// 返回i18n配置
return {
legacy: false,
locale: locale,
fallbackLocale: locale,
messages: {
[locale]: message,
},
availableLocales: [LOCALE.ZH_CN, LOCALE.EN_US],
sync: true,
silentTranslationWarn: true,
missingWarn: false,
silentFallbackWarn: true,
};
}
// 注册
export async function setupI18n(app: App) {
const options = await createI18nOptions();
i18n = createI18n(options) as I18n;
app.use(i18n);
}
处理语言包递归文件
- 使用vite提供的
import.meta.glob
方法去动态加载文件。 - 调用语言包处理函数
getMessage
,即可读取各个语言包模块内容并生成对应的语言对象。
import { genMessage } from '../helper';
// TODO: 其他库语言包 例如antd
// import antdLocale from 'ant-design-vue/es/locale/en_US';
// 获取en文件夹下的所有ts结尾文件,嵌套文件
const modules = import.meta.glob('./en/**/*.ts', { eager: true });
export default {
message: {
// 处理文件,拿出所有嵌套文件语言
...genMessage(modules, 'en'),
// TODO: 其他库语言包 例如antd
// antdLocale,
},
};
import { genMessage } from '../helper';
// TODO: 其他库语言包 例如antd
// import antdLocale from 'ant-design-vue/es/locale/zh_CN';
const modules = import.meta.glob('./zh-CN/**/*.ts', { eager: true });
export default {
message: {
// 处理文件,拿出所有嵌套文件语言
...genMessage(modules, 'zh-CN'),
// TODO: 其他库语言包 例如antd
// antdLocale,
},
};
hook 封装
- 循环传入的语言包文件模块,处理分析路径。
- 通过
lodash.set
方法生成对应路径下的对象。 - 返回处理好的语言对象。
这样我们就可以区分不同模块的语言包,而不是写在一个文件中,看起来很臃肿了。
// helper.ts
import { set } from 'lodash-es';
export function genMessage(langs: any, prefix = 'lang') {
const obj: Record<string, any> = {};
// 循环动态加载的模块路径,并获取内容。
Object.keys(langs).forEach((key) => {
const langFileModule = langs[key].default;
/**
* TODO:
* ./en/common.ts => common.ts
* ./en/routes/b.ts => routes/b.ts
*/
let fileName = key.replace(`./${prefix}/`, '').replace(/^\.\//, '');
const lastIndex = fileName.lastIndexOf('.');
/**
* TODO:
* common.ts => common
* routes/b.ts => routes/b
*/
fileName = fileName.substring(0, lastIndex);
const keyList = fileName.split('/');
// 拿到第一层文件夹名称
/**
* common
* routes
*/
const moduleName = keyList.shift();
// 拿到最后文件名称
const objKey = keyList.join('.');
if (moduleName) {
if (objKey) {
/**
* set(object, path, value)
*
* 设置 object对象中对应 path 属性路径上的值,如果path不存在,则创建。 缺少的索引属性会创建为数组,而缺少的属性会创建为对象。
*
*/
set(obj, moduleName, obj[moduleName] || {});
set(obj[moduleName], objKey, langFileModule);
} else {
set(obj, moduleName, langFileModule || {});
}
}
});
return obj;
}
封装useI18n hook,处理t函数参数边界情况,并调用i18n内部的t函数做语言包取参。
import { i18n } from './index';
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];
// 区分使用hooks时是否传递通用的前缀
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;
} {
// 标准化 t 中的参数
const normalFn = {
t: (key: string) => {
return getKey(namespace, key);
},
};
if (!i18n) {
return normalFn;
}
// 国际化函数就是在i18n.global中的
const { t, ...methods } = i18n.global;
const tFn: I18nGlobalTranslation = (key: string, ...arg: any[]) => {
// 排除不正确的key,因为国际化传参都是通过`[string].[string]`来取值的
if (!key) return '';
if (!key.includes('.') && !namespace) return key;
return t(getKey(namespace, key), ...(arg as I18nTranslationRestParameters));
};
return {
...methods,
t: tFn,
};
}
切换语言更新页面
当切换语言时,我们需要更新三部分内容。
- i18n locale属性
if (i18n.mode === 'legacy') {
i18n.global.locale = locale;
} else {
(i18n.global.locale as any).value = locale;
}
- 更新当前语言包message
globalI18n.setLocaleMessage(locale, message);
- 还有一个html lang属性,有利于SEO和用户体验。
import { useAppStore } from './../store/index';
import { setHtmlPageLang } from "./helper";
import { i18n } from "./index"
import {unref} from "vue"
export type LocaleType = "zh_CN" | "en"
function setI18nLanguage(locale: LocaleType) {
if (i18n.mode === 'legacy') {
i18n.global.locale = locale;
} else {
(i18n.global.locale as any).value = locale;
}
// TODO: 更新全局状态中的语言变量
useAppStore().changeLocaleLang(locale)
// 设置html lang属性
setHtmlPageLang(locale);
}
export function useLocale() {
/**
* 切换语言类型调用
*/
async function changeLocale(locale: LocaleType) {
const globalI18n = i18n.global;
// 获取当前语言类型
const currentLocale = unref(globalI18n.locale);
if (currentLocale === locale) {
return locale;
}
// 拿到当前语言包message对象
const langModule = ((await import(`./lang/${locale}.ts`)) as any).default;
if (!langModule) return;
const { message } = langModule;
// 更新i18n语言包
globalI18n.setLocaleMessage(locale, message);
// 更新locale
setI18nLanguage(locale);
return locale;
}
return {
changeLocale
}
}
还有一个问题就是,当我们在配置文件进行国际化时,切换语言也不会更新页面(不刷新的情况下),我们可以对语言进行监听,让逻辑重新执行一遍。注意配置文件导出时,需要导出一个函数。
/**
*
* @param {...any} cbs 所有的回调
*/
export function watchSwitchLang(...cbs) {
watch(
// TODO: 监听语言变化
() => store.getters.language,
() => {
cbs.forEach((cb) => cb(store.getters.language))
}
)
}
往期文章
- 近三个月的排错,原来的憧憬消失喽
- 带你从0开始了解vue3核心(运行时)
- 带你从0开始了解vue3核心(computed, watch)
- 带你从0开始了解vue3核心(响应式)
- 3w+字的后台管理通用功能解决方案送给你
- 入职之前,狂补技术,4w字的前端技术解决方案送给你(vue3 + vite )
专栏文章
最近也在学习nestjs,有一起小伙伴的@我哦。
已辞职,先玩他个把月,舒服。结束了,一切都结束了。 🎇🎉✨
接上篇总结,近三个月的排错,原来的憧憬消失喽