added i18n, english and french for now

This commit is contained in:
2026-01-03 23:01:15 +01:00
parent f71d1ff9ac
commit 53b854d03a
19 changed files with 715 additions and 207 deletions

View File

@@ -10,11 +10,13 @@
"tauri": "tauri"
},
"dependencies": {
"@fluent/bundle": "^0.19.1",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-http": "~2",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-store": "~2",
"@tauri-apps/plugin-websocket": "~2",
"fluent-vue": "^3.8.1",
"vue": "^3.5.13",
"vue-router": "^4.6.4"
},
@@ -22,6 +24,7 @@
"@tauri-apps/cli": "^2",
"@vitejs/plugin-vue": "^5.2.1",
"typescript": "~5.6.2",
"unplugin-fluent-vue": "^1.4.1",
"vite": "^6.0.3",
"vue-tsc": "^2.1.10"
}

View File

@@ -53,3 +53,14 @@ export async function getLastRoom(): Promise<string | null> {
const s = await getStore()
return (await s.get<string>('last_room_uuid')) ?? null
}
export async function saveLocalePreference(locale: string) {
const s = await getStore()
await s.set('language', locale)
await s.save()
}
export async function getLocalePreference(): Promise<string | null> {
const s = await getStore()
return (await s.get<string>('language')) ?? null
}

View File

@@ -4,7 +4,7 @@
<div v-if="uuid === 'none'" class="no-room">
<div class="empty-state">
<i class="fa-solid fa-comments"></i>
<p>Select a room to start talking</p>
<p>{{ $t('chat-no-room') }}</p>
</div>
</div>
@@ -15,7 +15,7 @@
<div class="input-container">
<button v-if="isOwner && !currentRoom?.global" class="invite-btn" @click="showInviteModal = true"
title="Invite people">
:title="$t('chat-invite-title')">
<i class="fa-solid fa-users"></i>
</button>

View File

@@ -1,22 +1,22 @@
<template>
<div class="backdrop" @click.self="emit('close')">
<form class="modal" @submit.prevent="submit">
<h2>Create room</h2>
<h2>{{ $t('chat-create-title') }}</h2>
<input v-model="name" placeholder="Room name" autofocus />
<input v-model="name" :placeholder="$t('chat-create-name-placeholder')" autofocus />
<label class="checkbox">
<input type="checkbox" v-model="global" />
<span>Global room</span>
<span>{{ $t('chat-create-global') }}</span>
</label>
<div class="actions">
<button type="button" @click="emit('close')" class="secondary">
Cancel
{{ $t('shared-cancel') }}
</button>
<button type="submit">
Create
{{ $t('chat-create-submit') }}
</button>
</div>
</form>

View File

@@ -1,27 +1,27 @@
<template>
<div class="backdrop" @click.self="emit('close')">
<form class="modal" @submit.prevent="submit">
<h2>Invite People</h2>
<h2>{{ $t('chat-invite-title') }}</h2>
<p v-if="errorMessage" class="error-message">{{ errorMessage }}</p>
<div class="input-group">
<label>Receiver username</label>
<label>{{ $t('chat-invite-receiver') }}</label>
<input v-model="receiverUsername" placeholder="username" autofocus />
</div>
<label class="checkbox">
<input type="checkbox" v-model="requestFriend" />
<span>Also send a friend request</span>
<span>{{ $t('chat-invite-friend-too') }}</span>
</label>
<div class="actions">
<button type="button" @click="emit('close')" class="secondary">
Cancel
{{ $t('shared-cancel') }}
</button>
<button type="submit">
Send
{{ $t('chat-invite-send') }}
</button>
</div>
</form>
@@ -32,6 +32,9 @@
import { ref } from 'vue'
import { sendRoomInvite } from '../api/rooms'
import { sendFriendRequest } from '../api/friends';
import { useFluent } from 'fluent-vue';
const { $t } = useFluent();
const props = defineProps<{ room_uuid: string }>();
@@ -64,8 +67,7 @@ async function submit() {
emit('close')
} catch (err: any) {
console.error(err)
errorMessage.value = err?.message || err || 'An error occurred'
errorMessage.value = err?.message || err || $t('shared-error');
}
}
</script>

View File

