aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.eslintrc-auto-import.json1
-rw-r--r--auto-imports.d.ts2
-rw-r--r--locales/en.yml3
-rw-r--r--locales/fr.yml3
-rw-r--r--package.json2
-rw-r--r--pnpm-lock.yaml182
-rw-r--r--src/main.ts2
-rw-r--r--src/pages/Home.page.vue3
-rw-r--r--src/plugins/i18n.plugin.ts50
-rw-r--r--tsconfig.app.json2
-rw-r--r--vite.config.ts11
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'],
},