在公司里的一个项目中,刚开始是没有做国际化相关的配置的,这段时间希望能加上双语言,研究了一下常见的国际化配置和别的项目配置,发现基本都是一个语言分一个.js文件维护,这样做的缺点就是每生成一个字段就要cv到所有语言文件里,每修改一次也要做相同的操作,非常的不方便。
由于这个项目目前只要求了双语言(未来最多也只有额外扩展4种),因此我打算使用数组来储存各个语种,用数组的下标来维护对应的语言,这样需要约定语言对应的下标顺序,不过带来的好处是比各自维护一个文件更方便
export const login = {
username: ['登录','Username','ログイン']
}
为了能少写一些import导入语句,我使用了webpack环境下的一个api (require.context(path, deep, filter)) 来实现自动导入。
const moduleContext = require.context('./modules', false, /\.js|\.ts$/);
将所有文件导入后,需要定义一个对象,用于约定和储存生成后的语言数据
const locale = {
zh: {},
en: {},
jp: {}
};
接下来需要定义一个储存语言顺序的一个数组,对象内已经约定好了顺序,取它的keys就可以
const localeKeys = Object.keys(locale);
现在需要开始对导入的文件进行操作,将他们全部储存到locale对象中
function initLocale() {
moduleContext.keys().forEach((key) => {
// 这里的的key就是导入文件的路径,如'./.xxx.js',可以写一个正则删除掉他们的路径提取出该文件名称
const namespace = getModuleNameSpace(key);
// 在这里的default就是文件内的信息了,语言文件是以对象形式储存的因此类型必须是对象
const { default: context } = moduleContext(key);
if (typeof context === 'object') {
for (let contextKey in context) {
// 语言文件的结构必须为 key => value(string [])
const contextValues: string[] = context[contextKey];
// 语言文件的value和locale里的属性的顺序是对应的 index = 0 = zh
contextValues.forEach((item, index) => {
// 拿到locale中对应的key
const localeItem = locale[localeKeys[index]];
// 以文件名作为名称,创建模块对象 无则先初始化一个空对象
const module = localeItem[namespace]
? localeItem[namespace]
: (localeItem[namespace] = {});
module[contextKey] = item;
});
}
}
});
}
function getModuleNameSpace(path: string) {
const removePathRegExp = /\.\/|\.ts|\.js/g;
return path.replace(removePathRegExp, '');
}
然后调用initLocale(),在此就完成了所有语言文件的转化,就可以在main.js/ts内导入使用了
在vue2中的i18n,没有类型提示是一件很麻烦的事情,字段一长,就需要去文件内cv,因此给他做类型提示是一件很有必要的事情,不过由于vue2-i18n的path参数的类型被写死了,因此我重写了原声明文件内的类型,创建一个声明文件 vue-i18n.d.ts
declare module 'vue/types/vue' {
interface Vue {
// 我的项目内几乎只用了$t,因此只重写了$t
$t: (key: LocaleModules, values?: VueI18n.Values) => VueI18n.TranslateResult;
}
}
这里的LocaleModules 就是之后会有提示的类型了,一开始编写类型时我的想法是在各个模块内额外导出一个类型,然后从一个主文件内一一导入合并
export const login = {
username: ['登录','Username','ログイン']
}
// 手动给它加上对应的文件名称,方便调用
export type LocaleLogin = `login.${keyof typeof login}`
// LocaleLogin = 'login.username'
这样写虽然可以做到类型提示,不过每个文件都要定义一次,有点麻烦,经同事的推荐下,vue-i18n-next下有一个工具类型很符合这个需求,于是我就去cv了过来,瞧瞧这个类型体操,给我看花眼了
export type __ResourcePath<T, Key extends keyof T> = Key extends string
? T[Key] extends Record<string, any>
?
| `${Key}.${__ResourcePath<T[Key], Exclude<keyof T[Key], keyof any[]>> & string}`
| `${Key}.${Exclude<keyof T[Key], keyof any[]> & string}`
: never
: never;
export type _ResourcePath<T> = __ResourcePath<T, keyof T> | keyof T;
export type ResourcePath<T> = _ResourcePath<T> extends string | keyof T ? _ResourcePath<T> : never;
然后在语言文件下创建一个 index.ts 后统一导出
// 之后需要新增哪些模块直接import进来后按照这样放在这里就可以了
export type LocaleModules = ResourcePath<{
login: typeof login;
}>;
回到vue-i18n文件内,将鼠标悬浮在LocaleModules上,就能看见现在已经有以‘文件名.字段名’的类型提示了,一个回车省去cv的力气
到这里自动化导入和类型推导过程基本完成了,但是 还有一个问题,如果我想拼接一些字符串来方便复用呢, 类似于"请输入","成功" 这些通用字段是比较有必要作为一个公共字段来使用的,我使用了根据传进来的数组字段来判断哪些是需要拼接的字段, 再多加一个判断条件为数组类型的参数判断就可以支持自定义字段
const fieldsConnect = <Target extends Record<string, string[]>>(
target: Target,
// 目标对象,也是拼接的数据源
fields: Array<keyof Target | string[]>,
// 需要拼接的字段key, 这里做了类型提示,key均来自target
) => {
const connectList: string[][] = [];
// 首先将需要拼接的字段在target中的数据一一存入新数组中
// 一般来说有了ts提示后这里可以不用判空
fields.forEach((field) => {
// 赋予额外自定义字段功能,不需要一定在Target内定义才能使用
if (Array.isArray(field)){
connectList.push(field)
} else if (target[field] !== undefined){
// 这里需要复制一遍引用的数据
connectList.push([...target[field]])
}
})
const connectHeader = connectList.shift() || []
if (connectHeader.length) {
connectList.forEach((connectItems) => {
// 将数据按照下标的语言顺序一一拼接到头部字符串中
connectItems.forEach((field, index) => {
if (connectHeader[index] !== undefined) {
connectHeader[index] += field;
}
});
});
}
return connectHeader;
};
调用之
fieldsConnect(target, ['PasswordSpace', 'Login'])
如果你的项目内使用了vue-i18n-composable(这是给Vue2使用composition-api插件的use用法)这个库的话,记得也要额外添加类型声明
export type VueI18nT = (key: LocaleModules, values?: VueI18n.Values) => VueI18n.TranslateResult;
declare module 'vue/types/vue' {
interface Vue {
$t: VueI18nT;
}
}
declare module 'vue-i18n-composable/dist' {
// 给useI18n的返回结果中的t方法联合上面的类型
declare function useI18n(): Composer & { t: VueI18nT };
}
在公司里的一个项目中,刚开始是没有做国际化相关的配置的,这段时间希望能加上双语言,研究了一下常见的国际化配置和别的项目配置,发现基本都是一个语言分一个.js文件维护,这样做的缺点就是每生成一个字段就要cv到所有语言文件里,每修改一次也要做相同的操作,非常的不方便。
由于这个项目目前只要求了双语言(未来最多也只有额外扩展4种),因此我打算使用数组来储存各个语种,用数组的下标来维护对应的语言,这样需要约定语言对应的下标顺序,不过带来的好处是比各自维护一个文件更方便
export const login = {
username: ['登录','Username','ログイン']
}
为了能少写一些import导入语句,我使用了webpack环境下的一个api (require.context(path, deep, filter)) 来实现自动导入。
const moduleContext = require.context('./modules', false, /\.js|\.ts$/);
将所有文件导入后,需要定义一个对象,用于约定和储存生成后的语言数据
const locale = {
zh: {},
en: {},
jp: {}
};
接下来需要定义一个储存语言顺序的一个数组,对象内已经约定好了顺序,取它的keys就可以
const localeKeys = Object.keys(locale);
现在需要开始对导入的文件进行操作,将他们全部储存到locale对象中
function initLocale() {
moduleContext.keys().forEach((key) => {
// 这里的的key就是导入文件的路径,如'./.xxx.js',可以写一个正则删除掉他们的路径提取出该文件名称
const namespace = getModuleNameSpace(key);
// 在这里的default就是文件内的信息了,语言文件是以对象形式储存的因此类型必须是对象
const { default: context } = moduleContext(key);
if (typeof context === 'object') {
for (let contextKey in context) {
// 语言文件的结构必须为 key => value(string [])
const contextValues: string[] = context[contextKey];
// 语言文件的value和locale里的属性的顺序是对应的 index = 0 = zh
contextValues.forEach((item, index) => {
// 拿到locale中对应的key
const localeItem = locale[localeKeys[index]];
// 以文件名作为名称,创建模块对象 无则先初始化一个空对象
const module = localeItem[namespace]
? localeItem[namespace]
: (localeItem[namespace] = {});
module[contextKey] = item;
});
}
}
});
}
function getModuleNameSpace(path: string) {
const removePathRegExp = /\.\/|\.ts|\.js/g;
return path.replace(removePathRegExp, '');
}
然后调用initLocale(),在此就完成了所有语言文件的转化,就可以在main.js/ts内导入使用了
在vue2中的i18n,没有类型提示是一件很麻烦的事情,字段一长,就需要去文件内cv,因此给他做类型提示是一件很有必要的事情,不过由于vue2-i18n的path参数的类型被写死了,因此我重写了原声明文件内的类型,创建一个声明文件 vue-i18n.d.ts
declare module 'vue/types/vue' {
interface Vue {
// 我的项目内几乎只用了$t,因此只重写了$t
$t: (key: LocaleModules, values?: VueI18n.Values) => VueI18n.TranslateResult;
}
}
这里的LocaleModules 就是之后会有提示的类型了,一开始编写类型时我的想法是在各个模块内额外导出一个类型,然后从一个主文件内一一导入合并
export const login = {
username: ['登录','Username','ログイン']
}
// 手动给它加上对应的文件名称,方便调用
export type LocaleLogin = `login.${keyof typeof login}`
// LocaleLogin = 'login.username'
这样写虽然可以做到类型提示,不过每个文件都要定义一次,有点麻烦,经同事的推荐下,vue-i18n-next下有一个工具类型很符合这个需求,于是我就去cv了过来,瞧瞧这个类型体操,给我看花眼了
export type __ResourcePath<T, Key extends keyof T> = Key extends string
? T[Key] extends Record<string, any>
?
| `${Key}.${__ResourcePath<T[Key], Exclude<keyof T[Key], keyof any[]>> & string}`
| `${Key}.${Exclude<keyof T[Key], keyof any[]> & string}`
: never
: never;
export type _ResourcePath<T> = __ResourcePath<T, keyof T> | keyof T;
export type ResourcePath<T> = _ResourcePath<T> extends string | keyof T ? _ResourcePath<T> : never;
然后在语言文件下创建一个 index.ts 后统一导出
// 之后需要新增哪些模块直接import进来后按照这样放在这里就可以了
export type LocaleModules = ResourcePath<{
login: typeof login;
}>;
回到vue-i18n文件内,将鼠标悬浮在LocaleModules上,就能看见现在已经有以‘文件名.字段名’的类型提示了,一个回车省去cv的力气
到这里自动化导入和类型推导过程基本完成了,但是 还有一个问题,如果我想拼接一些字符串来方便复用呢, 类似于"请输入","成功" 这些通用字段是比较有必要作为一个公共字段来使用的,我使用了根据传进来的数组字段来判断哪些是需要拼接的字段, 再多加一个判断条件为数组类型的参数判断就可以支持自定义字段
const fieldsConnect = <Target extends Record<string, string[]>>(
target: Target,
// 目标对象,也是拼接的数据源
fields: Array<keyof Target | string[]>,
// 需要拼接的字段key, 这里做了类型提示,key均来自target
) => {
const connectList: string[][] = [];
// 首先将需要拼接的字段在target中的数据一一存入新数组中
// 一般来说有了ts提示后这里可以不用判空
fields.forEach((field) => {
// 赋予额外自定义字段功能,不需要一定在Target内定义才能使用
if (Array.isArray(field)){
connectList.push(field)
} else if (target[field] !== undefined){
// 这里需要复制一遍引用的数据
connectList.push([...target[field]])
}
})
const connectHeader = connectList.shift() || []
if (connectHeader.length) {
connectList.forEach((connectItems) => {
// 将数据按照下标的语言顺序一一拼接到头部字符串中
connectItems.forEach((field, index) => {
if (connectHeader[index] !== undefined) {
connectHeader[index] += field;
}
});
});
}
return connectHeader;
};
调用之
fieldsConnect(target, ['PasswordSpace', 'Login'])
如果你的项目内使用了vue-i18n-composable(这是给Vue2使用composition-api插件的use用法)这个库的话,记得也要额外添加类型声明
export type VueI18nT = (key: LocaleModules, values?: VueI18n.Values) => VueI18n.TranslateResult;
declare module 'vue/types/vue' {
interface Vue {
$t: VueI18nT;
}
}
declare module 'vue-i18n-composable/dist' {
// 给useI18n的返回结果中的t方法联合上面的类型
declare function useI18n(): Composer & { t: VueI18nT };
}