@@ -1,3 +1,8 @@
<template>
<textarea ref="textareaRef" v-model="content" @input="resize" @keydown="handleKeydown" rows="1"
:placeholder="$t('chat-input-placeholder')"></textarea>
</template>
<script setup lang="ts">
import { ref, nextTick } from 'vue'
@@ -46,11 +51,6 @@ function handleKeydown(e: KeyboardEvent) {
}
</script>
<template>
<textarea ref="textareaRef" v-model="content" @input="resize" @keydown="handleKeydown" placeholder="type a message"
rows="1"></textarea>
</template>
<style scoped>
textarea {
width: 100%;

View File

@@ -1,24 +1,24 @@
<template>
<div>
<nav id="bottom-nav">
<router-link to="/" class="nav-item" :class="{ 'router-link-active': $route.name === 'chat' }">
<i class="fa-solid fa-message"></i>
</router-link>
<div>
<nav id="bottom-nav">
<router-link to="/" class="nav-item" :class="{ 'router-link-active': $route.name === 'chat' }">
<i class="fa-solid fa-message"></i>
</router-link>
<router-link to="/friendlist" class="nav-item">
<i class="fa-solid fa-user-group"></i>
</router-link>
<router-link to="/friendlist" class="nav-item">
<i class="fa-solid fa-user-group"></i>
</router-link>
<router-link to="/notifications" class="nav-item">
<i class="fa-solid fa-bell"></i>
<span v-if="totalCount > 0" class="badge">{{ totalCount }}</span>
</router-link>
<router-link to="/notifications" class="nav-item">
<i class="fa-solid fa-bell"></i>
<span v-if="totalCount > 0" class="badge">{{ totalCount }}</span>
</router-link>
<router-link to="/account" class="nav-item">
<i class="fa-solid fa-circle-user"></i>
</router-link>
</nav>
</div>
<router-link to="/account" class="nav-item">
<i class="fa-solid fa-circle-user"></i>
</router-link>
</nav>
</div>
</template>
<script setup lang="ts">
@@ -28,85 +28,85 @@ import { useNotifications } from '../store'
const { totalCount, refreshNotifications } = useNotifications()
onMounted(() => {
refreshNotifications()
refreshNotifications()
})
</script>
<style scoped>
#bottom-nav {
display: flex;
gap: 28px;
padding: 5px 22px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 100vh;
z-index: 50;
user-select: none;
-webkit-user-select: none;
display: flex;
gap: 28px;
padding: 5px 22px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 100vh;
z-index: 50;
user-select: none;
-webkit-user-select: none;
}
.nav-item {
all: unset;
all: unset;
position: relative;
cursor: pointer;
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
border-radius: 100vh;
transition:
color 0.2s ease,
background-color 0.2s ease,
transform 0.15s ease;
position: relative;
cursor: pointer;
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
border-radius: 100vh;
transition:
color 0.2s ease,
background-color 0.2s ease,
transform 0.15s ease;
}
.nav-item i {
transition:
color 0.2s ease,
background-color 0.2s ease,
transform 0.15s ease;
transition:
color 0.2s ease,
background-color 0.2s ease,
transform 0.15s ease;
}
.badge {
position: absolute;
top: 4px;
right: 4px;
background-color: var(--accent);
color: black;
font-size: 0.65rem;
font-weight: bold;
min-width: 18px;
height: 18px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
padding: 2px;
border: 2px solid var(--panel);
pointer-events: none;
position: absolute;
top: 4px;
right: 4px;
background-color: var(--accent);
color: black;
font-size: 0.65rem;
font-weight: bold;
min-width: 18px;
height: 18px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
padding: 2px;
border: 2px solid var(--panel);
pointer-events: none;
}
@media (hover: hover) {
.nav-item:hover {
background: rgba(255, 255, 255, 0.04);
}
.nav-item:hover {
background: rgba(255, 255, 255, 0.04);
}
.nav-item:not(.router-link-active):hover i {
color: var(--text);
}
.nav-item:not(.router-link-active):hover i {
color: var(--text);
}
}
.router-link-active i {
color: var(--accent);
color: var(--accent);
}
@media (max-width: 720px) {
#bottom-nav {
bottom: 16px;
}
#bottom-nav {
bottom: 16px;
}
}
</style>

View File

@@ -1,9 +1,10 @@
<template>
<div class="room-list">
<header class="rooms-header">
<h2>Rooms</h2>
<button class="create-btn" @click="showCreate = true" title="Create a room"><i
class="fa-solid fa-plus"></i></button>
<h2>{{ $t('chat-room-list-title') }}</h2>
<button class="create-btn" @click="showCreate = true" :title="$t('chat-create-title')">
<i class="fa-solid fa-plus"></i>
</button>
</header>
<Teleport to="body">
@@ -15,7 +16,7 @@
:class="{ active: route.params.uuid === room.uuid }" @click="emit('select-room')">
<div class="room-info">
<span class="room-name">{{ room.name }}</span>
<span class="room-owner">by {{ room.owner_name }}</span>
<span class="room-owner">{{ $t('chat-room-owner', { owner: room.owner_name }) }}</span>
</div>
</router-link>
</div>

View File

