Browse Source

Add internationalization (i18n) and API clients

**Internationalization:**
- Installed vue-i18n with Russian (default) and English
- Added language switcher (RU/EN) in both layouts
- Translated all navigation and UI strings
- Locale persisted in localStorage

**Navigation Updates:**
- Reordered menu: Dashboard → Devices → Organizations → Users
- Applied translations to all menu items

**API Clients:**
- organizations.js - CRUD operations for organizations
- devices.js - superadmin and client endpoints
- users.js - superadmin and client endpoints

**Documentation:**
- Added Frontend section with i18n implementation details
- Added Host Monitoring (TODO) section with metrics plan
- Documented API endpoint structure for host monitoring
- Listed monitoring solution options (custom daemon, Prometheus, Netdata)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
root 1 month ago
parent
commit
6b71112c81

+ 93 - 0
BACKEND_NOTES.md

@@ -567,3 +567,96 @@ GROUP BY organization_id;
 **Repository:** https://h2.e-bash.ru/root/mybeacon-backend.git
 **Repository:** https://h2.e-bash.ru/root/mybeacon-backend.git
 **Branch:** master
 **Branch:** master
 **Latest commit:** Implement complete MyBeacon backend MVP
 **Latest commit:** Implement complete MyBeacon backend MVP
+
+## Frontend
+
+### Technology Stack
+
+- **Framework:** Vue 3 (Composition API)
+- **Build Tool:** Vite 5
+- **State Management:** Pinia
+- **Routing:** Vue Router 4
+- **HTTP Client:** Axios
+- **Internationalization:** vue-i18n 9
+
+### Internationalization (i18n)
+
+**Supported Languages:**
+- Russian (ru) - default
+- English (en)
+
+**Implementation:**
+- Language selector in sidebar (RU/EN buttons)
+- Locale stored in localStorage
+- All UI strings translated in `/src/i18n/index.js`
+- Usage: `{{ $t('nav.dashboard') }}`
+
+**Adding new translations:**
+```javascript
+// src/i18n/index.js
+const messages = {
+  en: {
+    mySection: {
+      myKey: 'My English Text'
+    }
+  },
+  ru: {
+    mySection: {
+      myKey: 'Мой русский текст'
+    }
+  }
+}
+```
+
+### Navigation Structure
+
+**Superadmin:**
+1. Dashboard
+2. Devices  
+3. Organizations
+4. Users
+
+**Client (owner/admin):**
+1. Dashboard
+2. Devices
+3. Users (owner/admin only)
+
+### Host Monitoring (TODO)
+
+Dashboard должен отображать метрики хоста в реальном времени:
+
+**Метрики:**
+- CPU Usage (%)
+- Memory Usage (GB/%)
+- Network Traffic (rx/tx)
+- Load Average (1m, 5m, 15m)
+- Disk I/O (read/write)
+- Disk Usage (%)
+
+**Варианты реализации:**
+1. **Custom daemon** - написать свой демон на Python/Go для сбора метрик
+2. **Node Exporter** (Prometheus) - использовать готовое решение
+3. **Netdata** - полноценный мониторинг с API
+4. **collectd** - легковесный сборщик метрик
+
+**API Endpoint (планируется):**
+```
+GET /api/v1/monitoring/host
+{
+  "cpu": {"usage": 45.2, "cores": 4},
+  "memory": {"total": 16384, "used": 8192, "percent": 50},
+  "network": {"rx_bytes": 1024000, "tx_bytes": 512000},
+  "load": {"1m": 0.5, "5m": 0.7, "15m": 0.6},
+  "disk": {
+    "io": {"read_bytes": 2048000, "write_bytes": 1024000},
+    "usage": {"total": 512000, "used": 256000, "percent": 50}
+  },
+  "timestamp": "2025-12-28T16:00:00Z"
+}
+```
+
+**Frontend компонент:**
+- Real-time графики (Chart.js / Apache ECharts)
+- WebSocket для live updates
+- Алерты при превышении порогов
+

+ 65 - 0
frontend/package-lock.json

@@ -11,6 +11,7 @@
         "axios": "^1.6.5",
         "axios": "^1.6.5",
         "pinia": "^2.1.7",
         "pinia": "^2.1.7",
         "vue": "^3.4.15",
         "vue": "^3.4.15",
+        "vue-i18n": "^9.14.5",
         "vue-router": "^4.2.5"
         "vue-router": "^4.2.5"
       },
       },
       "devDependencies": {
       "devDependencies": {
@@ -455,6 +456,50 @@
         "node": ">=12"
         "node": ">=12"
       }
       }
     },
     },
