diff --git a/build/vite/plugin/context/transform.ts b/build/vite/plugin/context/transform.ts deleted file mode 100644 index 49f5e01a..00000000 --- a/build/vite/plugin/context/transform.ts +++ /dev/null @@ -1,96 +0,0 @@ -// 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; - 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[] = []; - 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; diff --git a/build/vite/plugin/dynamicImport/index.ts b/build/vite/plugin/transform/dynamic-import/index.ts similarity index 97% rename from build/vite/plugin/dynamicImport/index.ts rename to build/vite/plugin/transform/dynamic-import/index.ts index 73cfe3c5..7fd52379 100644 --- a/build/vite/plugin/dynamicImport/index.ts +++ b/build/vite/plugin/transform/dynamic-import/index.ts @@ -1,7 +1,6 @@ // 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() }); diff --git a/build/vite/plugin/transform/globby/index.ts b/build/vite/plugin/transform/globby/index.ts new file mode 100644 index 00000000..235b00d3 --- /dev/null +++ b/build/vite/plugin/transform/globby/index.ts @@ -0,0 +1,201 @@ +// 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; + 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 = {}; + 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 = {}; + 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[] = []; + 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; diff --git a/package.json b/package.json index 42bb4b5a..5554c339 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ }, "dependencies": { "@iconify/iconify": "^2.0.0-rc.2", - "@vueuse/core": "4.0.0-beta.41", + "@vueuse/core": "4.0.0-rc.3", "ant-design-vue": "2.0.0-beta.15", "apexcharts": "3.22.0", "axios": "^0.21.0", @@ -35,7 +35,7 @@ "qrcode": "^1.4.4", "vditor": "^3.6.2", "vue": "^3.0.2", - "vue-i18n": "^9.0.0-beta.7", + "vue-i18n": "^9.0.0-beta.8", "vue-router": "^4.0.0-rc.3", "vuex": "^4.0.0-rc.1", "vuex-module-decorators": "^1.0.1", @@ -50,6 +50,7 @@ "@purge-icons/generated": "^0.4.1", "@types/echarts": "^4.9.1", "@types/fs-extra": "^9.0.4", + "@types/globrex": "^0.1.0", "@types/koa-static": "^4.0.1", "@types/lodash-es": "^4.17.3", "@types/mockjs": "^1.0.3", @@ -68,6 +69,7 @@ "conventional-changelog-cli": "^2.1.1", "conventional-changelog-custom-config": "^0.3.1", "cross-env": "^7.0.2", + "dot-prop": "^6.0.0", "dotenv": "^8.2.0", "eslint": "^7.13.0", "eslint-config-prettier": "^6.15.0", @@ -75,6 +77,7 @@ "eslint-plugin-vue": "^7.1.0", "esno": "^0.2.4", "fs-extra": "^9.0.1", + "globrex": "^0.1.2", "husky": "^4.3.0", "koa-static": "^5.0.0", "less": "^3.12.2", diff --git a/src/hooks/web/useLocale.ts b/src/hooks/web/useLocale.ts new file mode 100644 index 00000000..33e25a48 --- /dev/null +++ b/src/hooks/web/useLocale.ts @@ -0,0 +1,21 @@ +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 { + appStore.commitProjectConfigState({ locale: locale }); + } + + return { getLocale, changeLocale }; +} diff --git a/src/locales/index.ts b/src/locales/index.ts new file mode 100644 index 00000000..9f7a9f67 --- /dev/null +++ b/src/locales/index.ts @@ -0,0 +1,3 @@ +import messages from 'globby?locale!/@/locales/lang/**/*.@(ts)'; + +export default messages; diff --git a/src/locales/lang/en/routes/menus/dashboard.ts b/src/locales/lang/en/routes/menus/dashboard.ts new file mode 100644 index 00000000..6eebaa61 --- /dev/null +++ b/src/locales/lang/en/routes/menus/dashboard.ts @@ -0,0 +1,3 @@ +export default { + someentry: 'some text', +}; diff --git a/src/locales/lang/en/system/basic.ts b/src/locales/lang/en/system/basic.ts new file mode 100644 index 00000000..55feebfc --- /dev/null +++ b/src/locales/lang/en/system/basic.ts @@ -0,0 +1,3 @@ +export default { + some: 'Get Out', +}; diff --git a/src/locales/lang/en/system/login.ts b/src/locales/lang/en/system/login.ts new file mode 100644 index 00000000..55a3fd03 --- /dev/null +++ b/src/locales/lang/en/system/login.ts @@ -0,0 +1,3 @@ +export default { + button: 'Login', +}; diff --git a/src/locales/lang/ru/routes/menus/dashboard.ts b/src/locales/lang/ru/routes/menus/dashboard.ts new file mode 100644 index 00000000..6eebaa61 --- /dev/null +++ b/src/locales/lang/ru/routes/menus/dashboard.ts @@ -0,0 +1,3 @@ +export default { + someentry: 'some text', +}; diff --git a/src/locales/lang/ru/system/basic.ts b/src/locales/lang/ru/system/basic.ts new file mode 100644 index 00000000..55feebfc --- /dev/null +++ b/src/locales/lang/ru/system/basic.ts @@ -0,0 +1,3 @@ +export default { + some: 'Get Out', +}; diff --git a/src/locales/lang/ru/system/login.ts b/src/locales/lang/ru/system/login.ts new file mode 100644 index 00000000..73634a98 --- /dev/null +++ b/src/locales/lang/ru/system/login.ts @@ -0,0 +1,7 @@ +export default { + button: 'Login', + validation: { + account: 'Required Field account', + password: 'Required Field password', + }, +}; diff --git a/src/locales/lang/zhCN/routes/menus/dashboard.ts b/src/locales/lang/zhCN/routes/menus/dashboard.ts new file mode 100644 index 00000000..e9fbe4bc --- /dev/null +++ b/src/locales/lang/zhCN/routes/menus/dashboard.ts @@ -0,0 +1,3 @@ +export default { + someentry: '一些文本', +}; diff --git a/src/locales/lang/zhCN/system/basic.ts b/src/locales/lang/zhCN/system/basic.ts new file mode 100644 index 00000000..82e87773 --- /dev/null +++ b/src/locales/lang/zhCN/system/basic.ts @@ -0,0 +1,3 @@ +export default { + some: '出去', +}; diff --git a/src/locales/lang/zhCN/system/login.ts b/src/locales/lang/zhCN/system/login.ts new file mode 100644 index 00000000..802e82b5 --- /dev/null +++ b/src/locales/lang/zhCN/system/login.ts @@ -0,0 +1,3 @@ +export default { + button: '登录', +}; diff --git a/src/locales/types.ts b/src/locales/types.ts new file mode 100644 index 00000000..530f1871 --- /dev/null +++ b/src/locales/types.ts @@ -0,0 +1 @@ +export type LocaleType = 'zhCN' | 'en' | 'ru' | 'ja'; diff --git a/src/main.ts b/src/main.ts index 6b9296a5..60de292a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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); diff --git a/src/settings/projectSetting.ts b/src/settings/projectSetting.ts index 7702387d..8f98f171 100644 --- a/src/settings/projectSetting.ts +++ b/src/settings/projectSetting.ts @@ -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, diff --git a/src/setup/i18n/index.ts b/src/setup/i18n/index.ts new file mode 100644 index 00000000..ee77fa42 --- /dev/null +++ b/src/setup/i18n/index.ts @@ -0,0 +1,35 @@ +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); +} diff --git a/src/store/index.ts b/src/store/index.ts index 5feab466..92799a44 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,16 +1,19 @@ 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[] = isDev ? [createLogger()] : []; +// const plugins: Plugin[] = isDev ? [createLogger()] : []; const store = createStore({ // modules: {}, strict: isDev, - plugins, + // plugins, }); export function setupStore(app: App) { diff --git a/src/types/config.d.ts b/src/types/config.d.ts index 99739357..e1339a91 100644 --- a/src/types/config.d.ts +++ b/src/types/config.d.ts @@ -1,7 +1,7 @@ // 左侧菜单, 顶部菜单 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; // 左侧菜单背景色 diff --git a/src/types/module.d.ts b/src/types/module.d.ts index 9f3b5912..ddaa4b0a 100644 --- a/src/types/module.d.ts +++ b/src/types/module.d.ts @@ -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; diff --git a/src/views/sys/login/Login.vue b/src/views/sys/login/Login.vue index a6014858..1146150f 100644 --- a/src/views/sys/login/Login.vue +++ b/src/views/sys/login/Login.vue @@ -11,14 +11,14 @@ - + @@ -28,13 +28,13 @@ - + 自动登录 - + 忘记密码 @@ -47,7 +47,7 @@ :block="true" @click="login" :loading="formState.loading" - >登录{{ t('system.login.button') }} @@ -57,20 +57,15 @@