From 932063cfb3eebdf2a38fc76fed755bd19e54cc7b Mon Sep 17 00:00:00 2001 From: sven Date: Thu, 28 Mar 2024 22:17:11 +0800 Subject: [PATCH] feat: Support vue i18n --- .vscode/extensions.json | 15 ++--- .vscode/settings.json | 10 +++- build/vite/index.ts | 11 ++++ package.json | 2 + pnpm-lock.yaml | 111 +++++++++++++++++++++++++++++++++++- src/auto-imports.d.ts | 3 + src/components.d.ts | 4 ++ src/components/NavBar.vue | 7 ++- src/locales/en-US.json | 17 ++++++ src/locales/zh-CN.json | 17 ++++++ src/main.ts | 2 + src/pages/charts/index.vue | 1 + src/pages/counter/index.vue | 1 + src/pages/index.vue | 45 ++++++++++++--- src/pages/mock/index.vue | 9 +-- src/pages/unocss/index.vue | 51 +++++++++-------- src/utils/i18n.ts | 41 +++++++++++++ src/vue-router.d.ts | 9 +++ tsconfig.json | 3 +- 19 files changed, 310 insertions(+), 49 deletions(-) create mode 100644 src/locales/en-US.json create mode 100644 src/locales/zh-CN.json create mode 100644 src/utils/i18n.ts create mode 100644 src/vue-router.d.ts diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 5fc3957e..5c28e8c9 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,7 +1,8 @@ -{ - "recommendations": [ - "Vue.volar", - "antfu.unocss", - "simonhe.common-intellisense" - ] -} +{ + "recommendations": [ + "Vue.volar", + "antfu.unocss", + "simonhe.common-intellisense", + "lokalise.i18n-ally" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index e9e859d6..dccc8782 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -49,5 +49,13 @@ "common-intellisense.showSlots": false, "common-intellisense.ui": [ "vant4" - ] + ], + + // Configuration of i18n i18n-ally + "i18n-ally.enabledParsers": ["json"], + "i18n-ally.displayLanguage": "zh-CN", + "i18n-ally.localesPaths": [ + "src/locales" + ], + "i18n-ally.keystyle": "nested" } diff --git a/build/vite/index.ts b/build/vite/index.ts index 6ba58ed3..a2bd75be 100644 --- a/build/vite/index.ts +++ b/build/vite/index.ts @@ -1,3 +1,5 @@ +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' import { unheadVueComposablesImports } from '@unhead/vue' import legacy from '@vitejs/plugin-legacy' import vue from '@vitejs/plugin-vue' @@ -12,6 +14,7 @@ import { VitePWA } from 'vite-plugin-pwa' import Sitemap from 'vite-plugin-sitemap' import VueDevTools from 'vite-plugin-vue-devtools' import Layouts from 'vite-plugin-vue-layouts' +import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite' import { createViteVConsole } from './vconsole' export function createVitePlugins() { @@ -56,6 +59,8 @@ export function createVitePlugins() { VueRouterAutoImports, { 'vue-router/auto': ['useLink'], + '@/utils/i18n': ['i18n', 'locale'], + 'vue-i18n': ['useI18n'], }, unheadVueComposablesImports, ], @@ -65,6 +70,12 @@ export function createVitePlugins() { ], }), + // https://github.com/intlify/bundle-tools/tree/main/packages/unplugin-vue-i18n + VueI18nPlugin({ + // locale messages resource pre-compile option + include: resolve(dirname(fileURLToPath(import.meta.url)), '../../src/locales/**'), + }), + legacy({ targets: ['defaults', 'not IE 11'], }), diff --git a/package.json b/package.json index 86f7d7c1..23716e49 100644 --- a/package.json +++ b/package.json @@ -32,10 +32,12 @@ "vant": "^4.8.5", "vconsole": "^3.15.1", "vue": "^3.4.21", + "vue-i18n": "^9.10.2", "vue-router": "^4.3.0" }, "devDependencies": { "@antfu/eslint-config": "2.8.0", + "@intlify/unplugin-vue-i18n": "^4.0.0", "@types/lodash-es": "^4.17.12", "@types/node": "^20.11.25", "@types/nprogress": "^0.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94818540..a48d9d02 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,6 +50,9 @@ dependencies: vue: specifier: ^3.4.21 version: 3.4.21(typescript@5.4.2) + vue-i18n: + specifier: ^9.10.2 + version: 9.10.2(vue@3.4.21) vue-router: specifier: ^4.3.0 version: 4.3.0(vue@3.4.21) @@ -58,6 +61,9 @@ devDependencies: '@antfu/eslint-config': specifier: 2.8.0 version: 2.8.0(@unocss/eslint-plugin@0.58.5)(@vue/compiler-sfc@3.4.21)(eslint-ts-patch@8.57.0-0)(typescript@5.4.2)(vitest@1.3.1) + '@intlify/unplugin-vue-i18n': + specifier: ^4.0.0 + version: 4.0.0(rollup@4.12.1)(vue-i18n@9.10.2) '@types/lodash-es': specifier: ^4.17.12 version: 4.17.12 @@ -1908,6 +1914,81 @@ packages: - supports-color dev: true + /@intlify/bundle-utils@8.0.0(vue-i18n@9.10.2): + resolution: {integrity: sha512-1B++zykRnMwQ+20SpsZI1JCnV/YJt9Oq7AGlEurzkWJOFtFAVqaGc/oV36PBRYeiKnTbY9VYfjBimr2Vt42wLQ==} + engines: {node: '>= 14.16'} + peerDependencies: + petite-vue-i18n: '*' + vue-i18n: '*' + peerDependenciesMeta: + petite-vue-i18n: + optional: true + vue-i18n: + optional: true + dependencies: + '@intlify/message-compiler': 9.10.2 + '@intlify/shared': 9.10.2 + acorn: 8.11.3 + escodegen: 2.1.0 + estree-walker: 2.0.2 + jsonc-eslint-parser: 2.4.0 + mlly: 1.6.1 + source-map-js: 1.0.2 + vue-i18n: 9.10.2(vue@3.4.21) + yaml-eslint-parser: 1.2.2 + dev: true + + /@intlify/core-base@9.10.2: + resolution: {integrity: sha512-HGStVnKobsJL0DoYIyRCGXBH63DMQqEZxDUGrkNI05FuTcruYUtOAxyL3zoAZu/uDGO6mcUvm3VXBaHG2GdZCg==} + engines: {node: '>= 16'} + dependencies: + '@intlify/message-compiler': 9.10.2 + '@intlify/shared': 9.10.2 + + /@intlify/message-compiler@9.10.2: + resolution: {integrity: sha512-ntY/kfBwQRtX5Zh6wL8cSATujPzWW2ZQd1QwKyWwAy5fMqJyyixHMeovN4fmEyCqSu+hFfYOE63nU94evsy4YA==} + engines: {node: '>= 16'} + dependencies: + '@intlify/shared': 9.10.2 + source-map-js: 1.0.2 + + /@intlify/shared@9.10.2: + resolution: {integrity: sha512-ttHCAJkRy7R5W2S9RVnN9KYQYPIpV2+GiS79T4EE37nrPyH6/1SrOh3bmdCRC1T3ocL8qCDx7x2lBJ0xaITU7Q==} + engines: {node: '>= 16'} + + /@intlify/unplugin-vue-i18n@4.0.0(rollup@4.12.1)(vue-i18n@9.10.2): + resolution: {integrity: sha512-q2Mhqa/mLi0tulfLFO4fMXXvEbkSZpI5yGhNNsLTNJJ41icEGUuyDe+j5zRZIKSkOJRgX6YbCyibTDJdRsukmw==} + engines: {node: '>= 14.16'} + peerDependencies: + petite-vue-i18n: '*' + vue-i18n: '*' + vue-i18n-bridge: '*' + peerDependenciesMeta: + petite-vue-i18n: + optional: true + vue-i18n: + optional: true + vue-i18n-bridge: + optional: true + dependencies: + '@intlify/bundle-utils': 8.0.0(vue-i18n@9.10.2) + '@intlify/shared': 9.10.2 + '@rollup/pluginutils': 5.1.0(rollup@4.12.1) + '@vue/compiler-sfc': 3.4.21 + debug: 4.3.4 + fast-glob: 3.3.2 + js-yaml: 4.1.0 + json5: 2.2.3 + pathe: 1.1.2 + picocolors: 1.0.0 + source-map-js: 1.0.2 + unplugin: 1.8.3 + vue-i18n: 9.10.2(vue@3.4.21) + transitivePeerDependencies: + - rollup + - supports-color + dev: true + /@jest/schemas@29.6.3: resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4264,6 +4345,18 @@ packages: engines: {node: '>=12'} dev: true + /escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + dev: true + /eslint-compat-utils@0.1.2(eslint-ts-patch@8.57.0-0): resolution: {integrity: sha512-Jia4JDldWnFNIru1Ehx1H5s9/yxiRHY/TimCuUc0jNexew3cF1gI6CYZil1ociakfWO3rRqFjl1mskBblB3RYg==} engines: {node: '>=12'} @@ -4699,6 +4792,12 @@ packages: eslint-visitor-keys: 3.4.3 dev: true + /esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + dev: true + /esquery@1.5.0: resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} engines: {node: '>=0.10'} @@ -8346,6 +8445,17 @@ packages: - supports-color dev: true + /vue-i18n@9.10.2(vue@3.4.21): + resolution: {integrity: sha512-ECJ8RIFd+3c1d3m1pctQ6ywG5Yj8Efy1oYoAKQ9neRdkLbuKLVeW4gaY5HPkD/9ssf1pOnUrmIFjx2/gkGxmEw==} + engines: {node: '>= 16'} + peerDependencies: + vue: ^3.0.0 + dependencies: + '@intlify/core-base': 9.10.2 + '@intlify/shared': 9.10.2 + '@vue/devtools-api': 6.6.1 + vue: 3.4.21(typescript@5.4.2) + /vue-router@4.3.0(vue@3.4.21): resolution: {integrity: sha512-dqUcs8tUeG+ssgWhcPbjHvazML16Oga5w34uCUmsk7i0BcnskoLGwjpa15fqMr2Fa5JgVBrdL2MEgqz6XZ/6IQ==} peerDependencies: @@ -8543,7 +8653,6 @@ packages: /workbox-google-analytics@7.0.0: resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==} - deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained dependencies: workbox-background-sync: 7.0.0 workbox-core: 7.0.0 diff --git a/src/auto-imports.d.ts b/src/auto-imports.d.ts index c9484b82..ab28eda7 100644 --- a/src/auto-imports.d.ts +++ b/src/auto-imports.d.ts @@ -46,6 +46,7 @@ declare global { const getCurrentInstance: typeof import('vue')['getCurrentInstance'] const getCurrentScope: typeof import('vue')['getCurrentScope'] const h: typeof import('vue')['h'] + const i18n: typeof import('@/utils/i18n')['i18n'] const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch'] const inject: typeof import('vue')['inject'] const injectHead: typeof import('@unhead/vue')['injectHead'] @@ -57,6 +58,7 @@ declare global { const isReadonly: typeof import('vue')['isReadonly'] const isRef: typeof import('vue')['isRef'] const it: typeof import('vitest')['it'] + const locale: typeof import('@/utils/i18n')['locale'] const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable'] const markRaw: typeof import('vue')['markRaw'] const nextTick: typeof import('vue')['nextTick'] @@ -193,6 +195,7 @@ declare global { const useGeolocation: typeof import('@vueuse/core')['useGeolocation'] const useHead: typeof import('@unhead/vue')['useHead'] const useHeadSafe: typeof import('@unhead/vue')['useHeadSafe'] + const useI18n: typeof import('vue-i18n')['useI18n'] const useIdle: typeof import('@vueuse/core')['useIdle'] const useImage: typeof import('@vueuse/core')['useImage'] const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll'] diff --git a/src/components.d.ts b/src/components.d.ts index 5718836e..b9a6a2b1 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -16,8 +16,12 @@ declare module 'vue' { VanCellGroup: typeof import('vant/es')['CellGroup'] VanConfigProvider: typeof import('vant/es')['ConfigProvider'] VanEmpty: typeof import('vant/es')['Empty'] + VanField: typeof import('vant/es')['Field'] VanIcon: typeof import('vant/es')['Icon'] VanNavBar: typeof import('vant/es')['NavBar'] + VanPicker: typeof import('vant/es')['Picker'] + VanPopup: typeof import('vant/es')['Popup'] + VanRadio: typeof import('vant/es')['Radio'] VanSpace: typeof import('vant/es')['Space'] VanSwitch: typeof import('vant/es')['Switch'] } diff --git a/src/components/NavBar.vue b/src/components/NavBar.vue index 50f09050..ce2ffcac 100644 --- a/src/components/NavBar.vue +++ b/src/components/NavBar.vue @@ -9,9 +9,12 @@ function onBack() { router.replace('/') } +const { t } = useI18n() + const title = computed(() => { - const { title } = (route.meta as { title?: string }) || {} - return title || '' + if (!route.meta || !route.meta.title) + return '' + return route.meta.i18n ? t(route.meta.i18n) : route.meta.title }) diff --git a/src/locales/en-US.json b/src/locales/en-US.json new file mode 100644 index 00000000..bbfc07db --- /dev/null +++ b/src/locales/en-US.json @@ -0,0 +1,17 @@ +{ + "home": { + "darkMode": "🌗 Dark Mode", + "mockGuide": "💿 Mock Guide", + "language": "📚 Language", + "404Demo": "🙅 Page 404 Demo", + "echartsDemo": "📊 Echarts Demo", + "persistPiniaState": "🍍 Persistent Pinia state", + "unocssExample": "🎨 Unocss example" + }, + "mock": { + "fromAsyncData": "Data from asynchronous requests", + "noData": "No data", + "pull": "Pull", + "reset": "Reset" + } +} diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json new file mode 100644 index 00000000..514efcda --- /dev/null +++ b/src/locales/zh-CN.json @@ -0,0 +1,17 @@ +{ + "home": { + "darkMode": "🌗 暗黑模式", + "mockGuide": "💿 Mock 指南", + "language": "📚 语言", + "echartsDemo": "📊 Echarts 演示", + "persistPiniaState": "🍍 持久化 Pinia 状态", + "404Demo": "🙅 404页 演示", + "unocssExample": "🎨 Unocss 示例" + }, + "mock": { + "fromAsyncData": "来自异步请求的数据", + "pull": "请求", + "reset": "清空", + "noData": "暂无数据" + } +} diff --git a/src/main.ts b/src/main.ts index 78740085..8b6abdae 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,6 +5,7 @@ import router from '@/router' import pinia from '@/stores' import 'virtual:uno.css' import '@/styles/app.less' +import { i18n } from '@/utils/i18n' // Vant 桌面端适配 import '@vant/touch-emulator' @@ -26,5 +27,6 @@ const head = createHead() app.use(head) app.use(router) app.use(pinia) +app.use(i18n) app.mount('#app') diff --git a/src/pages/charts/index.vue b/src/pages/charts/index.vue index 8c6590c8..27e1d602 100644 --- a/src/pages/charts/index.vue +++ b/src/pages/charts/index.vue @@ -4,6 +4,7 @@ definePage({ meta: { level: 2, title: '📊 Echarts 演示', + i18n: 'home.echartsDemo', }, }) diff --git a/src/pages/counter/index.vue b/src/pages/counter/index.vue index 66d12e9e..4ff03697 100644 --- a/src/pages/counter/index.vue +++ b/src/pages/counter/index.vue @@ -7,6 +7,7 @@ definePage({ meta: { level: 2, title: '🍍 持久化 Pinia 状态', + i18n: 'home.persistPiniaState', }, }) diff --git a/src/pages/index.vue b/src/pages/index.vue index 5e3dfc02..cdf69fa4 100644 --- a/src/pages/index.vue +++ b/src/pages/index.vue @@ -1,5 +1,7 @@