From e890aa843b77525a09bf6fb552d7fe7f4814aaa6 Mon Sep 17 00:00:00 2001 From: Alexander Kaschta <alexander.kaschta9@kit.edu> Date: Mon, 24 Mar 2025 02:14:31 +0100 Subject: [PATCH] ADD: i18n support --- package.json | 2 + pnpm-lock.yaml | 136 ++++++++++++++++++++++++++++++ src/components/GlobalSearch.vue | 2 +- src/components/LocaleSwitcher.vue | 36 ++++++++ src/components/TopBar.vue | 1 + src/locales/de.json | 4 + src/locales/en.json | 4 + src/main.ts | 8 ++ tsconfig.app.json | 3 +- vite.config.ts | 5 ++ 10 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 src/components/LocaleSwitcher.vue create mode 100644 src/locales/de.json create mode 100644 src/locales/en.json diff --git a/package.json b/package.json index b8b36c2f7..ea46c1ccc 100644 --- a/package.json +++ b/package.json @@ -37,12 +37,14 @@ "tailwind-merge": "^2.5.5", "vee-validate": "^4.14.7", "vue": "^3.5.12", + "vue-i18n": "^11.1.2", "vue-router": "^4.4.5", "vue-sonner": "^1.3.0", "zod": "^3.23.8" }, "devDependencies": { "@antfu/eslint-config": "^3.11.2", + "@intlify/unplugin-vue-i18n": "^6.0.5", "@nightwatch/vue": "^3.1.2", "@tsconfig/node22": "^22.0.0", "@types/jsdom": "^21.1.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a7ee7cbc1..339bc83d3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,9 @@ importers: vue: specifier: ^3.5.12 version: 3.5.13(typescript@5.6.3) + vue-i18n: + specifier: ^11.1.2 + version: 11.1.2(vue@3.5.13(typescript@5.6.3)) vue-router: specifier: ^4.4.5 version: 4.5.0(vue@3.5.13(typescript@5.6.3)) @@ -69,6 +72,9 @@ importers: '@antfu/eslint-config': specifier: ^3.11.2 version: 3.11.2(@typescript-eslint/utils@8.26.1(eslint@9.16.0(jiti@2.4.2))(typescript@5.6.3))(@unocss/eslint-plugin@66.1.0-beta.5(eslint@9.16.0(jiti@2.4.2))(typescript@5.6.3))(@vue/compiler-sfc@3.5.13)(eslint@9.16.0(jiti@2.4.2))(typescript@5.6.3)(vitest@2.1.8) + '@intlify/unplugin-vue-i18n': + specifier: ^6.0.5 + version: 6.0.5(@vue/compiler-dom@3.5.13)(eslint@9.16.0(jiti@2.4.2))(rollup@4.28.1)(typescript@5.6.3)(vue-i18n@11.1.2(vue@3.5.13(typescript@5.6.3)))(vue@3.5.13(typescript@5.6.3)) '@nightwatch/vue': specifier: ^3.1.2 version: 3.1.2(@types/node@22.10.1)(stylus@0.57.0)(vue@3.5.13(typescript@5.6.3)) @@ -992,6 +998,61 @@ packages: '@internationalized/number@3.6.0': resolution: {integrity: sha512-PtrRcJVy7nw++wn4W2OuePQQfTqDzfusSuY1QTtui4wa7r+rGVtR75pO8CyKvHvzyQYi3Q1uO5sY0AsB4e65Bw==} + '@intlify/bundle-utils@10.0.1': + resolution: {integrity: sha512-WkaXfSevtpgtUR4t8K2M6lbR7g03mtOxFeh+vXp5KExvPqS12ppaRj1QxzwRuRI5VUto54A22BjKoBMLyHILWQ==} + engines: {node: '>= 18'} + peerDependencies: + petite-vue-i18n: '*' + vue-i18n: '*' + peerDependenciesMeta: + petite-vue-i18n: + optional: true + vue-i18n: + optional: true + + '@intlify/core-base@11.1.2': + resolution: {integrity: sha512-nmG512G8QOABsserleechwHGZxzKSAlggGf9hQX0nltvSwyKNVuB/4o6iFeG2OnjXK253r8p8eSDOZf8PgFdWw==} + engines: {node: '>= 16'} + + '@intlify/message-compiler@11.1.2': + resolution: {integrity: sha512-T/xbNDzi+Yv0Qn2Dfz2CWCAJiwNgU5d95EhhAEf4YmOgjCKktpfpiUSmLcBvK1CtLpPQ85AMMQk/2NCcXnNj1g==} + engines: {node: '>= 16'} + + '@intlify/shared@11.1.2': + resolution: {integrity: sha512-dF2iMMy8P9uKVHV/20LA1ulFLL+MKSbfMiixSmn6fpwqzvix38OIc7ebgnFbBqElvghZCW9ACtzKTGKsTGTWGA==} + engines: {node: '>= 16'} + + '@intlify/unplugin-vue-i18n@6.0.5': + resolution: {integrity: sha512-0MKaYhLvM46Mtm+OArkK75ztmqaFfhIvnm5mg8XKqCPAKVAK98o+8tB6gUQFkKrF5PMYsNXvyMJCi40cRCDJbA==} + engines: {node: '>= 18'} + peerDependencies: + petite-vue-i18n: '*' + vue: ^3.2.25 + vue-i18n: '*' + peerDependenciesMeta: + petite-vue-i18n: + optional: true + vue-i18n: + optional: true + + '@intlify/vue-i18n-extensions@8.0.0': + resolution: {integrity: sha512-w0+70CvTmuqbskWfzeYhn0IXxllr6mU+IeM2MU0M+j9OW64jkrvqY+pYFWrUnIIC9bEdij3NICruicwd5EgUuQ==} + engines: {node: '>= 18'} + peerDependencies: + '@intlify/shared': ^9.0.0 || ^10.0.0 || ^11.0.0 + '@vue/compiler-dom': ^3.0.0 + vue: ^3.0.0 + vue-i18n: ^9.0.0 || ^10.0.0 || ^11.0.0 + peerDependenciesMeta: + '@intlify/shared': + optional: true + '@vue/compiler-dom': + optional: true + vue: + optional: true + vue-i18n: + optional: true + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -5563,6 +5624,12 @@ packages: peerDependencies: vue: ^3.4.37 + vue-i18n@11.1.2: + resolution: {integrity: sha512-MfdkdKGUHN+jkkaMT5Zbl4FpRmN7kfelJIwKoUpJ32ONIxdFhzxZiLTVaAXkAwvH3y9GmWpoiwjDqbPIkPIMFA==} + engines: {node: '>= 16'} + peerDependencies: + vue: ^3.0.0 + vue-metamorph@3.2.0: resolution: {integrity: sha512-dCWwwh7OngblFqUvD/pilS++TVnTHY1Nft2Tp02mHYv8ZrxFHN3NDMOKOoIg8lo7WR3TKxi3+bD/rmnN9tS2yw==} hasBin: true @@ -6537,6 +6604,68 @@ snapshots: dependencies: '@swc/helpers': 0.5.15 + '@intlify/bundle-utils@10.0.1(vue-i18n@11.1.2(vue@3.5.13(typescript@5.6.3)))': + dependencies: + '@intlify/message-compiler': 11.1.2 + '@intlify/shared': 11.1.2 + acorn: 8.14.0 + escodegen: 2.1.0 + estree-walker: 2.0.2 + jsonc-eslint-parser: 2.4.0 + mlly: 1.7.4 + source-map-js: 1.2.1 + yaml-eslint-parser: 1.2.3 + optionalDependencies: + vue-i18n: 11.1.2(vue@3.5.13(typescript@5.6.3)) + + '@intlify/core-base@11.1.2': + dependencies: + '@intlify/message-compiler': 11.1.2 + '@intlify/shared': 11.1.2 + + '@intlify/message-compiler@11.1.2': + dependencies: + '@intlify/shared': 11.1.2 + source-map-js: 1.2.1 + + '@intlify/shared@11.1.2': {} + + '@intlify/unplugin-vue-i18n@6.0.5(@vue/compiler-dom@3.5.13)(eslint@9.16.0(jiti@2.4.2))(rollup@4.28.1)(typescript@5.6.3)(vue-i18n@11.1.2(vue@3.5.13(typescript@5.6.3)))(vue@3.5.13(typescript@5.6.3))': + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.16.0(jiti@2.4.2)) + '@intlify/bundle-utils': 10.0.1(vue-i18n@11.1.2(vue@3.5.13(typescript@5.6.3))) + '@intlify/shared': 11.1.2 + '@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.1.2)(@vue/compiler-dom@3.5.13)(vue-i18n@11.1.2(vue@3.5.13(typescript@5.6.3)))(vue@3.5.13(typescript@5.6.3)) + '@rollup/pluginutils': 5.1.3(rollup@4.28.1) + '@typescript-eslint/scope-manager': 8.26.1 + '@typescript-eslint/typescript-estree': 8.26.1(typescript@5.6.3) + debug: 4.4.0 + fast-glob: 3.3.3 + js-yaml: 4.1.0 + json5: 2.2.3 + pathe: 1.1.2 + picocolors: 1.1.1 + source-map-js: 1.2.1 + unplugin: 1.16.0 + vue: 3.5.13(typescript@5.6.3) + optionalDependencies: + vue-i18n: 11.1.2(vue@3.5.13(typescript@5.6.3)) + transitivePeerDependencies: + - '@vue/compiler-dom' + - eslint + - rollup + - supports-color + - typescript + + '@intlify/vue-i18n-extensions@8.0.0(@intlify/shared@11.1.2)(@vue/compiler-dom@3.5.13)(vue-i18n@11.1.2(vue@3.5.13(typescript@5.6.3)))(vue@3.5.13(typescript@5.6.3))': + dependencies: + '@babel/parser': 7.26.10 + optionalDependencies: + '@intlify/shared': 11.1.2 + '@vue/compiler-dom': 3.5.13 + vue: 3.5.13(typescript@5.6.3) + vue-i18n: 11.1.2(vue@3.5.13(typescript@5.6.3)) + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -11804,6 +11933,13 @@ snapshots: dependencies: vue: 3.5.13(typescript@5.6.3) + vue-i18n@11.1.2(vue@3.5.13(typescript@5.6.3)): + dependencies: + '@intlify/core-base': 11.1.2 + '@intlify/shared': 11.1.2 + '@vue/devtools-api': 6.6.4 + vue: 3.5.13(typescript@5.6.3) + vue-metamorph@3.2.0(eslint@9.16.0(jiti@2.4.2)): dependencies: '@babel/parser': 8.0.0-alpha.12 diff --git a/src/components/GlobalSearch.vue b/src/components/GlobalSearch.vue index 216a9ed0b..7ba683489 100644 --- a/src/components/GlobalSearch.vue +++ b/src/components/GlobalSearch.vue @@ -1,3 +1,3 @@ <template> - <Input type="search" placeholder="Search..." /> + <Input type="search" :placeholder="$t('search')" /> </template> diff --git a/src/components/LocaleSwitcher.vue b/src/components/LocaleSwitcher.vue new file mode 100644 index 000000000..2ff39d02a --- /dev/null +++ b/src/components/LocaleSwitcher.vue @@ -0,0 +1,36 @@ +<script setup lang="ts"> +const languages = { + de: { + full_name: 'Deutsch', + flag: '🇩🇪', + }, + en: { + full_name: 'English', + flag: '🇬🇧', + }, +} +</script> + +<template> + <DropdownMenu> + <DropdownMenuTrigger as-child> + <Button variant="outline"> + <!-- TODO: Replace text with icon --> + Locale + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + <DropdownMenuRadioGroup v-model="$i18n.locale"> + <DropdownMenuRadioItem v-for="locale in $i18n.availableLocales" :key="`locale-${locale}`" :value="locale"> + <!-- TODO: Change check mark of radio group --> + <!-- Improve flag loading --> + {{ languages[locale].flag }} {{ languages[locale].full_name }} + </DropdownMenuRadioItem> + </DropdownMenuRadioGroup> + </DropdownMenuContent> + </DropdownMenu> +</template> + +<style scoped> + +</style> diff --git a/src/components/TopBar.vue b/src/components/TopBar.vue index 68107ea83..eaaba41fc 100644 --- a/src/components/TopBar.vue +++ b/src/components/TopBar.vue @@ -4,6 +4,7 @@ <GlobalSearch /> <UserNav /> <DarkModeToggle /> + <LocaleSwitcher /> </div> </div> </template> diff --git a/src/locales/de.json b/src/locales/de.json new file mode 100644 index 000000000..3ce86225d --- /dev/null +++ b/src/locales/de.json @@ -0,0 +1,4 @@ +{ + "hello": "Hallo", + "search": "Suche" +} \ No newline at end of file diff --git a/src/locales/en.json b/src/locales/en.json new file mode 100644 index 000000000..dcf636df8 --- /dev/null +++ b/src/locales/en.json @@ -0,0 +1,4 @@ +{ + "hello": "Hello", + "search": "Search" +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 7e242198d..64591a59a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,8 @@ +import messages from '@intlify/unplugin-vue-i18n/messages' import { createPinia } from 'pinia' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' import { createApp } from 'vue' +import { createI18n } from 'vue-i18n' import App from './App.vue' import router from './router' @@ -14,7 +16,13 @@ import '@unocss/reset/tailwind.css' const app = createApp(App) const pinia = createPinia() pinia.use(piniaPluginPersistedstate) +const i18n = createI18n({ + locale: 'de', + fallbackLocale: ['en', 'de'], + messages, +}) app.use(pinia) +app.use(i18n) app.use(router) app.mount('#app') diff --git a/tsconfig.app.json b/tsconfig.app.json index 3920bb32c..579c6a1d6 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -7,7 +7,8 @@ "baseUrl": ".", "paths": { "@/*": ["./src/*"] - } + }, + "types": ["@intlify/unplugin-vue-i18n/messages"] }, "include": ["env.d.ts", "src/**/*", "src/**/*.vue", "auto-imports.d.ts", "components.d.ts"], "exclude": ["src/**/__tests__/*"] diff --git a/vite.config.ts b/vite.config.ts index 97af01e90..cbbadd54f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,5 +1,7 @@ +import path from 'node:path' import { fileURLToPath, URL } from 'node:url' +import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite' import vue from '@vitejs/plugin-vue' import colors from 'picocolors' import UnoCSS from 'unocss/vite' @@ -36,6 +38,9 @@ export default defineConfig({ silent: true, disablePassLogs: true, }), + VueI18nPlugin({ + include: [path.resolve(__dirname, './src/locales/**')], + }), { name: 'nextvs-startup-logo', configureServer: (server) => { -- GitLab