@@ -1,34 +1,34 @@
<template>
<div class="backdrop" @click.self="emit('close')">
<form class="modal" @submit.prevent="submit">
<h2>Update your Account</h2>
<p class="subtitle">Only fill in the fields you wish to change.</p>
<h2>{{ $t('account-update-modal-title') }}</h2>
<p class="subtitle">{{ $t('account-update-subtitle') }}</p>
<p v-if="errorMessage" class="error-message">{{ errorMessage }}</p>
<div class="input-group">
<label>Username</label>
<input v-model="username" placeholder="New username" autofocus />
<label>{{ $t('auth-username') }}</label>
<input v-model="username" :placeholder="$t('auth-username')" autofocus />
</div>
<div class="input-group">
<label>Email</label>
<input v-model="email" type="email" placeholder="New email" />
<label>{{ $t('auth-email') }}</label>
<input v-model="email" type="email" :placeholder="$t('auth-email')" />
</div>
<div class="input-group">
<label>New Password</label>
<input v-model="password" type="password" placeholder="Leave blank to keep current" />
<input v-model="confirmPassword" type="password" placeholder="Confirm new password" />
<label>{{ $t('account-new-password') }}</label>
<input v-model="password" type="password" placeholder="..." />
<input v-model="confirmPassword" type="password" :placeholder="$t('account-new-password-confirm')" />
</div>
<div class="actions">
<button type="button" @click="emit('close')" class="secondary" :disabled="isSubmitting">
Cancel
{{ $t('shared-cancel') }}
</button>
<button type="submit" :disabled="isSubmitting">
{{ isSubmitting ? 'Updating...' : 'Save Changes' }}
{{ isSubmitting ? $t('account-updating') : $t('account-update-save') }}
</button>
</div>
</form>
@@ -40,6 +40,9 @@ import { ref } from 'vue'
import { updateAccount } from '../api/account'
import { updateLocalUser } from '../authStore'
import type { User } from '../types'
import { useFluent } from 'fluent-vue';
const { $t } = useFluent();
const props = defineProps<{ user: User | null }>()
const emit = defineEmits(['close', 'updated'])
@@ -56,13 +59,13 @@ async function submit() {
errorMessage.value = ''
if (password.value !== confirmPassword.value) {
errorMessage.value = "Passwords do not match."
errorMessage.value = $t('auth-error-password-match')
return
}
// Prevent empty submission
if (!username.value || !email.value) {
errorMessage.value = "Username and Email are required."
errorMessage.value = $t('account-error-required')
return
}
@@ -81,7 +84,7 @@ async function submit() {
emit('updated')
emit('close')
} catch (err: any) {
errorMessage.value = err?.message || 'Update failed'
errorMessage.value = err?.message || $t('account-error-failed')
} finally {
isSubmitting.value = false
}

66
src/i18n.ts Normal file
View File

@@ -0,0 +1,66 @@
import { FluentBundle, FluentResource } from '@fluent/bundle';
import { createFluentVue } from 'fluent-vue';
import enMessages from './locales/en.ftl?raw';
import frMessages from './locales/fr.ftl?raw';
const MESSAGES: Record<string, string> = {
en: enMessages,
fr: frMessages,
// es: esMessages,
};
const BUNDLES: Record<string, FluentBundle> = {};
// Helper to create and cache bundles
function getBundle(locale: string): FluentBundle {
if (BUNDLES[locale]) return BUNDLES[locale];
const bundle = new FluentBundle(locale);
const resource = new FluentResource(MESSAGES[locale] || MESSAGES['en']);
bundle.addResource(resource);
BUNDLES[locale] = bundle;
return bundle;
}
const fallbackBundle = getBundle('en');
export const fluent = createFluentVue({
bundles: [fallbackBundle]
});
/**
* Matches input (e.g., 'fr-FR' or 'fr') against supported keys.
*/
export function setLanguage(localeCode: string) {
// Extract base language (e.g., 'en' from 'en-US')
const base = localeCode.split('-')[0].toLowerCase();
// Check if supported
const target = MESSAGES[localeCode] ? localeCode : (MESSAGES[base] ? base : 'en');
const mainBundle = getBundle(target);
fluent.bundles = [mainBundle, fallbackBundle];
return target;
}
export const supportedLocales = Object.keys(MESSAGES);
/**
* Iterates through supported locales and gets the value of the 'language' key
* from each specific bundle.
*/
export function getSupportedLanguagesMetadata() {
return supportedLocales.map(code => {
const bundle = getBundle(code);
const message = bundle.getMessage('language');
let nativeName = code.toUpperCase();
if (message && message.value) {
nativeName = bundle.formatPattern(message.value);
}
return { code, name: nativeName };
});
}

79
src/locales/en.ftl Normal file
View File

@@ -0,0 +1,79 @@
## Language
language = English
## Navigation
nav-chat = Chat
nav-friends = Friendlist
nav-notifications = Notifications
nav-account = Account
## Auth
auth-login-title = Login
auth-register-title = Register
auth-email = Email
auth-username = Username
auth-password = Password
auth-confirm-password = confirm password
auth-update-password =
auth-login-btn = Login
auth-register-btn = Create Account
auth-no-account = Don't have an account?
auth-has-account = Already have an account?
auth-error-unknown = An unknown error occurred
auth-error-password-match = Passwords do not match
auth-error-password-length = Password must be at least 8 characters long
auth-error-email-invalid = Please enter a valid email address
## Chat page
chat-no-room = Select a room to start talking
chat-input-placeholder = type a message
chat-invite-title = Invite People
chat-invite-receiver = Receiver username
chat-invite-friend-too = Also send a friend request
chat-invite-send = Send
chat-room-list-title = Rooms
chat-room-owner = by {$owner}
chat-create-title = Create room
chat-create-name-placeholder = Room name
chat-create-global = Global room
chat-create-submit = Create
## Friends page
friends-title = Your friends
friends-add-title = Add Friend
friends-send-request = Send Request
friends-list-header = Your Friends
friends-error-required = Username is required.
## Notifications page
notifications-title = Notifications
notifications-friend-requests = Friend Requests
notifications-room-invites = Room Invites
notifications-no-requests = No pending requests
notifications-no-invites = No pending invites
notifications-accept = Accept
notifications-join = Join
notifications-invite-from = from: {$user}
notifications-error-friend = An error occurred while accepting the request.
notifications-error-room = An error occurred while accepting the invite.
## Account page
account-title = Your Account
account-label-username = Username:
account-label-email = Email:
account-update-btn = Update
account-logout-btn = Logout
account-loading = Loading account details...
account-update-modal-title = Update your Account
account-update-subtitle = Only fill in the fields you wish to change.
account-new-password = New Password
account-new-password-confirm = Confirm new password
account-update-save = Save Changes
account-updating = Updating...
account-language = Language
account-error-required = Username and Email are required.
account-error-failed = Update failed
## Shared
shared-cancel = Cancel
shared-error = An error occurred

78
src/locales/fr.ftl Normal file
View File

@@ -0,0 +1,78 @@
## Language
language = Français
## Navigation
nav-chat = Messagerie
nav-friends = Amis
nav-notifications = Notifications
nav-account = Compte
## Auth
auth-login-title = Connexion
auth-register-title = Inscription
auth-email = Email
auth-username = Nom d'utilisateur
auth-password = Mot de passe
auth-confirm-password = confirmer le mot de passe
auth-login-btn = Se connecter
auth-register-btn = Créer un compte
auth-no-account = Pas encore de compte ?
auth-has-account = Déjà un compte ?
auth-error-unknown = Une erreur inconnue est survenue
auth-error-password-match = Les mots de passe ne correspondent pas
auth-error-password-length = Le mot de passe doit faire au moins 8 caractères
auth-error-email-invalid = Veuillez entrer une adresse email valide
## Chat page
chat-no-room = Sélectionnez un salon pour commencer à discuter
chat-input-placeholder = tapez un message
chat-invite-title = Inviter des gens
chat-invite-receiver = Nom d'utilisateur
chat-invite-friend-too = Envoyer aussi une demande d'ami
chat-invite-send = Envoyer
chat-room-list-title = Salons
chat-room-owner = par {$owner}
chat-create-title = Créer un salon
chat-create-name-placeholder = Nom du salon
chat-create-global = Salon public
chat-create-submit = Créer
## Friends page
friends-title = Vos amis
friends-add-title = Ajouter un ami
friends-send-request = Envoyer
friends-list-header = Vos Amis
friends-error-required = Le nom d'utilisateur est requis.
## Notifications page
notifications-title = Notifications
notifications-friend-requests = Demandes d'amis
notifications-room-invites = Invitations
notifications-no-requests = Aucune demande en attente
notifications-no-invites = Aucune invitation en attente
notifications-accept = Accepter
notifications-join = Rejoindre
notifications-invite-from = de : {$user}
notifications-error-friend = Erreur lors de l'acceptation de la demande.
notifications-error-room = Erreur lors de l'acceptation de l'invitation.
## Account page
account-title = Votre Compte
account-label-username = Nom d'utilisateur :
account-label-email = Email :
account-update-btn = Modifier
account-logout-btn = Déconnexion
account-loading = Chargement du compte...
account-update-modal-title = Modifier votre compte
account-update-subtitle = Remplissez uniquement ce que vous souhaitez changer.
account-new-password = Nouveau mot de passe
account-new-password-confirm = Confirmer le mot de passe
account-update-save = Enregistrer
account-updating = Mise à jour...
account-language = Langue
account-error-required = Le nom d'utilisateur et l'email sont requis.
account-error-failed = Échec de la mise à jour
## Shared
shared-cancel = Annuler
shared-error = Une erreur est survenue

View File

@@ -2,14 +2,27 @@ import { createApp } from 'vue'
import router from './router.ts'
import App from './App.vue'
import { validateToken } from './store.ts'
import { fluent, setLanguage } from './i18n'
import './base.css'
import { getLocalePreference } from './authStore.ts'
async function init() {
await validateToken()
const app = createApp(App)
app.use(router)
app.use(fluent)
const savedLocale = await getLocalePreference();
const osLocale = navigator.language;
if (savedLocale) {
setLanguage(savedLocale);
} else {
setLanguage(osLocale);
}
app.mount('#app')
}

View File

@@ -1,36 +1,53 @@
<template>
<div class="account-page">
<h1>Your Account</h1>
<h1>{{ $t('account-title') }}</h1>
<UpdateAccountModal v-if="showUpdateModal" :user="user" @close="showUpdateModal = false" @updated="fetchUserData" />
<div v-if="user" class="info-card">
<button class="update-btn" @click="showUpdateModal = true">Update</button>
<p><strong>Username:</strong> {{ user.username }}</p>
<p><strong>Email:</strong> {{ user.email }}</p>
<button class="update-btn" @click="showUpdateModal = true">{{ $t('account-update-btn') }}</button>
<p><strong>{{ $t('account-label-username') }}</strong> {{ user.username }}</p>
<p><strong>{{ $t('account-label-email') }}</strong> {{ user.email }}</p>
</div>
<div v-else class="loading-state">
<p>Loading account details...</p>
<p>{{ $t('account-loading') }}</p>
</div>
<div class="input-group">
<span>{{ $t('account-language') }}</span>
<div class="lang-grid">
<button v-for="lang in languages" :key="lang.code" class="lang-btn"
:class="{ active: currentLang === lang.code }" @click="changeLanguage(lang.code)">
{{ lang.name }}
</button>
</div>
</div>
<button class="logout-btn" @click="logout">
<i class="fa-solid fa-right-from-bracket"></i>
<span>Logout</span>
<span>{{ $t('account-logout-btn') }}</span>
</button>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { logout as authLogout } from '../store.ts'
import { getAuthData } from "../authStore.ts"
import type { User } from "../types"
import UpdateAccountModal from '../components/UpdateAccountModal.vue'
import { useFluent } from 'fluent-vue'
import { saveLocalePreference, getLocalePreference } from "../authStore.ts"
import { getSupportedLanguagesMetadata, setLanguage } from '../i18n'
const router = useRouter()
const user = ref<User | null>(null)
const showUpdateModal = ref(false)
const { $t } = useFluent()
const currentLang = ref('')
const languages = computed(() => getSupportedLanguagesMetadata())
async function fetchUserData() {
try {
@@ -41,7 +58,19 @@ async function fetchUserData() {
}
}
onMounted(fetchUserData)
onMounted(async () => {
const pref = await getLocalePreference()
// Synchronize the UI state with the actual active language
currentLang.value = pref || (navigator.language.split('-')[0])
fetchUserData()
})
async function changeLanguage(code: string) {
const actual = setLanguage(code)
currentLang.value = actual
await saveLocalePreference(actual)
}
function logout() {
authLogout()
@@ -73,6 +102,26 @@ function logout() {
top: 10px;
}
.lang-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 0.5rem;
margin-top: 0.5rem;
}
.lang-btn {
background: var(--panel);
border: 1px solid var(--border);
color: var(--text);
margin: 0;
}
.lang-btn.active {
background: var(--accent);
color: #000;
border-color: var(--accent);
}
.logout-btn {
cursor: pointer;
display: flex;

View File

@@ -1,21 +1,20 @@
<template>
<div class="friends-page">
<header class="friends-header">
<h1>Your friends</h1>
<h1>{{ $t('friends-title') }}</h1>
<form class="friend-request-form" @submit.prevent="send">
<h3>Add Friend</h3>
<h3>{{ $t('friends-add-title') }}</h3>
<div class="input-container">
<input v-model="username" placeholder="Username" autofocus />
<button type="submit">Send Request</button>
<input v-model="username" :placeholder="$t('auth-username')" autofocus />
<button type="submit">{{ $t('friends-send-request') }}</button>
</div>
<p v-if="errorMessage" class="error-message">{{ errorMessage }}</p>
</form>
</header>
<!-- Friends List -->
<div class="friends-list">
<h2>Your Friends</h2>
<h2>{{ $t('friends-list-header') }}</h2>
<ul>
<li v-for="friend in friends" :key="friend.uuid">
{{ friend.username }}

View File

@@ -1,7 +1,29 @@
<template>
<div class="login-page">
<form class="login-card" @submit.prevent="submit">
<h1>{{ $t('auth-login-title') }}</h1>
<div class="input-group">
<label>{{ $t('auth-email') }}</label>
<input v-model="email" :placeholder="$t('auth-email').toLowerCase()" />
</div>
<div class="input-group">
<label>{{ $t('auth-password') }}</label>
<input v-model="password" type="password" :placeholder="$t('auth-password').toLowerCase()" />
</div>
<button type="submit">{{ $t('auth-login-btn') }}</button>
<p v-if="errorMessage" class="error-message">{{ errorMessage }}</p>
<p class="register-link">
{{ $t('auth-no-account') }} <router-link to="/register">{{ $t('auth-register-title') }}</router-link>
</p>
</form>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { login } from '../store.ts'
import { useRouter } from "vue-router";
import { useFluent } from 'fluent-vue';
const email = ref("");
const password = ref("");
@@ -9,85 +31,61 @@ const errorMessage = ref("");
const router = useRouter();
const { $t } = useFluent();
async function submit() {
errorMessage.value = "";
try {
await login(email.value, "", password.value);
router.push("/");
} catch (err: any) {
errorMessage.value = err?.message || "An unknown error occurred";
}
errorMessage.value = "";
try {
await login(email.value, "", password.value);
router.push("/");
} catch (err: any) {
errorMessage.value = err?.message || $t('auth-error-unknown');
}
}
</script>
<template>
<div class="login-page">
<form class="login-card" @submit.prevent="submit">
<h1>Login</h1>
<div class="input-group">
<label>Email</label>
<input v-model="email" placeholder="email" />
</div>
<div class="input-group">
<label>Password</label>
<input v-model="password" type="password" placeholder="password" />
</div>
<button type="submit">Login</button>
<p v-if="errorMessage" class="error-message">{{ errorMessage }}</p>
<p class="register-link">
Don't have an account? <router-link to="/register">Register</router-link>
</p>
</form>
</div>
</template>
<style scoped>
.login-page {
height: 100%;
display: grid;
place-items: center;
height: 100%;
display: grid;
place-items: center;
}
.login-card {
width: 100%;
max-width: 360px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 2rem;
width: 100%;
max-width: 360px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 2rem;
display: flex;
flex-direction: column;
gap: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.login-card h1 {
font-size: 1.4rem;
font-weight: 600;
margin-bottom: 0.5rem;
text-align: center;
font-size: 1.4rem;
font-weight: 600;
margin-bottom: 0.5rem;
text-align: center;
}
.register-link {
font-size: 0.9rem;
text-align: center;
color: var(--text-muted);
font-size: 0.9rem;
text-align: center;
color: var(--text-muted);
}
.register-link a {
color: var(--accent);
margin-left: 5px;
color: var(--accent);
margin-left: 5px;
}
.error-message {
color: var(--error);
font-size: 0.9rem;
text-align: center;
margin-top: 0.5rem;
color: var(--error);
font-size: 0.9rem;
text-align: center;
margin-top: 0.5rem;
}
</style>

View File

@@ -1,32 +1,31 @@
<template>
<div class="notifications-page">
<h1>Notifications</h1>
<h1>{{ $t('notifications-title') }}</h1>
<p v-if="errorMessage" class="error-message">{{ errorMessage }}</p>
<!-- Friend Request List -->
<div class="list">
<h2>Friend Requests</h2>
<h2>{{ $t('notifications-friend-requests') }}</h2>
<ul v-if="requests.length">
<li v-for="req in requests" :key="req.sender_uuid">
<span>{{ req.sender_username }}</span>
<button @click="acceptFriend(req.sender_uuid)">Accept</button>
<button @click="acceptFriend(req.sender_uuid)">{{ $t('notifications-accept') }}</button>
</li>
</ul>
<p v-else>No pending requests</p>
<p v-else>{{ $t('notifications-no-requests') }}</p>
</div>
<!-- Room Invites List -->
<div class="list">
<h2>Room Invites</h2>
<h2>{{ $t('notifications-room-invites') }}</h2>
<ul v-if="invites.length">
<li v-for="inv in invites" :key="inv.sender_uuid">
<span>{{ inv.room_name }}</span>
<span>from: {{ inv.sender_username }}</span>
<button @click="acceptRoom(inv.sender_uuid, inv.room_uuid)">Join</button>
<span>{{ $t('notifications-invite-from', { user: inv.sender_username }) }}</span>
<button @click="acceptRoom(inv.sender_uuid, inv.room_uuid)">{{ $t('notifications-join') }}</button>
</li>
</ul>
<p v-else>No pending invites</p>
<p v-else>{{ $t('notifications-no-invites') }}</p>
</div>
</div>
</template>
@@ -36,6 +35,9 @@ import { onMounted, ref } from 'vue'
import { fetchFriendRequests, acceptFriendRequest } from '../api/friends'
import { fetchRoomInvites, acceptRoomInvite } from '../api/rooms.ts'
import { useNotifications } from '../store'
import { useFluent } from 'fluent-vue';
const { $t } = useFluent();
const errorMessage = ref('')
const { requests, invites, refreshNotifications } = useNotifications()
@@ -52,7 +54,7 @@ async function acceptFriend(senderUuid: string) {
requests.value = requests.value.filter(r => r.sender_uuid !== senderUuid)
// fetchFriends().then(f => (friends.value = f))
} catch (err) {
errorMessage.value = 'An error occurred while accepting the request.' // TODO: handle this case
errorMessage.value = $t('notifications-error-friend') // TODO: handle this case
}
}
@@ -62,7 +64,7 @@ async function acceptRoom(senderUuid: string, roomUuid: string) {
invites.value = invites.value.filter(r => r.room_uuid !== roomUuid)
// fetchFriends().then(f => (friends.value = f))
} catch (err) {
errorMessage.value = 'An error occurred while accepting the invite.' // TODO: handle this case
errorMessage.value = $t('notifications-error-room') // TODO: handle this case
throw err
}
}

View File

@@ -1,28 +1,28 @@
<template>
<div class="login-page">
<form class="login-card" @submit.prevent="submit" novalidate>
<h1>Register</h1>
<h1>{{ $t('auth-register-title') }}</h1>
<div class="input-group">
<label>Email</label>
<input v-model="email" type="email" placeholder="email" required />
<label>{{ $t('auth-email') }}</label>
<input v-model="email" type="email" :placeholder="$t('auth-email').toLowerCase()" required />
</div>
<div class="input-group">
<label>Username</label>
<input v-model="username" placeholder="username" required />
<label>{{ $t('auth-username') }}</label>
<input v-model="username" :placeholder="$t('auth-username').toLowerCase()" required />
</div>
<div class="input-group">
<label>Password</label>
<input v-model="password" type="password" placeholder="password" required />
<input v-model="confirmPassword" type="password" placeholder="confirm password" required />
<label>{{ $t('auth-password') }}</label>
<input v-model="password" type="password" :placeholder="$t('auth-password').toLowerCase()" required />
<input v-model="confirmPassword" type="password" :placeholder="$t('auth-confirm-password')" required />
</div>
<button type="submit">Create Account</button>
<button type="submit">{{ $t('auth-register-btn') }}</button>
<p class="login-link">
Already have an account? <router-link to="/login">Login</router-link>
{{ $t('auth-has-account') }} <router-link to="/login">{{ $t('auth-login-title') }}</router-link>
</p>
<p v-if="errorMessage" class="error-message">{{ errorMessage }}</p>
@@ -34,6 +34,9 @@
import { ref } from "vue";
import { register } from '../store.ts'
import { useRouter } from "vue-router";
import { useFluent } from 'fluent-vue';
const { $t } = useFluent();
const email = ref("");
const username = ref("");
@@ -51,16 +54,16 @@ async function submit(event: Event) {
// Check password length and email
if (!form.checkValidity()) {
if (password.value.length < 8) {
errorMessage.value = "Password must be at least 8 characters long";
errorMessage.value = $t('auth-error-password-length');
} else {
errorMessage.value = "Please enter a valid email address";
errorMessage.value = $t('auth-error-email-invalid');
}
return;
}
// Check password match
if (password.value !== confirmPassword.value) {
errorMessage.value = "Passwords do not match";
errorMessage.value = $t('auth-error-password-match');
return;
}

207
yarn.lock
View File

@@ -157,11 +157,64 @@
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz#9bdad8176be7811ad148d1f8772359041f46c6c5"
integrity sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==
"@jridgewell/sourcemap-codec@^1.5.5":
"@fluent/bundle@^0.19.1":
version "0.19.1"
resolved "https://registry.yarnpkg.com/@fluent/bundle/-/bundle-0.19.1.tgz#d9931a0be577fc6ebebd97d73c6d15d20fa5f54c"
integrity sha512-SWJLZrPamDPsJlFFOW1nkgN0j0rbPbmSdmK0XAoXlyqKieLtMVl4vzng3aR5pwKoUx0scug8+YY2oct3fdfy9A==
"@fluent/sequence@^0.8.0":
version "0.8.0"
resolved "https://registry.yarnpkg.com/@fluent/sequence/-/sequence-0.8.0.tgz#dd3da353a0635f1efa5f27e6d4fc59e01f162372"
integrity sha512-eV5QlEEVV/wR3AFQLXO67x4yPRPQXyqke0c8yucyMSeW36B3ecZyVFlY1UprzrfFV8iPJB4TAehDy/dLGbvQ1Q==
"@fluent/syntax@^0.19.0":
version "0.19.0"
resolved "https://registry.yarnpkg.com/@fluent/syntax/-/syntax-0.19.0.tgz#43f882faba6908b0f1013f6a94e009d0dfbdcb77"
integrity sha512-5D2qVpZrgpjtqU4eNOcWGp1gnUCgjfM+vKGE2y03kKN6z5EBhtx0qdRFbg8QuNNj8wXNoX93KJoYb+NqoxswmQ==
"@jridgewell/gen-mapping@^0.3.5":
version "0.3.13"
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f"
integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==
dependencies:
"@jridgewell/sourcemap-codec" "^1.5.0"
"@jridgewell/trace-mapping" "^0.3.24"
"@jridgewell/remapping@^2.3.5":
version "2.3.5"
resolved "https://registry.yarnpkg.com/@jridgewell/remapping/-/remapping-2.3.5.tgz#375c476d1972947851ba1e15ae8f123047445aa1"
integrity sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==
dependencies:
"@jridgewell/gen-mapping" "^0.3.5"
"@jridgewell/trace-mapping" "^0.3.24"
"@jridgewell/resolve-uri@^3.1.0":
version "3.1.2"
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6"
integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0", "@jridgewell/sourcemap-codec@^1.5.5":
version "1.5.5"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba"
integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==
"@jridgewell/trace-mapping@^0.3.24":
version "0.3.31"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0"
integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==
dependencies:
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@rollup/pluginutils@^5.0.0":
version "5.3.0"
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.3.0.tgz#57ba1b0cbda8e7a3c597a4853c807b156e21a7b4"
integrity sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==
dependencies:
"@types/estree" "^1.0.0"
estree-walker "^2.0.2"
picomatch "^4.0.2"
"@rollup/rollup-android-arm-eabi@4.53.4":
version "4.53.4"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.4.tgz#c02c6fcd53ebae26feff7bfdcfb3b6b9015ff56f"
@@ -377,7 +430,7 @@
dependencies:
"@tauri-apps/api" "^2.8.0"
"@types/estree@1.0.8":
"@types/estree@1.0.8", "@types/estree@^1.0.0":
version "1.0.8"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
@@ -419,6 +472,17 @@
estree-walker "^2.0.2"
source-map-js "^1.2.1"
"@vue/compiler-core@^3.4.21":
version "3.5.26"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.5.26.tgz#1a91ea90980528bedff7b1c292690bfb30612485"
integrity sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==
dependencies:
"@babel/parser" "^7.28.5"
"@vue/shared" "3.5.26"
entities "^7.0.0"
estree-walker "^2.0.2"
source-map-js "^1.2.1"
"@vue/compiler-dom@3.5.25", "@vue/compiler-dom@^3.5.0":
version "3.5.25"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.5.25.tgz#dd799ac2474cda54303039310b8994f0cfb40957"
@@ -463,6 +527,33 @@
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343"
integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==
"@vue/devtools-api@^8.0.0":
version "8.0.5"
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-8.0.5.tgz#313c563954b06b8254100ebc0d0e3773b1a06e18"
integrity sha512-DgVcW8H/Nral7LgZEecYFFYXnAvGuN9C3L3DtWekAncFBedBczpNW8iHKExfaM559Zm8wQWrwtYZ9lXthEHtDw==
dependencies:
"@vue/devtools-kit" "^8.0.5"
"@vue/devtools-kit@^8.0.5":
version "8.0.5"
resolved "https://registry.yarnpkg.com/@vue/devtools-kit/-/devtools-kit-8.0.5.tgz#d16927554adf527785706caa11e910ff4e00a998"
integrity sha512-q2VV6x1U3KJMTQPUlRMyWEKVbcHuxhqJdSr6Jtjz5uAThAIrfJ6WVZdGZm5cuO63ZnSUz0RCsVwiUUb0mDV0Yg==
dependencies:
"@vue/devtools-shared" "^8.0.5"
birpc "^2.6.1"
hookable "^5.5.3"
mitt "^3.0.1"
perfect-debounce "^2.0.0"
speakingurl "^14.0.1"
superjson "^2.2.2"
"@vue/devtools-shared@^8.0.5":
version "8.0.5"
resolved "https://registry.yarnpkg.com/@vue/devtools-shared/-/devtools-shared-8.0.5.tgz#d97e887640fb2cad1e9b9e40fb46010d69852103"
integrity sha512-bRLn6/spxpmgLk+iwOrR29KrYnJjG9DGpHGkDFG82UM21ZpJ39ztUT9OXX3g+usW7/b2z+h46I9ZiYyB07XMXg==
dependencies:
rfdc "^1.4.1"
"@vue/language-core@2.2.12":
version "2.2.12"
resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-2.2.12.tgz#d01f7e865f593f968cb65c12a13d8337e65641f0"
@@ -515,6 +606,16 @@
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.25.tgz#21edcff133a5a04f72c4e4c6142260963fe5afbe"
integrity sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==
"@vue/shared@3.5.26":
version "3.5.26"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.26.tgz#1e02ef2d64aced818cd31d81ce5175711dc90a9f"
integrity sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==
acorn@^8.15.0:
version "8.15.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816"
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
alien-signals@^1.0.3:
version "1.0.13"
resolved "https://registry.yarnpkg.com/alien-signals/-/alien-signals-1.0.13.tgz#8d6db73462f742ee6b89671fbd8c37d0b1727a7e"
@@ -525,6 +626,11 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
birpc@^2.6.1:
version "2.9.0"
resolved "https://registry.yarnpkg.com/birpc/-/birpc-2.9.0.tgz#b59550897e4cd96a223e2a6c1475b572236ed145"
integrity sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==
brace-expansion@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7"
@@ -532,6 +638,18 @@ brace-expansion@^2.0.1:
dependencies:
balanced-match "^1.0.0"
cached-iterable@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/cached-iterable/-/cached-iterable-0.3.0.tgz#2618204549e9c5dd102102a6e7e50af6adb77df8"
integrity sha512-MDqM6TpBVebZD4UDtmlFp8EjVtRcsB6xt9aRdWymjk0fWVUUGgmt/V7o0H0gkI2Tkvv8B0ucjidZm4mLosdlWw==
copy-anything@^4:
version "4.0.5"
resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-4.0.5.tgz#16cabafd1ea4bb327a540b750f2b4df522825aea"
integrity sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==
dependencies:
is-what "^5.2.0"
csstype@^3.1.3:
version "3.2.3"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a"
@@ -547,6 +665,11 @@ entities@^4.5.0:
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
entities@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-7.0.0.tgz#2ae4e443f3f17d152d3f5b0f79b932c1e59deb7a"
integrity sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==
esbuild@^0.25.0:
version "0.25.12"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.12.tgz#97a1d041f4ab00c2fce2f838d2b9969a2d2a97a5"
@@ -589,6 +712,16 @@ fdir@^6.4.4, fdir@^6.5.0:
resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350"
integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==
fluent-vue@^3.8.1:
version "3.8.1"
resolved "https://registry.yarnpkg.com/fluent-vue/-/fluent-vue-3.8.1.tgz#950ef423375d2d575312a066f70ff4bbef0481b1"
integrity sha512-e+6bMOS6tAztEJsOKTWozsQqFzrL6ywd6yBiQ4NhJ0gHenx6F9uBCFuIhwzgxBQHyOUej83Ep+xMGj+FtoPg/Q==
dependencies:
"@fluent/sequence" "^0.8.0"
"@vue/devtools-api" "^8.0.0"
cached-iterable "^0.3.0"
vue-demi latest
fsevents@~2.3.2, fsevents@~2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
@@ -599,7 +732,17 @@ he@^1.2.0:
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
magic-string@^0.30.21:
hookable@^5.5.3:
version "5.5.3"
resolved "https://registry.yarnpkg.com/hookable/-/hookable-5.5.3.tgz#6cfc358984a1ef991e2518cb9ed4a778bbd3215d"
integrity sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==
is-what@^5.2.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/is-what/-/is-what-5.5.0.tgz#a3031815757cfe1f03fed990bf6355a2d3f628c4"
integrity sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==
magic-string@^0.30.0, magic-string@^0.30.21:
version "0.30.21"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91"
integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==
@@ -613,6 +756,11 @@ minimatch@^9.0.3:
dependencies:
brace-expansion "^2.0.1"
mitt@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1"
integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==
muggle-string@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/muggle-string/-/muggle-string-0.4.1.tgz#3b366bd43b32f809dc20659534dd30e7c8a0d328"
@@ -628,6 +776,11 @@ path-browserify@^1.0.1:
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd"
integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==
perfect-debounce@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/perfect-debounce/-/perfect-debounce-2.0.0.tgz#0ff94f1ecbe0a6bca4b1703a2ed08bbe43739aa7"
integrity sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==
picocolors@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
@@ -647,6 +800,11 @@ postcss@^8.5.3, postcss@^8.5.6:
picocolors "^1.1.1"
source-map-js "^1.2.1"
rfdc@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca"
integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==
rollup@^4.34.9:
version "4.53.4"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.53.4.tgz#5517de2593624928ac18f041b269f3b79cb64e09"
@@ -683,6 +841,18 @@ source-map-js@^1.2.1:
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
speakingurl@^14.0.1:
version "14.0.1"
resolved "https://registry.yarnpkg.com/speakingurl/-/speakingurl-14.0.1.tgz#f37ec8ddc4ab98e9600c1c9ec324a8c48d772a53"
integrity sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==
superjson@^2.2.2:
version "2.2.6"
resolved "https://registry.yarnpkg.com/superjson/-/superjson-2.2.6.tgz#a223a3a988172a5f9656e2063fe5f733af40d099"
integrity sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==
dependencies:
copy-anything "^4"
tinyglobby@^0.2.13:
version "0.2.15"
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2"
@@ -696,6 +866,27 @@ typescript@~5.6.2:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.3.tgz#5f3449e31c9d94febb17de03cc081dd56d81db5b"
integrity sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==
unplugin-fluent-vue@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/unplugin-fluent-vue/-/unplugin-fluent-vue-1.4.1.tgz#96e2eb614f974f2d4a200a1cbf9b1897094f3319"
integrity sha512-MktKWZP2Tub0pVkggM4yvu3x/kL5ebjNcDe+dgZBFD0fhTYcQNlbMZ520EIQTnx0MA4aSh1kt0cW+wgb2dbWTA==
dependencies:
"@fluent/syntax" "^0.19.0"
"@rollup/pluginutils" "^5.0.0"
"@vue/compiler-core" "^3.4.21"
magic-string "^0.30.0"
unplugin "^2.0.0"
unplugin@^2.0.0:
version "2.3.11"
resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-2.3.11.tgz#411e020dd2ba90e2fbe1e7bd63a5a399e6ee3b54"
integrity sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==
dependencies:
"@jridgewell/remapping" "^2.3.5"
acorn "^8.15.0"
picomatch "^4.0.3"
webpack-virtual-modules "^0.6.2"
vite@^6.0.3:
version "6.4.1"
resolved "https://registry.yarnpkg.com/vite/-/vite-6.4.1.tgz#afbe14518cdd6887e240a4b0221ab6d0ce733f96"
@@ -715,6 +906,11 @@ vscode-uri@^3.0.8:
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.1.0.tgz#dd09ec5a66a38b5c3fffc774015713496d14e09c"
integrity sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==
vue-demi@latest:
version "0.14.10"
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.10.tgz#afc78de3d6f9e11bf78c55e8510ee12814522f04"
integrity sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==
vue-router@^4.6.4:
version "4.6.4"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.6.4.tgz#a0a9cb9ef811a106d249e4bb9313d286718020d8"
@@ -740,3 +936,8 @@ vue@^3.5.13:
"@vue/runtime-dom" "3.5.25"
"@vue/server-renderer" "3.5.25"
"@vue/shared" "3.5.25"
webpack-virtual-modules@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz#057faa9065c8acf48f24cb57ac0e77739ab9a7e8"
integrity sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==