提交 8882d4e7 编写于 作者: V vben

wip: support multilingual configuration

上级 3a651767
// Modified from
// https://github.com/luxueyan/vite-transform-globby-import/blob/master/src/index.ts
// TODO Currently, it is not possible to monitor file addition and deletion. The content has been changed, the cache problem?
import { join } from 'path';
import { lstatSync } from 'fs';
import glob from 'glob';
import { createResolver, Resolver } from 'vite/dist/node/resolver.js';
import { Transform } from 'vite/dist/node/transform.js';
const modulesDir: string = join(process.cwd(), '/node_modules/');
interface SharedConfig {
root?: string;
alias?: Record<string, string>;
resolvers?: Resolver[];
}
function template(template: string) {
return (data: { [x: string]: any }) => {
return template.replace(/#([^#]+)#/g, (_, g1) => data[g1] || g1);
};
}
const globbyTransform = function (config: SharedConfig): Transform {
const resolver = createResolver(
config.root || process.cwd(),
config.resolvers || [],
config.alias || {}
);
const cache = new Map();
const urlMap = new Map();
return {
test({ path }) {
const filePath = path.replace('\u0000', ''); // why some path startsWith '\u0000'?
try {
return (
!filePath.startsWith(modulesDir) &&
/\.(vue|js|jsx|ts|tsx)$/.test(filePath) &&
lstatSync(filePath).isFile()
);
} catch {
return false;
}
},
transform({ code, path, isBuild }) {
let result = cache.get(path);
if (!result) {
const reg = /import\s+([\w\s{}*]+)\s+from\s+(['"])globby(\?path)?!([^'"]+)\2/g;
const match = code.match(reg);
if (!match) return code;
const lastImport = urlMap.get(path);
if (lastImport && match) {
code = code.replace(lastImport, match[0]);
}
result = code.replace(reg, (_, g1, g2, g3, g4) => {
const filePath = path.replace('\u0000', ''); // why some path startsWith '\u0000'?
// resolve path
const resolvedFilePath = g4.startsWith('.')
? resolver.resolveRelativeRequest(filePath, g4)
: { pathname: resolver.requestToFile(g4) };
const files = glob.sync(resolvedFilePath.pathname, { dot: true });
let templateStr = 'import #name# from #file#'; // import default
let name = g1;
const m = g1.match(/\{\s*(\w+)(\s+as\s+(\w+))?\s*\}/); // import module
const m2 = g1.match(/\*\s+as\s+(\w+)/); // import * as all module
if (m) {
templateStr = `import { ${m[1]} as #name# } from #file#`;
name = m[3] || m[1];
} else if (m2) {
templateStr = 'import * as #name# from #file#';
name = m2[1];
}
const temRender = template(templateStr);
const groups: Array<string>[] = [];
const replaceFiles = files.map((f, i) => {
const file = g2 + resolver.fileToRequest(f) + g2;
groups.push([name + i, file]);
return temRender({ name: name + i, file });
});
urlMap.set(path, replaceFiles.join('\n'));
return (
replaceFiles.join('\n') +
(g3 ? '\n' + groups.map((v) => `${v[0]}._path = ${v[1]}`).join('\n') : '') +
`\nconst ${name} = { ${groups.map((v) => v[0]).join(',')} }\n`
);
});
if (isBuild) cache.set(path, result);
}
return result;
},
};
};
export default globbyTransform;
// Used to import all files under `src/views`
// The built-in dynamic import of vite cannot meet the needs of importing all files under views
// Special usage ,Only for this project
import glob from 'glob';
import { Transform } from 'vite/dist/node/transform.js';
......@@ -28,7 +27,6 @@ const dynamicImportTransform = function (env: any = {}): Transform {
return code;
}
// if (!isBuild) return code;
// Only convert the dir
try {
const files = glob.sync('src/views/**/**.{vue,tsx}', { cwd: process.cwd() });
......
// Modified from
// https://github.com/luxueyan/vite-transform-globby-import/blob/master/src/index.ts
// TODO Deleting files requires re-running the project
import { join } from 'path';
import { lstatSync } from 'fs';
import glob from 'glob';
import globrex from 'globrex';
import dotProp from 'dot-prop';
import { createResolver, Resolver } from 'vite/dist/node/resolver.js';
import { Transform } from 'vite/dist/node/transform.js';
const modulesDir: string = join(process.cwd(), '/node_modules/');
interface SharedConfig {
root?: string;
alias?: Record<string, string>;
resolvers?: Resolver[];
includes?: string[];
}
function template(template: string) {
return (data: { [x: string]: any }) => {
return template.replace(/#([^#]+)#/g, (_, g1) => data[g1] || g1);
};
}
// TODO support hmr
function hmr(isBuild = false) {
if (isBuild) return '';
return `
if (import.meta.hot) {
import.meta.hot.accept();
}`;
}
// handle includes
function fileInclude(includes: string | string[] | undefined, filePath: string) {
return !includes || !Array.isArray(includes)
? true
: includes.some((item) => filePath.startsWith(item));
}
// Bare exporter
function compareString(modify: any, data: string[][]) {
return modify ? '\n' + data.map((v) => `${v[0]}._path = ${v[1]}`).join('\n') : '';
}
function varTemplate(data: string[][], name: string) {
//prepare deep data (for locales)
let deepData: Record<string, object | string> = {};
let hasDeepData = false;
//data modify
data.map((v) => {
//check for has deep data
if (v[0].includes('/')) {
hasDeepData = true;
}
// lastKey is a data
let pathValue = v[0].replace(/\//g, '.').split('.');
let lastKey: string | undefined = pathValue.pop();
let deepValue: Record<any, any> = {};
if (lastKey) {
deepValue[lastKey.replace('_' + pathValue[0], '')] = lastKey;
}
// Set Deep Value
deepValue = Object.assign(deepValue, dotProp.get(deepData, pathValue.join('.')));
dotProp.set(deepData, pathValue.join('.'), deepValue);
});
if (hasDeepData) {
return `const ${name} = ` + JSON.stringify(deepData).replace(/\"|\'/g, '');
}
return `const ${name} = { ${data.map((v) => v[0]).join(',')} }`;
}
const globTransform = function (config: SharedConfig): Transform {
const resolver = createResolver(
config.root || process.cwd(),
config.resolvers || [],
config.alias || {}
);
const { includes } = config;
const cache = new Map();
const urlMap = new Map();
return {
test({ path }) {
const filePath = path.replace('\u0000', ''); // why some path startsWith '\u0000'?
try {
return (
!filePath.startsWith(modulesDir) &&
/\.(vue|js|jsx|ts|tsx)$/.test(filePath) &&
fileInclude(includes, filePath) &&
lstatSync(filePath).isFile()
);
} catch {
return false;
}
},
transform({ code, path, isBuild }) {
let result = cache.get(path);
if (!result) {
const reg = /import\s+([\w\s{}*]+)\s+from\s+(['"])globby(\?locale)?(\?path)?!([^'"]+)\2/g;
const match = code.match(reg);
if (!match) return code;
const lastImport = urlMap.get(path);
if (lastImport && match) {
code = code.replace(lastImport, match[0]);
}
result = code.replace(
reg,
(
_,
// variable to export
exportName,
// bare export or not
bareExporter,
// is locale import
isLocale,
// inject _path attr
injectPath,
// path export
globPath
) => {
const filePath = path.replace('\u0000', ''); // why some path startsWith '\u0000'?
// resolve path
const resolvedFilePath = globPath.startsWith('.')
? resolver.resolveRelativeRequest(filePath, globPath)
: { pathname: resolver.requestToFile(globPath) };
const files = glob.sync(resolvedFilePath.pathname, { dot: true });
let templateStr = 'import #name# from #file#'; // import default
let name = exportName;
const m = exportName.match(/\{\s*(\w+)(\s+as\s+(\w+))?\s*\}/); // import module
const m2 = exportName.match(/\*\s+as\s+(\w+)/); // import * as all module
if (m) {
templateStr = `import { ${m[1]} as #name# } from #file#`;
name = m[3] || m[1];
} else if (m2) {
templateStr = 'import * as #name# from #file#';
name = m2[1];
}
const templateRender = template(templateStr);
const groups: Array<string>[] = [];
const replaceFiles = files.map((f, i) => {
const fileNameWithAlias = resolver.fileToRequest(f);
const file = bareExporter + fileNameWithAlias + bareExporter;
if (isLocale) {
const globrexRes = globrex(globPath, { extended: true, globstar: true });
// Get segments for files like an en/system ch/modules for:
// ['en', 'system'] ['ch', 'modules']
const matchedGroups = globrexRes.regex.exec(fileNameWithAlias);
if (matchedGroups && matchedGroups.length) {
const matchedSegments = matchedGroups[1]; //first everytime "Full Match"
const name = matchedGroups[2] + '_' + matchedSegments.split('/').shift();
//send deep way like an (en/modules/system/dashboard) into groups
groups.push([matchedSegments + name, file]);
return templateRender({
name,
file,
});
}
} else {
groups.push([name + i, file]);
return templateRender({ name: name + i, file });
}
});
// save in memory used result
const filesJoined = replaceFiles.join('\n');
urlMap.set(path, filesJoined);
return [
filesJoined,
compareString(injectPath, groups),
varTemplate(groups, name),
'',
].join('\n');
}
);
if (isBuild) cache.set(path, result);
}
return `${result}${hmr(isBuild)}`;
},
};
};
export default globTransform;
import type { LocaleType } from '/@/locales/types';
import { appStore } from '/@/store/modules/app';
export function useLocale() {
/**
*
*/
function getLocale(): string {
return appStore.getProjectConfig.locale;
}
/**
*
* @param locale
*/
async function changeLocale(locale: LocaleType): Promise<void> {
appStore.commitProjectConfigState({ locale: locale });
}
return { getLocale, changeLocale };
}
import messages from 'globby?locale!/@/locales/lang/**/*.@(ts)';
export default messages;
export default {
someentry: 'some text',
};
export default {
some: 'Get Out',
};
export default {
button: 'Login',
};
export default {
someentry: 'some text',
};
export default {
some: 'Get Out',
};
export default {
button: 'Login',
validation: {
account: 'Required Field account',
password: 'Required Field password',
},
};
export default {
someentry: '一些文本',
};
export default {
some: '出去',
};
export default {
button: '登录',
};
export type LocaleType = 'zhCN' | 'en' | 'ru' | 'ja';
......@@ -5,6 +5,7 @@ import { setupStore } from '/@/store';
import { setupAntd } from '/@/setup/ant-design-vue';
import { setupErrorHandle } from '/@/setup/error-handle';
import { setupGlobDirectives } from '/@/setup/directives';
import { setupI18n } from '/@/setup/i18n';
import { setupProdMockServer } from '../mock/_createProductionServer';
import { setApp } from '/@/setup/App';
......@@ -15,11 +16,16 @@ import { isDevMode, isProdMode, isUseMock } from '/@/utils/env';
import '/@/design/index.less';
import '/@/locales/index';
const app = createApp(App);
// Configure component library
setupAntd(app);
// Multilingual configuration
setupI18n(app);
// Configure routing
setupRouter(app);
......
......@@ -7,6 +7,7 @@ import { isProdMode } from '/@/utils/env';
// ! You need to clear the browser cache after the change
const setting: ProjectConfig = {
locale: 'en',
// color
// TODO 主题色
themeColor: primaryColor,
......
import type { App } from 'vue';
import type { I18n, Locale, I18nOptions } from 'vue-i18n';
import { createI18n } from 'vue-i18n';
import localeMessages from '/@/locales';
import { useLocale } from '/@/hooks/web/useLocale';
const { getLocale } = useLocale();
const localeData: I18nOptions = {
legacy: false,
locale: getLocale(),
// TODO: setting fallback inside settings
fallbackLocale: 'en',
messages: localeMessages,
// availableLocales: ['ru'],
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: false, // true - warning off
silentFallbackWarn: true,
};
let i18n: I18n;
// setup i18n instance with glob
export function setupI18n(app: App) {
i18n = createI18n(localeData) as I18n;
setI18nLanguage(getLocale());
app.use(i18n);
}
export function setI18nLanguage(locale: Locale): void {
// @ts-ignore
i18n.global.locale.value = locale;
// i18n.global.setLocaleMessage(locale, messages);
}
import type { App } from 'vue';
import { createStore, createLogger, Plugin } from 'vuex';
import {
createStore,
// createLogger, Plugin
} from 'vuex';
import { config } from 'vuex-module-decorators';
import { isDevMode } from '/@/utils/env';
config.rawError = true;
const isDev = isDevMode();
const plugins: Plugin<any>[] = isDev ? [createLogger()] : [];
// const plugins: Plugin<any>[] = isDev ? [createLogger()] : [];
const store = createStore({
// modules: {},
strict: isDev,
plugins,
// plugins,
});
export function setupStore(app: App<Element>) {
......
// 左侧菜单, 顶部菜单
import { MenuTypeEnum, MenuModeEnum, TriggerEnum } from '/@/enums/menuEnum';
import { ContentEnum, PermissionModeEnum, ThemeEnum, RouterTransitionEnum } from '/@/enums/appEnum';
import type { LocaleType } from '/@/locales/types';
export interface MessageSetting {
title: string;
// 取消按钮的文字,
......@@ -55,6 +55,7 @@ export interface HeaderSetting {
showNotice: boolean;
}
export interface ProjectConfig {
locale: LocaleType;
// header背景色
headerBgColor: string;
// 左侧菜单背景色
......
......@@ -4,4 +4,6 @@ declare module 'globby!/@/router/routes/modules/**/*.@(ts)';
declare module 'globby!/@/router/menus/modules/**/*.@(ts)';
declare module 'globby?locale!/@/locales/lang/**/*.@(ts)';
declare const React: string;
......@@ -11,14 +11,14 @@
<a-form class="mx-auto mt-10" :model="formData" :rules="formRules" ref="formRef">
<a-form-item name="account">
<a-input size="large" v-model:value="formData.account" placeholder="Username: vben" />
<a-input size="large" v-model:value="formData.account" placeholder="username: vben" />
</a-form-item>
<a-form-item name="password">
<a-input-password
size="large"
visibilityToggle
v-model:value="formData.password"
placeholder="Password: 123456"
placeholder="password: 123456"
/>
</a-form-item>
......@@ -28,13 +28,13 @@
<a-row>
<a-col :span="12">
<a-form-item>
<!-- 未做逻辑,需要自行处理 -->
<!-- No logic, you need to deal with it yourself -->
<a-checkbox v-model:checked="autoLogin" size="small">自动登录</a-checkbox>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item :style="{ 'text-align': 'right' }">
<!-- 未做逻辑,需要自行处理 -->
<!-- No logic, you need to deal with it yourself -->
<a-button type="link" size="small">忘记密码</a-button>
</a-form-item>
</a-col>
......@@ -47,7 +47,7 @@
:block="true"
@click="login"
:loading="formState.loading"
>登录</a-button
>{{ t('system.login.button') }}</a-button
>
</a-form-item>
</a-form>
......@@ -57,20 +57,15 @@
</div>
</template>
<script lang="ts">
import {
defineComponent,
reactive,
ref,
unref,
toRaw,
// computed
} from 'vue';
import { defineComponent, reactive, ref, unref, toRaw } from 'vue';
import { Checkbox } from 'ant-design-vue';
import Button from '/@/components/Button/index.vue';
// import { BasicDragVerify, DragVerifyActionType } from '/@/components/Verify/index';
import { userStore } from '/@/store/modules/user';
import { useI18n } from 'vue-i18n';
// import { appStore } from '/@/store/modules/app';
import { useMessage } from '/@/hooks/web/useMessage';
import { useSetting } from '/@/hooks/core/useSetting';
......@@ -139,7 +134,7 @@
formState.loading = false;
}
}
const { t } = useI18n();
return {
formRef,
// verifyRef,
......@@ -151,6 +146,7 @@
// openLoginVerify: openLoginVerifyRef,
title: globSetting && globSetting.title,
logo,
t,
};
},
});
......
......@@ -4,8 +4,9 @@ import { resolve } from 'path';
import { modifyVars } from './build/config/lessModifyVars';
import { createProxy } from './build/vite/proxy';
import globbyTransform from './build/vite/plugin/context/transform';
import dynamicImportTransform from './build/vite/plugin/dynamicImport/index';
import globbyTransform from './build/vite/plugin/transform/globby';
import dynamicImportTransform from './build/vite/plugin/transform/dynamic-import';
import { isDevFn, loadEnv } from './build/utils';
......@@ -111,6 +112,11 @@ const viteConfig: UserConfig = {
},
define: {
__VERSION__: pkg.version,
// use vue-i18-next
// Suppress warning
__VUE_I18N_LEGACY_API__: false,
__VUE_I18N_FULL_INSTALL__: false,
__INTLIFY_PROD_DEVTOOLS__: false,
},
cssPreprocessOptions: {
less: {
......@@ -135,5 +141,13 @@ const viteConfig: UserConfig = {
export default {
...viteConfig,
transforms: [globbyTransform(viteConfig), dynamicImportTransform(viteEnv)],
transforms: [
globbyTransform({
resolvers: viteConfig.resolvers,
root: viteConfig.root,
alias: viteConfig.alias,
includes: [resolve('src/router'), resolve('src/locales')],
}),
dynamicImportTransform(viteEnv),
],
} as UserConfig;
......@@ -1323,6 +1323,11 @@
dependencies:
"@types/node" "*"
"@types/globrex@^0.1.0":
version "0.1.0"
resolved "https://registry.npmjs.org/@types/globrex/-/globrex-0.1.0.tgz#baf4ac8e36947017612c01fde7c7b641dc0b6c89"
integrity sha512-aBkxDgp/UbnluE+CIT3V3PoNewwOlLCzXSF3ipD86Slv8xVjwxrDAfSGbsfGgMzPo/fEMPXc+gNUJbtiugwfoA==
"@types/http-assert@*":
version "1.5.1"
resolved "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.1.tgz#d775e93630c2469c2f980fc27e3143240335db3b"
......@@ -1732,18 +1737,18 @@
vscode-languageserver-textdocument "^1.0.1"
vscode-uri "^2.1.2"
"@vueuse/core@4.0.0-beta.41":
version "4.0.0-beta.41"
resolved "https://registry.npmjs.org/@vueuse/core/-/core-4.0.0-beta.41.tgz#0058aed5ade75ae2866283498009ad5172cbae84"
integrity sha512-CgUih65PzYScorm1S4F93e6XXm+qxA8GrRLOSB1kXaqtP6vXedwkBxKkNEYNACx4reL4VEHqM/BrM6FajXkQUg==
"@vueuse/core@4.0.0-rc.3":
version "4.0.0-rc.3"
resolved "https://registry.npmjs.org/@vueuse/core/-/core-4.0.0-rc.3.tgz#5381ca657e10df596cd7027fc5c96b2d4b3a090c"
integrity sha512-dQ/FZgo0z7kBFOvDWxuzaUrmuO8X1AlQk17e3PU1TVtG2Uu+mCvjPNbuvI2fjhTjl5rzPJawwoU2WZFj+nlFvw==
dependencies:
"@vueuse/shared" "4.0.0-beta.41"
"@vueuse/shared" "4.0.0-rc.3"
vue-demi latest
"@vueuse/shared@4.0.0-beta.41":
version "4.0.0-beta.41"
resolved "https://registry.npmjs.org/@vueuse/shared/-/shared-4.0.0-beta.41.tgz#395782ea2e580f1fc9488d25c89bd09f70170b25"
integrity sha512-dqnuEPPC3OUJ6L6rhMiOCuPWIR698DtdwOydwCZBISsG2V6gZ2QFND6xtRwLib6/lhUMYVYPwIz3hPjlx7BIzw==
"@vueuse/shared@4.0.0-rc.3":
version "4.0.0-rc.3"
resolved "https://registry.npmjs.org/@vueuse/shared/-/shared-4.0.0-rc.3.tgz#42fb56fed3779f3b8a17a82c16a364bad20d01b7"
integrity sha512-VY0x/XxpeTMHp/0FDiv1cgUUxkJGQl7liiM2AjR/J7+Ys/2Y2dijD5cAKViq9FGUPQQsOcLptMvMvUsDMoN4DA==
dependencies:
vue-demi latest
......@@ -3222,6 +3227,13 @@ dot-prop@^5.1.0:
dependencies:
is-obj "^2.0.0"
dot-prop@^6.0.0:
version "6.0.0"
resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.0.tgz#bd579fd704d970981c4b05de591db648959f2ebb"
integrity sha512-xCbB8IN3IT+tdgoEPOnJmYTNJDrygGFOmiQEiVa5eAD+JEB1vTgMNhVGRnN5Eex/6amck7cdcrixb1qN9Go+GQ==
dependencies:
is-obj "^2.0.0"
dotenv-expand@^5.1.0:
version "5.1.0"
resolved "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0"
......@@ -4092,6 +4104,11 @@ globjoin@^0.1.4:
resolved "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz#2f4494ac8919e3767c5cbb691e9f463324285d43"
integrity sha1-L0SUrIkZ43Z8XLtpHp9GMyQoXUM=
globrex@^0.1.2:
version "0.1.2"
resolved "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098"
integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==
gonzales-pe@^4.3.0:
version "4.3.0"
resolved "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.3.0.tgz#fe9dec5f3c557eead09ff868c65826be54d067b3"
......@@ -8167,10 +8184,10 @@ vue-eslint-parser@^7.1.1:
esquery "^1.0.1"
lodash "^4.17.15"
vue-i18n@^9.0.0-beta.7:
version "9.0.0-beta.7"
resolved "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.0.0-beta.7.tgz#f6fad5b4be218018aab4797f80dd2a95ee5236f9"
integrity sha512-hFl0XnV91P/4UyWvHYvdYxuk3GRnKIW9zXAm6hrUU4mOIwpqchi7jVQva2TJLr52Mpsu4zYXmzL1h5pgrKmCfQ==
vue-i18n@^9.0.0-beta.8:
version "9.0.0-beta.8"
resolved "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.0.0-beta.8.tgz#92282d5b5e0e6f15cc04943ce11bf49db610468f"
integrity sha512-tViSN96jLi0AKvAVi4twcYYN5Ld++SqN1/140ua+YWm/iRbO2M0rAcsZ7e6/4LTm6Pd1ldSwWihSuv2bSQmlnw==
dependencies:
source-map "0.6.1"
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册