+    "node_modules/@intlify/core-base": {
+      "version": "9.14.5",
+      "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.14.5.tgz",
+      "integrity": "sha512-5ah5FqZG4pOoHjkvs8mjtv+gPKYU0zCISaYNjBNNqYiaITxW8ZtVih3GS/oTOqN8d9/mDLyrjD46GBApNxmlsA==",
+      "license": "MIT",
+      "dependencies": {
+        "@intlify/message-compiler": "9.14.5",
+        "@intlify/shared": "9.14.5"
+      },
+      "engines": {
+        "node": ">= 16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/kazupon"
+      }
+    },
+    "node_modules/@intlify/message-compiler": {
+      "version": "9.14.5",
+      "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.14.5.tgz",
+      "integrity": "sha512-IHzgEu61/YIpQV5Pc3aRWScDcnFKWvQA9kigcINcCBXN8mbW+vk9SK+lDxA6STzKQsVJxUPg9ACC52pKKo3SVQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@intlify/shared": "9.14.5",
+        "source-map-js": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/kazupon"
+      }
+    },
+    "node_modules/@intlify/shared": {
+      "version": "9.14.5",
+      "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.14.5.tgz",
+      "integrity": "sha512-9gB+E53BYuAEMhbCAxVgG38EZrk59sxBtv3jSizNL2hEWlgjBjAw1AwpLHtNaeda12pe6W20OGEa0TwuMSRbyQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/kazupon"
+      }
+    },
     "node_modules/@jridgewell/sourcemap-codec": {
     "node_modules/@jridgewell/sourcemap-codec": {
       "version": "1.5.5",
       "version": "1.5.5",
       "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
       "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
@@ -1494,6 +1539,26 @@
         }
         }
       }
       }
     },
     },
