feat: allow editing user profile settings
This commit is contained in:
34
docs/FEATURES/user-profile-settings.md
Normal file
34
docs/FEATURES/user-profile-settings.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Feature: User Profile Settings
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Draft
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Allow authenticated users to manage the profile information shown inside the application shell and workspace activity.
|
||||||
|
|
||||||
|
## User Stories
|
||||||
|
|
||||||
|
- As an authenticated user, I want to update my name, alias, email, and portrait so that other workspace members see accurate profile information.
|
||||||
|
|
||||||
|
## Frontend Areas
|
||||||
|
|
||||||
|
- `/app/settings/user-information`
|
||||||
|
- `frontend/src/features/user-profile/`
|
||||||
|
|
||||||
|
## Backend Modules
|
||||||
|
|
||||||
|
- Identity
|
||||||
|
|
||||||
|
## Domain Rules
|
||||||
|
|
||||||
|
- Profile updates apply only to the authenticated user.
|
||||||
|
- Portrait uploads flow through the existing blob storage abstraction.
|
||||||
|
- Email changes use the identity module endpoint and should remain auditable through backend identity behavior.
|
||||||
|
|
||||||
|
## Done When
|
||||||
|
|
||||||
|
- [ ] User information settings show editable name, alias, and email fields.
|
||||||
|
- [ ] Portrait upload remains available from the settings page.
|
||||||
|
- [ ] Successful updates refresh the user profile state used by the app shell.
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Task: Edit user information settings
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Allow users to edit their profile details from the user information settings page.
|
||||||
|
|
||||||
|
## Feature Spec
|
||||||
|
|
||||||
|
- `docs/FEATURES/user-profile-settings.md`
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Replace read-only user information details with editable first name, last name, alias, and email fields.
|
||||||
|
- Keep portrait upload available on the page.
|
||||||
|
- Use the existing Identity endpoints for full name, alias, email, and portrait updates.
|
||||||
|
- Keep the profile store as the source of truth for app-shell user identity.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import {computed, watch} from 'vue'
|
import {computed, ref, watch} from 'vue'
|
||||||
import {defineStore} from 'pinia'
|
import {defineStore} from 'pinia'
|
||||||
import {useAuthStore} from "@/features/auth/stores/authStore.js";
|
import {useAuthStore} from "@/features/auth/stores/authStore.js";
|
||||||
import {useClient} from "@/plugins/api.js";
|
import {useClient} from "@/plugins/api.js";
|
||||||
@@ -9,6 +9,9 @@ export const useUserProfileStore = defineStore(
|
|||||||
() => {
|
() => {
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
const isUpdating = ref(false)
|
||||||
|
const isUploadingPortrait = ref(false)
|
||||||
|
const error = ref(null)
|
||||||
|
|
||||||
const authWatcher = watch(
|
const authWatcher = watch(
|
||||||
() => authStore.isAuthenticated,
|
() => authStore.isAuthenticated,
|
||||||
@@ -64,12 +67,15 @@ export const useUserProfileStore = defineStore(
|
|||||||
const client = useClient()
|
const client = useClient()
|
||||||
const userResponse = await client.get("/api/users/profile");
|
const userResponse = await client.get("/api/users/profile");
|
||||||
value.value = userResponse.data
|
value.value = userResponse.data
|
||||||
} catch (error) {
|
} catch (fetchError) {
|
||||||
console.error(error)
|
console.error(fetchError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function changeFullname(firstname, lastname) {
|
async function changeFullname(firstname, lastname) {
|
||||||
|
isUpdating.value = true
|
||||||
|
error.value = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = useClient()
|
const client = useClient()
|
||||||
await client.post(
|
await client.post(
|
||||||
@@ -80,12 +86,19 @@ export const useUserProfileStore = defineStore(
|
|||||||
})
|
})
|
||||||
value.value.firstname = firstname;
|
value.value.firstname = firstname;
|
||||||
value.value.lastname = lastname;
|
value.value.lastname = lastname;
|
||||||
} catch (error) {
|
} catch (updateError) {
|
||||||
console.error(error)
|
console.error(updateError)
|
||||||
|
error.value = 'Failed to update profile.'
|
||||||
|
throw updateError
|
||||||
|
} finally {
|
||||||
|
isUpdating.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function changeAlias(alias) {
|
async function changeAlias(alias) {
|
||||||
|
isUpdating.value = true
|
||||||
|
error.value = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = useClient()
|
const client = useClient()
|
||||||
await client.post(
|
await client.post(
|
||||||
@@ -94,8 +107,12 @@ export const useUserProfileStore = defineStore(
|
|||||||
alias: alias
|
alias: alias
|
||||||
})
|
})
|
||||||
value.value.alias = alias;
|
value.value.alias = alias;
|
||||||
} catch (error) {
|
} catch (updateError) {
|
||||||
console.error(error)
|
console.error(updateError)
|
||||||
|
error.value = 'Failed to update profile.'
|
||||||
|
throw updateError
|
||||||
|
} finally {
|
||||||
|
isUpdating.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,6 +145,9 @@ export const useUserProfileStore = defineStore(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function changeEmail(email) {
|
async function changeEmail(email) {
|
||||||
|
isUpdating.value = true
|
||||||
|
error.value = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = useClient()
|
const client = useClient()
|
||||||
await client.post(
|
await client.post(
|
||||||
@@ -136,8 +156,12 @@ export const useUserProfileStore = defineStore(
|
|||||||
email: email
|
email: email
|
||||||
})
|
})
|
||||||
value.value.email = email;
|
value.value.email = email;
|
||||||
} catch (error) {
|
} catch (updateError) {
|
||||||
console.error(error)
|
console.error(updateError)
|
||||||
|
error.value = 'Failed to update profile.'
|
||||||
|
throw updateError
|
||||||
|
} finally {
|
||||||
|
isUpdating.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,6 +180,9 @@ export const useUserProfileStore = defineStore(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function changePortrait(selectedFile) {
|
async function changePortrait(selectedFile) {
|
||||||
|
isUploadingPortrait.value = true
|
||||||
|
error.value = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = useClient()
|
const client = useClient()
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
@@ -166,8 +193,12 @@ export const useUserProfileStore = defineStore(
|
|||||||
formData)
|
formData)
|
||||||
|
|
||||||
value.value.portraitUrl = `${response.data.blobUrl}?${Date.now()}` // the Date.now() is for cache-busting
|
value.value.portraitUrl = `${response.data.blobUrl}?${Date.now()}` // the Date.now() is for cache-busting
|
||||||
} catch (error) {
|
} catch (uploadError) {
|
||||||
console.error(error)
|
console.error(uploadError)
|
||||||
|
error.value = 'Failed to update portrait.'
|
||||||
|
throw uploadError
|
||||||
|
} finally {
|
||||||
|
isUploadingPortrait.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,6 +207,9 @@ export const useUserProfileStore = defineStore(
|
|||||||
alias,
|
alias,
|
||||||
fullname,
|
fullname,
|
||||||
portraitUrl,
|
portraitUrl,
|
||||||
|
isUpdating,
|
||||||
|
isUploadingPortrait,
|
||||||
|
error,
|
||||||
roles,
|
roles,
|
||||||
persona,
|
persona,
|
||||||
authorizedWorkspaceIds,
|
authorizedWorkspaceIds,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, reactive, ref, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import AppAvatar from '@/components/AppAvatar.vue';
|
import AppAvatar from '@/components/AppAvatar.vue';
|
||||||
import ImageCropperDialog from '@/components/ImageCropperDialog.vue';
|
import ImageCropperDialog from '@/components/ImageCropperDialog.vue';
|
||||||
@@ -9,21 +9,86 @@
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const isPortraitDialogOpen = ref(false);
|
const isPortraitDialogOpen = ref(false);
|
||||||
const isSavingPortrait = ref(false);
|
const isSavingPortrait = ref(false);
|
||||||
|
const settingsError = ref(null);
|
||||||
|
const settingsStatus = ref(null);
|
||||||
|
const form = reactive({
|
||||||
|
firstname: '',
|
||||||
|
lastname: '',
|
||||||
|
alias: '',
|
||||||
|
email: '',
|
||||||
|
});
|
||||||
|
|
||||||
const email = computed(() => userProfileStore.user?.email || t('userSettings.noEmail'));
|
const email = computed(() => userProfileStore.user?.email || t('userSettings.noEmail'));
|
||||||
const alias = computed(() => userProfileStore.alias);
|
const alias = computed(() => userProfileStore.alias);
|
||||||
const fullname = computed(() => userProfileStore.fullname);
|
const fullname = computed(() => userProfileStore.fullname);
|
||||||
|
const canSave = computed(() => Boolean(form.email.trim()) && !userProfileStore.isUpdating);
|
||||||
|
|
||||||
|
function syncFormFromUser(user) {
|
||||||
|
form.firstname = user?.firstname ?? '';
|
||||||
|
form.lastname = user?.lastname ?? '';
|
||||||
|
form.alias = user?.alias ?? '';
|
||||||
|
form.email = user?.email ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitSettings() {
|
||||||
|
if (!form.email.trim()) {
|
||||||
|
settingsError.value = t('userSettings.errors.emailRequired');
|
||||||
|
settingsStatus.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = userProfileStore.user ?? {};
|
||||||
|
const nextFirstname = form.firstname.trim();
|
||||||
|
const nextLastname = form.lastname.trim();
|
||||||
|
const nextAlias = form.alias.trim();
|
||||||
|
const nextEmail = form.email.trim();
|
||||||
|
|
||||||
|
settingsError.value = null;
|
||||||
|
settingsStatus.value = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (nextFirstname !== (user.firstname ?? '') || nextLastname !== (user.lastname ?? '')) {
|
||||||
|
await userProfileStore.changeFullname(nextFirstname, nextLastname);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextAlias !== (user.alias ?? '')) {
|
||||||
|
await userProfileStore.changeAlias(nextAlias || null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextEmail !== (user.email ?? '')) {
|
||||||
|
await userProfileStore.changeEmail(nextEmail);
|
||||||
|
}
|
||||||
|
|
||||||
|
settingsStatus.value = t('userSettings.saved');
|
||||||
|
syncFormFromUser(userProfileStore.user);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update user settings:', error);
|
||||||
|
settingsError.value = t('userSettings.errors.saveFailed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function savePortrait(result) {
|
async function savePortrait(result) {
|
||||||
isSavingPortrait.value = true;
|
isSavingPortrait.value = true;
|
||||||
|
settingsError.value = null;
|
||||||
|
settingsStatus.value = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await userProfileStore.changePortrait(result.file);
|
await userProfileStore.changePortrait(result.file);
|
||||||
isPortraitDialogOpen.value = false;
|
isPortraitDialogOpen.value = false;
|
||||||
|
settingsStatus.value = t('userSettings.portraitSaved');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update user portrait:', error);
|
||||||
|
settingsError.value = t('userSettings.errors.portraitFailed');
|
||||||
} finally {
|
} finally {
|
||||||
isSavingPortrait.value = false;
|
isSavingPortrait.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => userProfileStore.user,
|
||||||
|
syncFormFromUser,
|
||||||
|
{ immediate: true, deep: true }
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -50,6 +115,7 @@
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
class="primary-button"
|
class="primary-button"
|
||||||
|
type="button"
|
||||||
@click="isPortraitDialogOpen = true"
|
@click="isPortraitDialogOpen = true"
|
||||||
>
|
>
|
||||||
{{ t('userSettings.updatePortrait') }}
|
{{ t('userSettings.updatePortrait') }}
|
||||||
@@ -62,20 +128,77 @@
|
|||||||
<span>{{ t('userSettings.accountDetailsDescription') }}</span>
|
<span>{{ t('userSettings.accountDetailsDescription') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="details-grid">
|
<div
|
||||||
<div class="detail-row">
|
v-if="settingsError"
|
||||||
<span>{{ t('userSettings.alias') }}</span>
|
class="page-message error"
|
||||||
<strong>{{ alias }}</strong>
|
>
|
||||||
</div>
|
{{ settingsError }}
|
||||||
<div class="detail-row">
|
|
||||||
<span>{{ t('userSettings.fullName') }}</span>
|
|
||||||
<strong>{{ fullname }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="detail-row">
|
|
||||||
<span>{{ t('userSettings.email') }}</span>
|
|
||||||
<strong>{{ email }}</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="settingsStatus"
|
||||||
|
class="page-message success"
|
||||||
|
>
|
||||||
|
{{ settingsStatus }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
class="form-stack"
|
||||||
|
@submit.prevent="submitSettings"
|
||||||
|
>
|
||||||
|
<div class="details-grid">
|
||||||
|
<label class="field">
|
||||||
|
<span>{{ t('userSettings.firstname') }}</span>
|
||||||
|
<input
|
||||||
|
v-model="form.firstname"
|
||||||
|
type="text"
|
||||||
|
autocomplete="given-name"
|
||||||
|
:disabled="userProfileStore.isUpdating"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="field">
|
||||||
|
<span>{{ t('userSettings.lastname') }}</span>
|
||||||
|
<input
|
||||||
|
v-model="form.lastname"
|
||||||
|
type="text"
|
||||||
|
autocomplete="family-name"
|
||||||
|
:disabled="userProfileStore.isUpdating"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="field">
|
||||||
|
<span>{{ t('userSettings.alias') }}</span>
|
||||||
|
<input
|
||||||
|
v-model="form.alias"
|
||||||
|
type="text"
|
||||||
|
autocomplete="nickname"
|
||||||
|
:placeholder="fullname"
|
||||||
|
:disabled="userProfileStore.isUpdating"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="field">
|
||||||
|
<span>{{ t('userSettings.email') }}</span>
|
||||||
|
<input
|
||||||
|
v-model="form.email"
|
||||||
|
type="email"
|
||||||
|
autocomplete="email"
|
||||||
|
:disabled="userProfileStore.isUpdating"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button
|
||||||
|
class="primary-button"
|
||||||
|
type="submit"
|
||||||
|
:disabled="!canSave"
|
||||||
|
>
|
||||||
|
{{ userProfileStore.isUpdating ? t('common.saving') : t('userSettings.saveDetails') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ImageCropperDialog
|
<ImageCropperDialog
|
||||||
@@ -84,6 +207,7 @@
|
|||||||
:confirm-label="t('userSettings.savePortrait')"
|
:confirm-label="t('userSettings.savePortrait')"
|
||||||
:upload-label="t('userSettings.choosePortrait')"
|
:upload-label="t('userSettings.choosePortrait')"
|
||||||
:is-saving="isSavingPortrait"
|
:is-saving="isSavingPortrait"
|
||||||
|
:initial-url="userProfileStore.portraitUrl"
|
||||||
@save="savePortrait"
|
@save="savePortrait"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
@@ -107,8 +231,7 @@
|
|||||||
.page-header p,
|
.page-header p,
|
||||||
.panel-heading span,
|
.panel-heading span,
|
||||||
.hero-identity span,
|
.hero-identity span,
|
||||||
.hero-identity small,
|
.hero-identity small {
|
||||||
.detail-row span {
|
|
||||||
@apply text-sm leading-6;
|
@apply text-sm leading-6;
|
||||||
color: #526178;
|
color: #526178;
|
||||||
}
|
}
|
||||||
@@ -128,8 +251,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hero-identity strong,
|
.hero-identity strong,
|
||||||
.panel-heading strong,
|
.panel-heading strong {
|
||||||
.detail-row strong {
|
|
||||||
color: #172033;
|
color: #172033;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,10 +271,45 @@
|
|||||||
@apply grid gap-4 md:grid-cols-2;
|
@apply grid gap-4 md:grid-cols-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-row {
|
.form-stack {
|
||||||
@apply flex flex-col gap-1 rounded-[1.25rem] border p-4;
|
@apply flex flex-col gap-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
@apply flex flex-col gap-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field span {
|
||||||
|
@apply text-sm font-semibold;
|
||||||
|
color: #172033;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field input {
|
||||||
|
@apply rounded-[1rem] border px-4 py-3 text-sm;
|
||||||
background: #fffaf2;
|
background: #fffaf2;
|
||||||
border-color: rgba(23, 32, 51, 0.08);
|
border-color: rgba(23, 32, 51, 0.08);
|
||||||
|
color: #172033;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field input:disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
@apply flex justify-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-message {
|
||||||
|
@apply rounded-[1rem] border px-4 py-3 text-sm font-semibold;
|
||||||
|
background: rgba(15, 118, 110, 0.08);
|
||||||
|
border-color: rgba(15, 118, 110, 0.18);
|
||||||
|
color: #0f766e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-message.error {
|
||||||
|
background: rgba(185, 28, 28, 0.08);
|
||||||
|
border-color: rgba(185, 28, 28, 0.16);
|
||||||
|
color: #b91c1c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary-button {
|
.primary-button {
|
||||||
@@ -160,4 +317,9 @@
|
|||||||
background: #172033;
|
background: #172033;
|
||||||
color: #fffaf2;
|
color: #fffaf2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.primary-button:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -310,14 +310,24 @@
|
|||||||
"description": "Manage the portrait and account details shown inside the workspace.",
|
"description": "Manage the portrait and account details shown inside the workspace.",
|
||||||
"updatePortrait": "Update portrait",
|
"updatePortrait": "Update portrait",
|
||||||
"accountDetails": "Account details",
|
"accountDetails": "Account details",
|
||||||
"accountDetailsDescription": "Additional account editing fields can be added here next.",
|
"accountDetailsDescription": "Edit the profile details other workspace members see.",
|
||||||
|
"saveDetails": "Save details",
|
||||||
|
"saved": "Profile details saved",
|
||||||
|
"portraitSaved": "Portrait saved",
|
||||||
"alias": "Alias",
|
"alias": "Alias",
|
||||||
|
"firstname": "First name",
|
||||||
|
"lastname": "Last name",
|
||||||
"fullName": "Full name",
|
"fullName": "Full name",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"noEmail": "No email set",
|
"noEmail": "No email set",
|
||||||
"cropperTitle": "Update user portrait",
|
"cropperTitle": "Update user portrait",
|
||||||
"savePortrait": "Save portrait",
|
"savePortrait": "Save portrait",
|
||||||
"choosePortrait": "Choose portrait"
|
"choosePortrait": "Choose portrait",
|
||||||
|
"errors": {
|
||||||
|
"emailRequired": "Email is required.",
|
||||||
|
"saveFailed": "Profile details could not be saved.",
|
||||||
|
"portraitFailed": "Portrait could not be saved."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"workspaceSettings": {
|
"workspaceSettings": {
|
||||||
"eyebrow": "Settings",
|
"eyebrow": "Settings",
|
||||||
|
|||||||
@@ -310,14 +310,24 @@
|
|||||||
"description": "Gérez le portrait et les informations du compte affichés dans l'espace.",
|
"description": "Gérez le portrait et les informations du compte affichés dans l'espace.",
|
||||||
"updatePortrait": "Mettre à jour le portrait",
|
"updatePortrait": "Mettre à jour le portrait",
|
||||||
"accountDetails": "Détails du compte",
|
"accountDetails": "Détails du compte",
|
||||||
"accountDetailsDescription": "Des champs supplémentaires d'édition du compte peuvent être ajoutés ici ensuite.",
|
"accountDetailsDescription": "Modifiez les informations de profil visibles par les autres membres.",
|
||||||
|
"saveDetails": "Enregistrer les détails",
|
||||||
|
"saved": "Informations de profil enregistrées",
|
||||||
|
"portraitSaved": "Portrait enregistré",
|
||||||
"alias": "Alias",
|
"alias": "Alias",
|
||||||
|
"firstname": "Prénom",
|
||||||
|
"lastname": "Nom",
|
||||||
"fullName": "Nom complet",
|
"fullName": "Nom complet",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"noEmail": "Aucun email défini",
|
"noEmail": "Aucun email défini",
|
||||||
"cropperTitle": "Mettre à jour le portrait utilisateur",
|
"cropperTitle": "Mettre à jour le portrait utilisateur",
|
||||||
"savePortrait": "Enregistrer le portrait",
|
"savePortrait": "Enregistrer le portrait",
|
||||||
"choosePortrait": "Choisir un portrait"
|
"choosePortrait": "Choisir un portrait",
|
||||||
|
"errors": {
|
||||||
|
"emailRequired": "L'email est requis.",
|
||||||
|
"saveFailed": "Les informations de profil n'ont pas pu être enregistrées.",
|
||||||
|
"portraitFailed": "Le portrait n'a pas pu être enregistré."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"workspaceSettings": {
|
"workspaceSettings": {
|
||||||
"eyebrow": "Paramètres",
|
"eyebrow": "Paramètres",
|
||||||
|
|||||||
Reference in New Issue
Block a user