added i18n, english and french for now
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
66
src/i18n.ts
Normal 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
79
src/locales/en.ftl
Normal 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
78
src/locales/fr.ftl
Normal 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
|
||||
13
src/main.ts
13
src/main.ts
@@ -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')
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user