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