feat(SPA): i18n internationzalization, english and french
This commit is contained in:
1 parent
7f1e8e572d
commit
721311a643
88 files changed
+4438
-1148
No files matched your search
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user