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

+60 -57
View File
@@ -1,4 +1,6 @@
<script setup lang="ts">
import { t, translate } from '@/i18n'
import { computed, nextTick, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import AuthShell from '@/components/AuthShell.vue'
@@ -16,37 +18,58 @@ interface AlertState {
const wrongCredentialsAlert: AlertState = {
tone: 'error',
icon: 'i-lucide-circle-x',
title: "That username and password don't match",
description: 'Check both and try again. Passwords are case-sensitive.',
get title() {
return t("That username and password don't match")
},
get description() {
return t('Check both and try again. Passwords are case-sensitive.')
},
}
const tooManyAttemptsAlert: AlertState = {
tone: 'info',
icon: 'i-lucide-timer',
title: 'Too many attempts',
description: 'For your security, wait a moment before trying again.',
get title() {
return t('Too many attempts')
},
get description() {
return t('For your security, wait a moment before trying again.')
},
}
const serverErrorAlert: AlertState = {
tone: 'error',
icon: 'i-lucide-circle-x',
title: "Couldn't reach RenewCMS",
description: 'Check your connection and try again.',
get title() {
return t("Couldn't reach RenewCMS")
},
get description() {
return t('Check your connection and try again.')
},
}
const sessionExpiredAlert: AlertState = {
tone: 'info',
icon: 'i-lucide-clock',
title: 'Your session expired',
description:
'Log in again to pick up where you left off. Unsaved changes are kept on this device.',
get title() {
return t('Your session expired')
},
get description() {
return t(
'Log in again to pick up where you left off. Unsaved changes are kept on this device.',
)
},
}
const passwordChangedAlert: AlertState = {
tone: 'success',
icon: 'i-lucide-circle-check',
title: 'Password changed',
description: 'Log in with your new password.',
get title() {
return t('Password changed')
},
get description() {
return t('Log in with your new password.')
},
}
const route = useRoute()
@@ -63,8 +86,7 @@ const passwordError = ref('')
const submitting = ref(false)
const passwordShown = ref(false)
const initialReason =
typeof route.query.reason === 'string' ? route.query.reason : null
const initialReason = typeof route.query.reason === 'string' ? route.query.reason : null
const alert = ref<AlertState | null>(
initialReason === 'expired'
@@ -74,23 +96,19 @@ const alert = ref<AlertState | null>(
: null,
)
const alertRole = computed(() =>
alert.value?.tone === 'error' ? 'alert' : 'status',
)
const alertRole = computed(() => (alert.value?.tone === 'error' ? 'alert' : 'status'))
const submitLabel = computed(() => {
if (submitting.value) {
return 'Logging in…'
return t('Logging in…')
}
if (retryAfter.active.value) {
return `Try again in ${retryAfter.formatted.value}`
return t('Try again in {0}', [retryAfter.formatted.value])
}
return 'Log in'
return t('Log in')
})
const submitDisabled = computed(
() => submitting.value || retryAfter.active.value,
)
const submitDisabled = computed(() => submitting.value || retryAfter.active.value)
async function focusField(selector: string) {
await nextTick()
@@ -103,7 +121,7 @@ function focusPassword() {
}
function focusUsername() {
focusField('input[placeholder="Your username"]')
focusField('input[autocomplete="username"]')
}
function focusFirstInvalid() {
@@ -117,18 +135,15 @@ function focusFirstInvalid() {
}
function validate() {
usernameError.value =
username.value.trim().length < 3 ? 'Use at least 3 characters.' : ''
passwordError.value =
password.value.length === 0 ? 'Enter your password.' : ''
usernameError.value = username.value.trim().length < 3 ? 'Use at least 3 characters.' : ''
passwordError.value = password.value.length === 0 ? 'Enter your password.' : ''
return !usernameError.value && !passwordError.value
}
async function afterLoginSuccess() {
await session.retry()
const redirect =
typeof route.query.redirect === 'string' ? route.query.redirect : null
const redirect = typeof route.query.redirect === 'string' ? route.query.redirect : null
if (redirect) {
await router.push(redirect)
return
@@ -193,9 +208,7 @@ onMounted(async () => {
}
try {
const status = await api.request<{ open: boolean }>(
'/v1/auth/registration',
)
const status = await api.request<{ open: boolean }>('/v1/auth/registration')
registrationOpen.value = status.open
} catch {
registrationOpen.value = false
@@ -205,8 +218,8 @@ onMounted(async () => {
<template>
<AuthShell
title="Log in"
description="Welcome back. Log in to manage your articles."
:title="t('Log in')"
:description="t('Welcome back. Log in to manage your articles.')"
>
<template v-if="alert" #notice>
<UAlert
@@ -221,15 +234,11 @@ onMounted(async () => {
</template>
<template #default="{ controlSize, controlClass, inputClass }">
<UForm
:state="{ username, password }"
class="flex flex-col gap-4.5"
@submit="onSubmit"
>
<UForm :state="{ username, password }" class="flex flex-col gap-4.5" @submit="onSubmit">
<UFormField
label="Username"
:label="t('Username')"
name="username"
:error="usernameError || undefined"
:error="translate(usernameError) || undefined"
:size="controlSize"
>
<template v-if="usernameError" #error="{ error }">
@@ -238,12 +247,12 @@ onMounted(async () => {
name="i-lucide-circle-alert"
class="size-3.5 shrink-0 text-error"
/>
{{ error }}
{{ t(error) }}
</span>
</template>
<UInput
v-model="username"
placeholder="Your username"
:placeholder="t('Your username')"
autocomplete="username"
:disabled="submitDisabled"
variant="outline"
@@ -256,9 +265,9 @@ onMounted(async () => {
</UFormField>
<UFormField
label="Password"
:label="t('Password')"
name="password"
:error="passwordError || undefined"
:error="translate(passwordError) || undefined"
:size="controlSize"
>
<template #hint>
@@ -266,7 +275,7 @@ onMounted(async () => {
to="/register/reset/request"
class="text-toned text-sm underline underline-offset-[3px]"
>
Forgot password?
{{ t('Forgot password?') }}
</RouterLink>
</template>
<template v-if="passwordError" #error="{ error }">
@@ -275,7 +284,7 @@ onMounted(async () => {
name="i-lucide-circle-alert"
class="size-3.5 shrink-0 text-error"
/>
{{ error }}
{{ t(error) }}
</span>
</template>
<UInput
@@ -295,20 +304,14 @@ onMounted(async () => {
>
<template #trailing>
<UButton
:icon="
passwordShown
? 'i-lucide-eye-off'
: 'i-lucide-eye'
"
:icon="passwordShown ? 'i-lucide-eye-off' : 'i-lucide-eye'"
variant="ghost"
color="neutral"
size="sm"
square
class="size-8 justify-center text-toned"
:aria-label="
passwordShown
? 'Hide password'
: 'Show password'
passwordShown ? t('Hide password') : t('Show password')
"
:aria-pressed="passwordShown"
@click="passwordShown = !passwordShown"
@@ -332,12 +335,12 @@ onMounted(async () => {
<template v-if="registrationOpen" #footer>
<span class="flex flex-wrap items-center gap-1.5">
First time here?
{{ t('First time here?') }}
<RouterLink
to="/register"
class="font-medium text-highlighted underline underline-offset-3"
>
Create the owner account
{{ t('Create the owner account') }}
</RouterLink>
</span>
</template>