diff options
-rw-r--r-- | .eslintrc-auto-import.json | 1 | ||||
-rw-r--r-- | auto-imports.d.ts | 2 | ||||
-rw-r--r-- | locales/en.yml | 3 | ||||
-rw-r--r-- | locales/fr.yml | 3 | ||||
-rw-r--r-- | package.json | 2 | ||||
-rw-r--r-- | pnpm-lock.yaml | 182 | ||||
-rw-r--r-- | src/main.ts | 2 | ||||
-rw-r--r-- | src/pages/Home.page.vue | 3 | ||||
-rw-r--r-- | src/plugins/i18n.plugin.ts | 50 | ||||
-rw-r--r-- | tsconfig.app.json | 2 | ||||
-rw-r--r-- | vite.config.ts | 11 |
11 files changed, 256 insertions, 5 deletions
diff --git a/.eslintrc-auto-import.json b/.eslintrc-auto-import.json index 744e14d..aaba2b3 100644 --- a/.eslintrc-auto-import.json +++ b/.eslintrc-auto-import.json @@ -173,6 +173,7 @@ "useFullscreen": true, "useGamepad": true, "useGeolocation": true, + "useI18n": true, "useIdle": true, "useImage": true, "useInfiniteScroll": true, diff --git a/auto-imports.d.ts b/auto-imports.d.ts index 9dccb44..9a69b9d 100644 --- a/auto-imports.d.ts +++ b/auto-imports.d.ts @@ -170,6 +170,7 @@ declare global { const useFullscreen: typeof import('@vueuse/core')['useFullscreen'] const useGamepad: typeof import('@vueuse/core')['useGamepad'] const useGeolocation: typeof import('@vueuse/core')['useGeolocation'] + 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'] @@ -459,6 +460,7 @@ declare module 'vue' { readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']> readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']> readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']> + readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']> readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']> readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']> readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']> diff --git a/locales/en.yml b/locales/en.yml new file mode 100644 index 0000000..4982dc5 --- /dev/null +++ b/locales/en.yml @@ -0,0 +1,3 @@ +home: + categories: + newestTools: "Newest tools" diff --git a/locales/fr.yml b/locales/fr.yml new file mode 100644 index 0000000..2846356 --- /dev/null +++ b/locales/fr.yml @@ -0,0 +1,3 @@ +home: + categories: + newestTools: "Nouveaux outils" diff --git a/package.json b/package.json index 277da55..268d015 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "ua-parser-js": "^1.0.35", "uuid": "^8.3.2", "vue": "^3.2.47", + "vue-i18n": "^9.2.2", "vue-router": "^4.1.6", "xml-formatter": "^3.3.2", "yaml": "^2.2.1" @@ -82,6 +83,7 @@ "devDependencies": { "@antfu/eslint-config": "^0.39.3", "@iconify-json/mdi": "^1.1.50", + "@intlify/unplugin-vue-i18n": "^0.11.0", "@playwright/test": "^1.32.3", "@rushstack/eslint-patch": "^1.2.0", "@types/bcryptjs": "^2.4.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 99488fe..d0149e0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,8 @@ -lockfileVersion: '6.0' +lockfileVersion: '6.1' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false dependencies: '@it-tools/bip39': @@ -127,6 +131,9 @@ dependencies: vue: specifier: ^3.2.47 version: 3.2.47 + vue-i18n: + specifier: ^9.2.2 + version: 9.2.2(vue@3.2.47) vue-router: specifier: ^4.1.6 version: 4.1.6(vue@3.2.47) @@ -144,6 +151,9 @@ devDependencies: '@iconify-json/mdi': specifier: ^1.1.50 version: 1.1.50 + '@intlify/unplugin-vue-i18n': + specifier: ^0.11.0 + version: 0.11.0(rollup@2.79.1)(vue-i18n@9.2.2) '@playwright/test': specifier: ^1.32.3 version: 1.32.3 @@ -1731,6 +1741,110 @@ packages: - supports-color dev: true + /@intlify/bundle-utils@6.0.1(vue-i18n@9.2.2): + resolution: {integrity: sha512-BkeZNKZiC0B7K3OYMwiPLoAqsZmKH3SxTL75vYAkuQ//XWR8WO0NpfjXhTxgLTVFHxMcNb2agAopC0DP6fqDrg==} + 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.3.0-beta.17 + '@intlify/shared': 9.3.0-beta.17 + acorn: 8.8.2 + escodegen: 2.0.0 + estree-walker: 2.0.2 + jsonc-eslint-parser: 1.4.1 + magic-string: 0.30.0 + mlly: 1.2.0 + source-map: 0.6.1 + vue-i18n: 9.2.2(vue@3.2.47) + yaml-eslint-parser: 0.3.2 + dev: true + + /@intlify/core-base@9.2.2: + resolution: {integrity: sha512-JjUpQtNfn+joMbrXvpR4hTF8iJQ2sEFzzK3KIESOx+f+uwIjgw20igOyaIdhfsVVBCds8ZM64MoeNSx+PHQMkA==} + engines: {node: '>= 14'} + dependencies: + '@intlify/devtools-if': 9.2.2 + '@intlify/message-compiler': 9.2.2 + '@intlify/shared': 9.2.2 + '@intlify/vue-devtools': 9.2.2 + + /@intlify/devtools-if@9.2.2: + resolution: {integrity: sha512-4ttr/FNO29w+kBbU7HZ/U0Lzuh2cRDhP8UlWOtV9ERcjHzuyXVZmjyleESK6eVP60tGC9QtQW9yZE+JeRhDHkg==} + engines: {node: '>= 14'} + dependencies: + '@intlify/shared': 9.2.2 + + /@intlify/message-compiler@9.2.2: + resolution: {integrity: sha512-IUrQW7byAKN2fMBe8z6sK6riG1pue95e5jfokn8hA5Q3Bqy4MBJ5lJAofUsawQJYHeoPJ7svMDyBaVJ4d0GTtA==} + engines: {node: '>= 14'} + dependencies: + '@intlify/shared': 9.2.2 + source-map: 0.6.1 + + /@intlify/message-compiler@9.3.0-beta.17: + resolution: {integrity: sha512-i7hvVIRk1Ax2uKa9xLRJCT57to08OhFMhFXXjWN07rmx5pWQYQ23MfX1xgggv9drnWTNhqEiD+u4EJeHoS5+Ww==} + engines: {node: '>= 14'} + dependencies: + '@intlify/shared': 9.3.0-beta.17 + source-map: 0.6.1 + dev: true + + /@intlify/shared@9.2.2: + resolution: {integrity: sha512-wRwTpsslgZS5HNyM7uDQYZtxnbI12aGiBZURX3BTR9RFIKKRWpllTsgzHWvj3HKm3Y2Sh5LPC1r0PDCKEhVn9Q==} + engines: {node: '>= 14'} + + /@intlify/shared@9.3.0-beta.17: + resolution: {integrity: sha512-mscf7RQsUTOil35jTij4KGW1RC9SWQjYScwLxP53Ns6g24iEd5HN7ksbt9O6FvTmlQuX77u+MXpBdfJsGqizLQ==} + engines: {node: '>= 14'} + dev: true + + /@intlify/unplugin-vue-i18n@0.11.0(rollup@2.79.1)(vue-i18n@9.2.2): + resolution: {integrity: sha512-ivcLZo08fvepHWV8o5lcKfhcKFSWqhwrqIAU6pUIbvq2ICo9fnXnIPYIZj7FeuHDLW1G3ADm44ZhQC3nYmvDlg==} + 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': 6.0.1(vue-i18n@9.2.2) + '@intlify/shared': 9.3.0-beta.17 + '@rollup/pluginutils': 5.0.2(rollup@2.79.1) + '@vue/compiler-sfc': 3.2.47 + debug: 4.3.4 + fast-glob: 3.2.12 + js-yaml: 4.1.0 + json5: 2.2.3 + pathe: 1.1.0 + picocolors: 1.0.0 + source-map: 0.6.1 + unplugin: 1.3.1 + vue-i18n: 9.2.2(vue@3.2.47) + transitivePeerDependencies: + - rollup + - supports-color + dev: true + + /@intlify/vue-devtools@9.2.2: + resolution: {integrity: sha512-+dUyqyCHWHb/UcvY1MlIpO87munedm3Gn6E9WWYdWrMuYLcoIoOEVDWSS8xSwtlPU+kA+MEQTP6Q1iI/ocusJg==} + engines: {node: '>= 14'} + dependencies: + '@intlify/core-base': 9.2.2 + '@intlify/shared': 9.2.2 + /@istanbuljs/schema@0.1.3: resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} @@ -3032,7 +3146,6 @@ packages: /@vue/devtools-api@6.5.0: resolution: {integrity: sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==} - dev: false /@vue/reactivity-transform@3.2.47: resolution: {integrity: sha512-m8lGXw8rdnPVVIdIFhf0LeQ/ixyHkH5plYuS83yop5n7ggVJU+z5v0zecwEnX7fa7HNLBhh2qngJJkxpwEEmYA==} @@ -3182,6 +3295,14 @@ packages: acorn-walk: 7.2.0 dev: true + /acorn-jsx@5.3.2(acorn@7.4.1): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 7.4.1 + dev: true + /acorn-jsx@5.3.2(acorn@8.8.2): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -4777,6 +4898,18 @@ packages: estraverse: 5.3.0 dev: true + /eslint-utils@2.1.0: + resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==} + engines: {node: '>=6'} + dependencies: + eslint-visitor-keys: 1.3.0 + dev: true + + /eslint-visitor-keys@1.3.0: + resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} + engines: {node: '>=4'} + dev: true + /eslint-visitor-keys@3.4.0: resolution: {integrity: sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4831,6 +4964,15 @@ packages: - supports-color dev: true + /espree@6.2.1: + resolution: {integrity: sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==} + engines: {node: '>=6.0.0'} + dependencies: + acorn: 7.4.1 + acorn-jsx: 5.3.2(acorn@7.4.1) + eslint-visitor-keys: 1.3.0 + dev: true + /espree@9.5.1: resolution: {integrity: sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -5951,6 +6093,17 @@ packages: engines: {node: '>=6'} hasBin: true + /jsonc-eslint-parser@1.4.1: + resolution: {integrity: sha512-hXBrvsR1rdjmB2kQmUjf1rEIa+TqHBGMge8pwi++C+Si1ad7EjZrJcpgwym+QGK/pqTx+K7keFAtLlVNdLRJOg==} + engines: {node: '>=8.10.0'} + dependencies: + acorn: 7.4.1 + eslint-utils: 2.1.0 + eslint-visitor-keys: 1.3.0 + espree: 6.2.1 + semver: 6.3.0 + dev: true + /jsonc-eslint-parser@2.3.0: resolution: {integrity: sha512-9xZPKVYp9DxnM3sd1yAsh/d59iIaswDkai8oTxbursfKYbg/ibjX0IzFt35+VZ8iEW453TVTXztnRvYUQlAfUQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -8498,6 +8651,18 @@ packages: - supports-color dev: true + /vue-i18n@9.2.2(vue@3.2.47): + resolution: {integrity: sha512-yswpwtj89rTBhegUAv9Mu37LNznyu3NpyLQmozF3i1hYOhwpG8RjcjIFIIfnu+2MDZJGSZPXaKWvnQA71Yv9TQ==} + engines: {node: '>= 14'} + peerDependencies: + vue: ^3.0.0 + dependencies: + '@intlify/core-base': 9.2.2 + '@intlify/shared': 9.2.2 + '@intlify/vue-devtools': 9.2.2 + '@vue/devtools-api': 6.5.0 + vue: 3.2.47 + /vue-router@4.1.6(vue@3.2.47): resolution: {integrity: sha512-DYWYwsG6xNPmLq/FmZn8Ip+qrhFEzA14EI12MsMgVxvHFDYvlr4NXpVF5hrRH1wVcDP8fGi5F4rxuJSl8/r+EQ==} peerDependencies: @@ -8928,6 +9093,14 @@ packages: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} dev: true + /yaml-eslint-parser@0.3.2: + resolution: {integrity: sha512-32kYO6kJUuZzqte82t4M/gB6/+11WAuHiEnK7FreMo20xsCKPeFH5tDBU7iWxR7zeJpNnMXfJyXwne48D0hGrg==} + dependencies: + eslint-visitor-keys: 1.3.0 + lodash: 4.17.21 + yaml: 1.10.2 + dev: true + /yaml-eslint-parser@1.2.2: resolution: {integrity: sha512-pEwzfsKbTrB8G3xc/sN7aw1v6A6c/pKxLAkjclnAyo5g5qOh6eL9WGu0o3cSDQZKrTNk4KL4lQSwZW+nBkANEg==} engines: {node: ^14.17.0 || >=16.0.0} @@ -8937,6 +9110,11 @@ packages: yaml: 2.2.1 dev: true + /yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + dev: true + /yaml@2.2.1: resolution: {integrity: sha512-e0WHiYql7+9wr4cWMx3TVQrNwejKaEe7/rHNmQmqRjazfOP5W8PB6Jpebb5o6fIapbz9o9+2ipcaTM2ZwDI6lw==} engines: {node: '>= 14'} diff --git a/src/main.ts b/src/main.ts index e23cb91..36ba3b7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,6 +11,7 @@ import { naive } from './plugins/naive.plugin'; import App from './App.vue'; import router from './router'; +import { i18nPlugin } from './plugins/i18n.plugin'; registerSW(); @@ -18,6 +19,7 @@ const app = createApp(App); app.use(createPinia()); app.use(createHead()); +app.use(i18nPlugin); app.use(router); app.use(naive); app.use(plausible); diff --git a/src/pages/Home.page.vue b/src/pages/Home.page.vue index 01a7296..5c7c3c4 100644 --- a/src/pages/Home.page.vue +++ b/src/pages/Home.page.vue @@ -9,6 +9,7 @@ import { config } from '@/config'; const toolStore = useToolStore(); useHead({ title: 'IT Tools - Handy online tools for developers' }); +const { t } = useI18n(); </script> <template> @@ -48,7 +49,7 @@ useHead({ title: 'IT Tools - Handy online tools for developers' }); </transition> <div v-if="toolStore.newTools.length > 0"> - <n-h3>Newest tools</n-h3> + <n-h3>{{ t('home.categories.newestTools') }}</n-h3> <n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8"> <n-gi v-for="tool in toolStore.newTools" :key="tool.name"> <ToolCard :tool="tool" /> diff --git a/src/plugins/i18n.plugin.ts b/src/plugins/i18n.plugin.ts new file mode 100644 index 0000000..a1a1000 --- /dev/null +++ b/src/plugins/i18n.plugin.ts @@ -0,0 +1,50 @@ +import type { App } from 'vue'; +import { createI18n } from 'vue-i18n'; +import type { Locale } from 'vue-i18n'; + +const i18n = createI18n({ + legacy: false, + locale: '', + messages: {}, +}); + +const localesMap = Object.fromEntries( + Object.entries(import.meta.glob('../../locales/*.yml')) + .map(([path, loadLocale]) => [path.match(/([\w-]*)\.yml$/)?.[1], loadLocale]), +) as Record<Locale, () => Promise<{ default: Record<string, string> }>>; + +export const availableLocales = Object.keys(localesMap); + +const loadedLanguages: string[] = []; + +function setI18nLanguage(lang: Locale) { + i18n.global.locale.value = lang as any; + if (typeof document !== 'undefined') { + document.querySelector('html')?.setAttribute('lang', lang); + } + return lang; +} + +export async function loadLanguageAsync(lang: string): Promise<Locale> { + if (i18n.global.locale.value === lang) { + return setI18nLanguage(lang); + } + + if (loadedLanguages.includes(lang)) { + return setI18nLanguage(lang); + } + + const messages = await localesMap[lang](); + + i18n.global.setLocaleMessage(lang, messages.default); + loadedLanguages.push(lang); + + return setI18nLanguage(lang); +} + +export const i18nPlugin = { + install: (app: App) => { + app.use(i18n); + loadLanguageAsync('en'); + }, +}; diff --git a/tsconfig.app.json b/tsconfig.app.json index ceea561..8f5064d 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -9,6 +9,6 @@ "paths": { "@/*": ["./src/*"] }, - "types": ["naive-ui/volar", "unplugin-icons/types/vue"] + "types": ["naive-ui/volar", "unplugin-icons/types/vue", "@intlify/unplugin-vue-i18n/messages"] } } diff --git a/vite.config.ts b/vite.config.ts index bca5bf6..3858676 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,4 +1,5 @@ -import { fileURLToPath, URL } from 'url'; +import { URL, fileURLToPath } from 'node:url'; +import { resolve } from 'node:path'; import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; @@ -13,15 +14,23 @@ import Unocss from 'unocss/vite'; import { configDefaults } from 'vitest/config'; import Icons from 'unplugin-icons/vite'; import IconsResolver from 'unplugin-icons/resolver'; +import VueI18n from '@intlify/unplugin-vue-i18n/vite'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [ + VueI18n({ + runtimeOnly: true, + compositionOnly: true, + fullInstall: true, + include: [resolve(__dirname, 'locales/**'), resolve(__dirname, 'src/tools/*/locales/**')], + }), AutoImport({ imports: [ 'vue', 'vue-router', '@vueuse/core', + 'vue-i18n', { 'naive-ui': ['useDialog', 'useMessage', 'useNotification', 'useLoadingBar'], }, |