+    "node_modules/vue-i18n": {
+      "version": "9.14.5",
+      "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.14.5.tgz",
+      "integrity": "sha512-0jQ9Em3ymWngyiIkj0+c/k7WgaPO+TNzjKSNq9BvBQaKJECqn9cd9fL4tkDhB5G1QBskGl9YxxbDAhgbFtpe2g==",
+      "license": "MIT",
+      "dependencies": {
+        "@intlify/core-base": "9.14.5",
+        "@intlify/shared": "9.14.5",
+        "@vue/devtools-api": "^6.5.0"
+      },
+      "engines": {
+        "node": ">= 16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/kazupon"
+      },
+      "peerDependencies": {
+        "vue": "^3.0.0"
+      }
+    },
     "node_modules/vue-router": {
     "node_modules/vue-router": {
       "version": "4.6.4",
       "version": "4.6.4",
       "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
       "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",

+ 4 - 3
frontend/package.json

@@ -9,10 +9,11 @@
     "preview": "vite preview"
     "preview": "vite preview"
   },
   },
   "dependencies": {
   "dependencies": {
-    "vue": "^3.4.15",
-    "vue-router": "^4.2.5",
+    "axios": "^1.6.5",
     "pinia": "^2.1.7",
     "pinia": "^2.1.7",
-    "axios": "^1.6.5"
+    "vue": "^3.4.15",
+    "vue-i18n": "^9.14.5",
+    "vue-router": "^4.2.5"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@vitejs/plugin-vue": "^5.0.3",
     "@vitejs/plugin-vue": "^5.0.3",

+ 35 - 0
frontend/src/api/devices.js

@@ -0,0 +1,35 @@
+import client from './client'
+
+export default {
+  // Superadmin endpoints
+  async getAllSuperadmin() {
+    const { data } = await client.get('/superadmin/devices')
+    return data
+  },
+
+  async getByIdSuperadmin(id) {
+    const { data } = await client.get(`/superadmin/devices/${id}`)
+    return data
+  },
+
+  async updateSuperadmin(id, deviceData) {
+    const { data } = await client.patch(`/superadmin/devices/${id}`, deviceData)
+    return data
+  },
+
+  async deleteSuperadmin(id) {
+    const { data } = await client.delete(`/superadmin/devices/${id}`)
+    return data
+  },
+
+  // Client endpoints
+  async getAllClient() {
+    const { data } = await client.get('/client/devices')
+    return data
+  },
+
+  async getByIdClient(id) {
+    const { data } = await client.get(`/client/devices/${id}`)
+    return data
+  }
+}

+ 28 - 0
frontend/src/api/organizations.js

@@ -0,0 +1,28 @@
+import client from './client'
+
+export default {
+  async getAll() {
+    const { data } = await client.get('/superadmin/organizations')
+    return data
+  },
+
+  async getById(id) {
+    const { data } = await client.get(`/superadmin/organizations/${id}`)
+    return data
+  },
+
+  async create(organizationData) {
+    const { data } = await client.post('/superadmin/organizations', organizationData)
+    return data
+  },
+
+  async update(id, organizationData) {
+    const { data } = await client.patch(`/superadmin/organizations/${id}`, organizationData)
+    return data
+  },
+
+  async delete(id) {
+    const { data } = await client.delete(`/superadmin/organizations/${id}`)
+    return data
+  }
+}

+ 65 - 0
frontend/src/api/users.js

@@ -0,0 +1,65 @@
+import client from './client'
+
+export default {
+  // Superadmin endpoints
+  async getAllSuperadmin() {
+    const { data } = await client.get('/superadmin/users')
+    return data
+  },
+
+  async getByIdSuperadmin(id) {
+    const { data } = await client.get(`/superadmin/users/${id}`)
+    return data
+  },
+
+  async createSuperadmin(userData) {
+    const { data } = await client.post('/superadmin/users', userData)
+    return data
+  },
+
+  async updateSuperadmin(id, userData) {
+    const { data } = await client.patch(`/superadmin/users/${id}`, userData)
+    return data
+  },
+
+  async deleteSuperadmin(id) {
+    const { data} = await client.delete(`/superadmin/users/${id}`)
+    return data
+  },
+
+  async changePasswordSuperadmin(id, passwordData) {
+    const { data } = await client.post(`/superadmin/users/${id}/change-password`, passwordData)
+    return data
+  },
+
+  // Client endpoints
+  async getAllClient() {
+    const { data } = await client.get('/client/users')
+    return data
+  },
+
+  async getByIdClient(id) {
+    const { data } = await client.get(`/client/users/${id}`)
+    return data
+  },
+
+  async createClient(userData) {
+    const { data } = await client.post('/client/users', userData)
+    return data
+  },
+
+  async updateClient(id, userData) {
+    const { data } = await client.patch(`/client/users/${id}`, userData)
+    return data
+  },
+
+  async deleteClient(id) {
+    const { data } = await client.delete(`/client/users/${id}`)
+    return data
+  },
+
+  async changePasswordClient(id, passwordData) {
+    const { data } = await client.post(`/client/users/${id}/change-password`, passwordData)
+    return data
+  }
+}

+ 197 - 0
frontend/src/i18n/index.js

@@ -0,0 +1,197 @@
+import { createI18n } from 'vue-i18n'
+
+const messages = {
+  en: {
+    nav: {
+      dashboard: 'Dashboard',
+      devices: 'Devices',
+      organizations: 'Organizations',
+      users: 'Users',
+      logout: 'Logout'
+    },
+    common: {
+      add: 'Add',
+      edit: 'Edit',
+      delete: 'Delete',
+      save: 'Save',
+      cancel: 'Cancel',
+      search: 'Search',
+      actions: 'Actions',
+      status: 'Status',
+      loading: 'Loading...',
+      confirm: 'Confirm',
+      yes: 'Yes',
+      no: 'No'
+    },
+    auth: {
+      login: 'Sign in',
+      email: 'Email',
+      password: 'Password',
+      signIn: 'Sign in',
+      signingIn: 'Signing in...',
+      testCredentials: 'Test Credentials',
+      invalidCredentials: 'Invalid email or password'
+    },
+    dashboard: {
+      title: 'Dashboard',
+      systemOverview: 'System overview and statistics',
+      organizationOverview: 'Organization overview',
+      quickActions: 'Quick Actions',
+      eventsToday: 'Events Today'
+    },
+    organizations: {
+      title: 'Organizations',
+      manage: 'Manage all organizations',
+      add: 'Add Organization',
+      name: 'Name',
+      contactEmail: 'Contact Email',
+      contactPhone: 'Contact Phone',
+      wifiEnabled: 'WiFi Enabled',
+      bleEnabled: 'BLE Enabled',
+      createdAt: 'Created',
+      manageAction: 'Manage Organizations',
+      manageDesc: 'Create and configure organizations'
+    },
+    devices: {
+      title: 'Devices',
+      manage: 'Manage all devices',
+      yourDevices: 'Your organization devices',
+      add: 'Add Device',
+      simpleId: 'ID',
+      macAddress: 'MAC Address',
+      organization: 'Organization',
+      lastSeen: 'Last Seen',
+      manageAction: 'Manage Devices',
+      manageDesc: 'View and assign devices',
+      online: 'Online',
+      offline: 'Offline'
+    },
+    users: {
+      title: 'Users',
+      manage: 'Manage all users',
+      manageOrg: 'Manage organization users',
+      add: 'Add User',
+      fullName: 'Full Name',
+      role: 'Role',
+      manageAction: 'Manage Users',
+      manageDesc: 'View all system users',
+      roles: {
+        superadmin: 'Superadmin',
+        owner: 'Owner',
+        admin: 'Admin',
+        manager: 'Manager',
+        operator: 'Operator',
+        viewer: 'Viewer'
+      }
+    },
+    host: {
+      monitoring: 'Host Monitoring',
+      cpu: 'CPU Usage',
+      memory: 'Memory Usage',
+      network: 'Network',
+      loadAverage: 'Load Average',
+      io: 'I/O'
+    }
+  },
+  ru: {
+    nav: {
+      dashboard: 'Панель управления',
+      devices: 'Устройства',
+      organizations: 'Организации',
+      users: 'Пользователи',
+      logout: 'Выйти'
+    },
+    common: {
+      add: 'Добавить',
+      edit: 'Редактировать',
+      delete: 'Удалить',
+      save: 'Сохранить',
+      cancel: 'Отмена',
+      search: 'Поиск',
+      actions: 'Действия',
+      status: 'Статус',
+      loading: 'Загрузка...',
+      confirm: 'Подтвердить',
+      yes: 'Да',
+      no: 'Нет'
+    },
+    auth: {
+      login: 'Вход',
+      email: 'Email',
+      password: 'Пароль',
+      signIn: 'Войти',
+      signingIn: 'Вход...',
+      testCredentials: 'Тестовые учетные данные',
+      invalidCredentials: 'Неверный email или пароль'
+    },
+    dashboard: {
+      title: 'Панель управления',
+      systemOverview: 'Обзор системы и статистика',
+      organizationOverview: 'Обзор организации',
+      quickActions: 'Быстрые действия',
+      eventsToday: 'События сегодня'
+    },
+    organizations: {
+      title: 'Организации',
+      manage: 'Управление всеми организациями',
+      add: 'Добавить организацию',
+      name: 'Название',
+      contactEmail: 'Email контакта',
+      contactPhone: 'Телефон контакта',
+      wifiEnabled: 'WiFi включен',
+      bleEnabled: 'BLE включен',
+      createdAt: 'Создано',
+      manageAction: 'Управление организациями',
+      manageDesc: 'Создание и настройка организаций'
+    },
+    devices: {
+      title: 'Устройства',
+      manage: 'Управление всеми устройствами',
+      yourDevices: 'Устройства вашей организации',
+      add: 'Добавить устройство',
+      simpleId: 'ID',
+      macAddress: 'MAC адрес',
+      organization: 'Организация',
+      lastSeen: 'Последняя активность',
+      manageAction: 'Управление устройствами',
+      manageDesc: 'Просмотр и назначение устройств',
+      online: 'Онлайн',
+      offline: 'Оффлайн'
+    },
+    users: {
+      title: 'Пользователи',
+      manage: 'Управление всеми пользователями',
+      manageOrg: 'Управление пользователями организации',
+      add: 'Добавить пользователя',
+      fullName: 'Полное имя',
+      role: 'Роль',
+      manageAction: 'Управление пользователями',
+      manageDesc: 'Просмотр всех пользователей системы',
+      roles: {
+        superadmin: 'Суперадмин',
+        owner: 'Владелец',
+        admin: 'Администратор',
+        manager: 'Менеджер',
+        operator: 'Оператор',
+        viewer: 'Наблюдатель'
+      }
+    },
+    host: {
+      monitoring: 'Мониторинг хоста',
+      cpu: 'Загрузка CPU',
+      memory: 'Использование памяти',
+      network: 'Сеть',
+      loadAverage: 'Средняя нагрузка',
+      io: 'Ввод/вывод'
+    }
+  }
+}
+
+const i18n = createI18n({
+  legacy: false,
+  locale: localStorage.getItem('locale') || 'ru',
+  fallbackLocale: 'en',
+  messages
+})
+
+export default i18n

+ 45 - 4
frontend/src/layouts/ClientLayout.vue

@@ -8,17 +8,21 @@
 
 
       <nav class="sidebar-nav">
       <nav class="sidebar-nav">
         <router-link to="/client" class="nav-item">
         <router-link to="/client" class="nav-item">
-          <span>📊</span> Dashboard
+          <span>📊</span> {{ $t('nav.dashboard') }}
         </router-link>
         </router-link>
         <router-link to="/client/devices" class="nav-item">
         <router-link to="/client/devices" class="nav-item">
-          <span>📡</span> Devices
+          <span>📡</span> {{ $t('nav.devices') }}
         </router-link>
         </router-link>
         <router-link to="/client/users" class="nav-item" v-if="canManageUsers">
         <router-link to="/client/users" class="nav-item" v-if="canManageUsers">
-          <span>👥</span> Users
+          <span>👥</span> {{ $t('nav.users') }}
         </router-link>
         </router-link>
       </nav>
       </nav>
 
 
       <div class="sidebar-footer">
       <div class="sidebar-footer">
+        <div class="language-switcher">
+          <button @click="switchLanguage('ru')" :class="{ active: currentLocale === 'ru' }">RU</button>
+          <button @click="switchLanguage('en')" :class="{ active: currentLocale === 'en' }">EN</button>
+        </div>
         <div class="user-info">
         <div class="user-info">
           <div class="user-avatar">{{ user?.email?.[0]?.toUpperCase() }}</div>
           <div class="user-avatar">{{ user?.email?.[0]?.toUpperCase() }}</div>
           <div class="user-details">
           <div class="user-details">
@@ -26,7 +30,7 @@
             <div class="user-email">{{ user?.email }}</div>
             <div class="user-email">{{ user?.email }}</div>
           </div>
           </div>
         </div>
         </div>
-        <button @click="handleLogout" class="logout-button">Logout</button>
+        <button @click="handleLogout" class="logout-button">{{ $t('nav.logout') }}</button>
       </div>
       </div>
     </aside>
     </aside>
 
 
@@ -39,17 +43,25 @@
 <script setup>
 <script setup>
 import { computed } from 'vue'
 import { computed } from 'vue'
 import { useRouter } from 'vue-router'
 import { useRouter } from 'vue-router'
+import { useI18n } from 'vue-i18n'
 import { useAuthStore } from '@/stores/auth'
 import { useAuthStore } from '@/stores/auth'
 
 
 const router = useRouter()
 const router = useRouter()
 const authStore = useAuthStore()
 const authStore = useAuthStore()
+const { locale } = useI18n()
 
 
 const user = computed(() => authStore.user)
 const user = computed(() => authStore.user)
+const currentLocale = computed(() => locale.value)
 const canManageUsers = computed(() => {
 const canManageUsers = computed(() => {
   const role = user.value?.role
   const role = user.value?.role
   return role === 'owner' || role === 'admin'
   return role === 'owner' || role === 'admin'
 })
 })
 
 
+function switchLanguage(lang) {
+  locale.value = lang
+  localStorage.setItem('locale', lang)
+}
+
 async function handleLogout() {
 async function handleLogout() {
   await authStore.logout()
   await authStore.logout()
   router.push('/login')
   router.push('/login')
@@ -123,6 +135,35 @@ async function handleLogout() {
   border-top: 1px solid rgba(255, 255, 255, 0.1);
   border-top: 1px solid rgba(255, 255, 255, 0.1);
 }
 }
 
 
+.language-switcher {
+  display: flex;
+  gap: 8px;
+  margin-bottom: 12px;
+}
+
+.language-switcher button {
+  flex: 1;
+  padding: 6px;
+  background: rgba(255, 255, 255, 0.1);
+  border: 1px solid rgba(255, 255, 255, 0.2);
+  border-radius: 6px;
+  color: #cbd5e0;
+  font-size: 12px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+
+.language-switcher button:hover {
+  background: rgba(255, 255, 255, 0.15);
+}
+
+.language-switcher button.active {
+  background: #48bb78;
+  border-color: #48bb78;
+  color: white;
+}
+
 .user-info {
 .user-info {
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;

+ 48 - 7
frontend/src/layouts/SuperadminLayout.vue

@@ -8,20 +8,24 @@
 
 
       <nav class="sidebar-nav">
       <nav class="sidebar-nav">
         <router-link to="/superadmin" class="nav-item">
         <router-link to="/superadmin" class="nav-item">
-          <span>📊</span> Dashboard
-        </router-link>
-        <router-link to="/superadmin/organizations" class="nav-item">
-          <span>🏢</span> Organizations
+          <span>📊</span> {{ $t('nav.dashboard') }}
         </router-link>
         </router-link>
         <router-link to="/superadmin/devices" class="nav-item">
         <router-link to="/superadmin/devices" class="nav-item">
-          <span>📡</span> Devices
+          <span>📡</span> {{ $t('nav.devices') }}
+        </router-link>
+        <router-link to="/superadmin/organizations" class="nav-item">
+          <span>🏢</span> {{ $t('nav.organizations') }}
         </router-link>
         </router-link>
         <router-link to="/superadmin/users" class="nav-item">
         <router-link to="/superadmin/users" class="nav-item">
-          <span>👥</span> Users
+          <span>👥</span> {{ $t('nav.users') }}
         </router-link>
         </router-link>
       </nav>
       </nav>
 
 
       <div class="sidebar-footer">
       <div class="sidebar-footer">
+        <div class="language-switcher">
+          <button @click="switchLanguage('ru')" :class="{ active: currentLocale === 'ru' }">RU</button>
+          <button @click="switchLanguage('en')" :class="{ active: currentLocale === 'en' }">EN</button>
+        </div>
         <div class="user-info">
         <div class="user-info">
           <div class="user-avatar">{{ user?.email?.[0]?.toUpperCase() }}</div>
           <div class="user-avatar">{{ user?.email?.[0]?.toUpperCase() }}</div>
           <div class="user-details">
           <div class="user-details">
@@ -29,7 +33,7 @@
             <div class="user-email">{{ user?.email }}</div>
             <div class="user-email">{{ user?.email }}</div>
           </div>
           </div>
         </div>
         </div>
-        <button @click="handleLogout" class="logout-button">Logout</button>
+        <button @click="handleLogout" class="logout-button">{{ $t('nav.logout') }}</button>
       </div>
       </div>
     </aside>
     </aside>
 
 
@@ -42,12 +46,20 @@
 <script setup>
 <script setup>
 import { computed } from 'vue'
 import { computed } from 'vue'
 import { useRouter } from 'vue-router'
 import { useRouter } from 'vue-router'
+import { useI18n } from 'vue-i18n'
 import { useAuthStore } from '@/stores/auth'
 import { useAuthStore } from '@/stores/auth'
 
 
 const router = useRouter()
 const router = useRouter()
 const authStore = useAuthStore()
 const authStore = useAuthStore()
+const { locale } = useI18n()
 
 
 const user = computed(() => authStore.user)
 const user = computed(() => authStore.user)
+const currentLocale = computed(() => locale.value)
+
+function switchLanguage(lang) {
+  locale.value = lang
+  localStorage.setItem('locale', lang)
+}
 
 
 async function handleLogout() {
 async function handleLogout() {
   await authStore.logout()
   await authStore.logout()
@@ -121,6 +133,35 @@ async function handleLogout() {
   border-top: 1px solid rgba(255, 255, 255, 0.1);
   border-top: 1px solid rgba(255, 255, 255, 0.1);
 }
 }
 
 
+.language-switcher {
+  display: flex;
+  gap: 8px;
+  margin-bottom: 12px;
+}
+
+.language-switcher button {
+  flex: 1;
+  padding: 6px;
+  background: rgba(255, 255, 255, 0.1);
+  border: 1px solid rgba(255, 255, 255, 0.2);
+  border-radius: 6px;
+  color: #cbd5e0;
+  font-size: 12px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+
+.language-switcher button:hover {
+  background: rgba(255, 255, 255, 0.15);
+}
+
+.language-switcher button.active {
+  background: #667eea;
+  border-color: #667eea;
+  color: white;
+}
+
 .user-info {
 .user-info {
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;

+ 2 - 0
frontend/src/main.js

@@ -2,10 +2,12 @@ import { createApp } from 'vue'
 import { createPinia } from 'pinia'
 import { createPinia } from 'pinia'
 import App from './App.vue'
 import App from './App.vue'
 import router from './router'
 import router from './router'
+import i18n from './i18n'
 
 
 const app = createApp(App)
 const app = createApp(App)
 
 
 app.use(createPinia())
 app.use(createPinia())
 app.use(router)
 app.use(router)
+app.use(i18n)
 
 
 app.mount('#app')
 app.mount('#app')