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

+50 -31
View File
@@ -1,6 +1,9 @@
<script setup lang="ts">
import { t, locale, formatNumber } from '@/i18n'
import { formatAbsoluteDateFromIso } from '@/lib/relativeDate'
import { computed } from 'vue'
import { useToast } from '@nuxt/ui/composables'
import { useLocalizedToast } from '@/i18n/presentation'
import type { Article } from '@/composables/useArticles'
import { bodyImageCount } from '@/lib/bodyImageIds'
@@ -8,7 +11,7 @@ const props = defineProps<{ article: Article }>()
const emit = defineEmits<{ 'copy-url': [] }>()
const toast = useToast()
const toast = useLocalizedToast()
const open = defineModel<boolean>('open', { default: false })
@@ -43,9 +46,11 @@ function formatDetailDate(iso: string, relative = false) {
const hours = date.getHours().toString().padStart(2, '0')
const minutes = date.getMinutes().toString().padStart(2, '0')
if (relative && isSameDay(date, new Date())) {
return `Today, ${hours}:${minutes}`
return t('Today, {0}:{1}', [hours, minutes])
}
return `${date.getDate()} ${MONTH_NAMES[date.getMonth()]} ${date.getFullYear()}, ${hours}:${minutes}`
return locale.value === 'fr'
? `${formatAbsoluteDateFromIso(iso)}, ${hours}:${minutes}`
: `${date.getDate()} ${MONTH_NAMES[date.getMonth()]} ${date.getFullYear()}, ${hours}:${minutes}`
}
const createdLabel = computed(() => formatDetailDate(props.article.created_at))
@@ -54,29 +59,37 @@ const publicUrl = computed(() => `/v1/article/${props.article.id}`)
const imagesCount = computed(() => bodyImageCount(props.article.body))
const apiNote = computed(() =>
props.article.is_online
? 'Live. Your website can read this article.'
: 'Returns 404 until the article is online.',
? t('Live. Your website can read this article.')
: t('Returns 404 until the article is online.'),
)
async function copyUrl() {
const url = `${window.location.origin}${publicUrl.value}`
try {
await navigator.clipboard.writeText(url)
toast.add({
toast.add(() => ({
id: `article-link-copied-${props.article.id}`,
color: 'success',
icon: 'i-lucide-circle-check',
title: 'Link copied',
description: `The API URL for "${props.article.title}" is on your clipboard.`,
})
get title() {
return t('Link copied')
},
get description() {
return t('The API URL for \u0022{0}\u0022 is on your clipboard.', [
props.article.title,
])
},
}))
emit('copy-url')
} catch {
toast.add({
toast.add(() => ({
id: `article-link-copy-failed-${props.article.id}`,
color: 'error',
icon: 'i-lucide-circle-x',
title: "Couldn't copy the link",
})
get title() {
return t("Couldn't copy the link")
},
}))
}
}
</script>
@@ -93,20 +106,24 @@ async function copyUrl() {
}"
>
<template #header>
<h2 class="text-base font-semibold text-highlighted">Details</h2>
<h2 class="text-base font-semibold text-highlighted">
{{ t('Details') }}
</h2>
</template>
<dl class="grid grid-cols-[auto_1fr] gap-x-3 gap-y-2 text-sm">
<dt class="text-muted">Created</dt>
<dt class="text-muted">{{ t('Created') }}</dt>
<dd class="text-highlighted">{{ createdLabel }}</dd>
<dt class="text-muted">Updated</dt>
<dt class="text-muted">{{ t('Updated') }}</dt>
<dd class="text-highlighted">{{ updatedLabel }}</dd>
<dt class="text-muted">ID</dt>
<dd class="font-mono text-highlighted">{{ article.id }}</dd>
<dt class="text-muted">Images</dt>
<dd class="text-highlighted">{{ imagesCount }}</dd>
<dt class="text-muted">{{ t('Images') }}</dt>
<dd class="text-highlighted">
{{ formatNumber(imagesCount) }}
</dd>
</dl>
<div class="flex flex-col gap-1.5 text-sm">
<p class="text-muted">Public API URL</p>
<p class="text-muted">{{ t('Public API URL') }}</p>
<div class="flex items-center gap-1.5 bg-elevated px-2 py-1.5 font-mono text-xs">
<span class="flex-1">{{ publicUrl }}</span>
<UButton
@@ -114,7 +131,7 @@ async function copyUrl() {
color="neutral"
variant="ghost"
size="sm"
aria-label="Copy the public API URL"
:aria-label="t('Copy the public API URL')"
class="size-5 p-0"
@click="copyUrl"
/>
@@ -122,28 +139,30 @@ async function copyUrl() {
<p class="text-xs/[18px] text-muted">{{ apiNote }}</p>
</div>
<div class="flex flex-col gap-1 border-t border-default pt-3 text-sm">
<p class="text-muted">Title</p>
<p class="text-xs/[18px] text-toned">{{ TITLE_NOTE }}</p>
<p class="text-muted">{{ t('Title') }}</p>
<p class="text-xs/[18px] text-toned">{{ t(TITLE_NOTE) }}</p>
</div>
</UCard>
<USlideover v-model:open="open" side="right" title="Details" class="lg:hidden">
<USlideover v-model:open="open" side="right" :title="t('Details')" class="lg:hidden">
<template #body>
<div class="flex flex-col gap-4 text-sm">
<dl class="grid grid-cols-[auto_1fr] gap-x-3 gap-y-2">
<dt class="text-muted">Created</dt>
<dt class="text-muted">{{ t('Created') }}</dt>
<dd class="text-highlighted">{{ createdLabel }}</dd>
<dt class="text-muted">Updated</dt>
<dt class="text-muted">{{ t('Updated') }}</dt>
<dd class="text-highlighted">{{ updatedLabel }}</dd>
<dt class="text-muted">ID</dt>
<dd class="font-mono text-highlighted">
{{ article.id }}
</dd>
<dt class="text-muted">Images</dt>
<dd class="text-highlighted">{{ imagesCount }}</dd>
<dt class="text-muted">{{ t('Images') }}</dt>
<dd class="text-highlighted">
{{ formatNumber(imagesCount) }}
</dd>
</dl>
<div class="flex flex-col gap-1.5">
<p class="text-muted">Public API URL</p>
<p class="text-muted">{{ t('Public API URL') }}</p>
<div
class="flex items-center gap-1.5 bg-elevated px-2 py-1.5 font-mono text-xs"
>
@@ -153,7 +172,7 @@ async function copyUrl() {
color="neutral"
variant="ghost"
size="sm"
aria-label="Copy the public API URL"
:aria-label="t('Copy the public API URL')"
class="size-5 p-0"
@click="copyUrl"
/>
@@ -161,9 +180,9 @@ async function copyUrl() {
<p class="text-xs/[18px] text-muted">{{ apiNote }}</p>
</div>
<div class="flex flex-col gap-1 border-t border-default pt-3">
<p class="text-muted">Title</p>
<p class="text-muted">{{ t('Title') }}</p>
<p class="text-xs/[18px] text-toned">
{{ TITLE_NOTE }}
{{ t(TITLE_NOTE) }}
</p>
</div>
</div>