feat(SPA): i18n internationzalization, english and french

This commit is contained in:
fsylvain committed 2026-10-03 01:53:49 +02:00
1 parent 7f1e8e572d
commit 721311a643
88 files changed
+4438 -1148

No files matched your search

+90 -42
View File
@@ -1,7 +1,9 @@
<script setup lang="ts">
import { t } from '@/i18n'
import { computed, nextTick, ref, watch } from 'vue'
import { onBeforeRouteLeave } from 'vue-router'
import { useOverlay, useToast } from '@nuxt/ui/composables'
import { useLocalizedOverlay, useLocalizedToast } from '@/i18n/presentation'
import CharacterCounter from '@/components/CharacterCounter.vue'
import ConfirmDialog from '@/components/ConfirmDialog.vue'
import DashboardShell from '@/components/DashboardShell.vue'
@@ -26,8 +28,8 @@ const SKELETON_CARDS = [
type ResetState = 'idle' | 'sending' | 'sent' | 'limited' | 'error'
const session = useSessionStore()
const toast = useToast()
const overlay = useOverlay()
const toast = useLocalizedToast()
const overlay = useLocalizedOverlay()
const { request } = useApi()
const retryAfter = useRetryAfter()
@@ -81,34 +83,46 @@ const showUnsaved = computed(() => dirty.value && !saving.value)
const statusText = computed(() => {
if (showUnsaved.value) {
return 'Unsaved changes'
return t('Unsaved changes')
}
return savedName.value
? `You appear as “${savedName.value}”.`
: `You appear as “${username.value}”, your username.`
? t('You appear as “{0}”.', [savedName.value])
: t('You appear as “{0}”, your username.', [username.value])
})
const inputUi = { base: 'h-11 text-lg lg:h-10 lg:text-base' }
const readOnlyUi = { base: 'h-11 text-lg disabled:text-toned lg:h-10 lg:text-base' }
const readOnlyUi = {
base: 'h-11 text-lg disabled:text-toned lg:h-10 lg:text-base',
}
const readOnlyLockUi = { trailingIcon: 'size-3.5 text-dimmed' }
const resetNotes = {
sent: {
color: 'success',
icon: 'i-lucide-mail-check',
title: 'Reset link sent',
get title() {
return t('Reset link sent')
},
},
limited: {
color: 'warning',
icon: 'i-lucide-clock-alert',
title: 'Too many requests',
description: "You've asked for several links recently. Wait a few minutes, then try again.",
get title() {
return t('Too many requests')
},
get description() {
return t("You've asked for several links recently. Wait a few minutes, then try again.")
},
},
error: {
color: 'error',
icon: 'i-lucide-circle-x',
title: "Couldn't send the link",
description: 'Check your connection and try again.',
get title() {
return t("Couldn't send the link")
},
get description() {
return t('Check your connection and try again.')
},
},
} as const
@@ -116,7 +130,9 @@ const resetNote = computed(() => {
if (resetState.value === 'sent') {
return {
...resetNotes.sent,
description: `Check ${email.value}. The link works once, within 1 hour.`,
get description() {
return t('Check {0}. The link works once, within 1 hour.', [email.value])
},
}
}
if (resetState.value === 'limited' || resetState.value === 'error') {
@@ -127,9 +143,9 @@ const resetNote = computed(() => {
const resetButtonLabel = computed(() => {
if (resetState.value === 'sending') {
return 'Sending…'
return t('Sending…')
}
return resetState.value === 'sent' ? 'Send again' : 'Send reset link'
return resetState.value === 'sent' ? t('Send again') : t('Send reset link')
})
const resetBusy = computed(() => resetState.value === 'sending')
@@ -140,15 +156,19 @@ function focusField() {
}
function announceSaved(displayName: string) {
toast.add({
toast.add(() => ({
id: 'display-name-saved',
color: 'success',
icon: 'i-lucide-circle-check',
title: displayName ? 'Display name saved' : 'Display name removed',
description: displayName
? `You now appear as ${displayName} across RenewCMS and in emails.`
: `You now appear as ${username.value}, your username.`,
})
get title() {
return displayName ? t('Display name saved') : t('Display name removed')
},
get description() {
return displayName
? t('You now appear as {0} across RenewCMS and in emails.', [displayName])
: t('You now appear as {0}, your username.', [username.value])
},
}))
}
function isHandledGlobally(error: unknown) {
@@ -220,11 +240,21 @@ onBeforeRouteLeave(async () => {
const keepEditing = await overlay
.create(ConfirmDialog, {
props: {
title: 'Leave without saving?',
description: "Your display name change hasn't been saved.",
confirmLabel: 'Keep editing',
cancelLabel: 'Discard changes',
pendingLabel: 'Keep editing',
get title() {
return t('Leave without saving?')
},
get description() {
return t("Your display name change hasn't been saved.")
},
get confirmLabel() {
return t('Keep editing')
},
get cancelLabel() {
return t('Discard changes')
},
get pendingLabel() {
return t('Keep editing')
},
tone: 'neutral',
action: async () => {},
dismissResolvesTo: 'confirm',
@@ -239,8 +269,12 @@ onBeforeRouteLeave(async () => {
<DashboardShell render-while-checking>
<div class="flex max-w-164 flex-col gap-5">
<div class="flex flex-col gap-0.5">
<h1 class="text-xl font-semibold text-highlighted">Profile</h1>
<p class="text-muted">Your account on this RenewCMS workspace.</p>
<h1 class="text-xl font-semibold text-highlighted">
{{ t('Profile') }}
</h1>
<p class="text-muted">
{{ t('Your account on this RenewCMS workspace.') }}
</p>
</div>
<div v-if="!account" aria-busy="true" class="flex flex-col gap-4">
@@ -262,8 +296,12 @@ onBeforeRouteLeave(async () => {
<template v-else>
<UForm :state="{ displayName: field }" @submit="onSave">
<ProfileCard
title="Display name"
description="Shown instead of your username across the back-office and in the emails RenewCMS sends. Leave it empty to use your username."
:title="t('Display name')"
:description="
t(
'Shown instead of your username across the back-office and in the emails RenewCMS sends. Leave it empty to use your username.',
)
"
>
<UAlert
v-if="saveFailed"
@@ -271,19 +309,20 @@ onBeforeRouteLeave(async () => {
color="error"
variant="soft"
icon="i-lucide-circle-x"
:description="SAVE_FAILED_COPY"
:description="t(SAVE_FAILED_COPY)"
:close="false"
:ui="{ root: 'gap-2 px-3 py-2.5' }"
/>
<UFormField name="displayName" :error="showFieldError">
<template #label>
Display name <span class="font-normal text-muted">(optional)</span>
{{ t('Display name') }}
<span class="font-normal text-muted">{{ t('(optional)') }}</span>
</template>
<UInput
ref="inputRef"
v-model="field"
placeholder="e.g. Marta Alves"
:placeholder="t('e.g. Marta Alves')"
autocomplete="off"
:disabled="saving"
class="w-full"
@@ -297,7 +336,7 @@ onBeforeRouteLeave(async () => {
class="flex flex-1 items-center gap-1.5 text-xs/5 text-error"
>
<UIcon name="i-lucide-circle-alert" class="size-3.5 shrink-0" />
{{ DISPLAY_NAME_LIMIT_COPY }}
{{ t(DISPLAY_NAME_LIMIT_COPY) }}
</span>
<span v-else class="flex-1" />
<CharacterCounter
@@ -322,18 +361,22 @@ onBeforeRouteLeave(async () => {
:disabled="!saveEnabled && !saving"
class="h-11 shrink-0 px-3.5 lg:h-8"
>
{{ saving ? 'Saving…' : 'Save' }}
{{ saving ? t('Saving…') : t('Save') }}
</UButton>
</template>
</ProfileCard>
</UForm>
<ProfileCard
title="Account"
description="Set when your account was created. You log in with your username, even after adding a display name."
:title="t('Account')"
:description="
t(
'Set when your account was created. You log in with your username, even after adding a display name.',
)
"
>
<div class="grid gap-3.5 lg:grid-cols-[minmax(0,1fr)_minmax(0,1.4fr)]">
<UFormField label="Username">
<UFormField :label="t('Username')">
<UInput
:model-value="username"
disabled
@@ -342,7 +385,7 @@ onBeforeRouteLeave(async () => {
:ui="{ ...readOnlyUi, ...readOnlyLockUi }"
/>
</UFormField>
<UFormField label="Email">
<UFormField :label="t('Email')">
<UInput
:model-value="email"
disabled
@@ -355,11 +398,16 @@ onBeforeRouteLeave(async () => {
</ProfileCard>
<ProfileCard
title="Password"
:description="`To change it, we'll email a reset link to ${email}. The link works once, within 1 hour.`"
:title="t('Password')"
:description="
t(
'To change it, we\'ll email a reset link to {0}. The link works once, within 1 hour.',
[email],
)
"
>
<div class="flex flex-col gap-3 lg:flex-row lg:items-end">
<UFormField label="Password" class="min-w-0 flex-1">
<UFormField :label="t('Password')" class="min-w-0 flex-1">
<UInput
:model-value="PASSWORD_MASK"
disabled