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

+88 -39
View File
@@ -1,4 +1,6 @@
<script setup lang="ts">
import { t } from '@/i18n'
import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
import MediaPicker from '@/components/MediaPicker.vue'
import MediaThumb from '@/components/MediaThumb.vue'
@@ -72,8 +74,20 @@ const DROP_ZONE_UI = {
const FOOTER_BUTTON_CLASS = 'h-11 justify-center text-sm sm:h-8 sm:w-auto sm:px-3'
const TAB_ITEMS = [
{ label: 'Upload', value: 'upload', icon: 'i-lucide-upload' },
{ label: 'Library', value: 'library', icon: 'i-lucide-images' },
{
get label() {
return t('Upload')
},
value: 'upload',
icon: 'i-lucide-upload',
},
{
get label() {
return t('Library')
},
value: 'library',
icon: 'i-lucide-images',
},
]
const open = defineModel<boolean>('open', { default: false })
@@ -147,7 +161,7 @@ const duplicateText = computed(() =>
: '',
)
const cancelLabel = computed(() => (phase.value === 'uploading' ? 'Cancel upload' : 'Cancel'))
const cancelLabel = computed(() => (phase.value === 'uploading' ? t('Cancel upload') : t('Cancel')))
const showCancel = computed(
() =>
@@ -163,7 +177,9 @@ const showCropActions = computed(() => !onLibraryTab.value && phase.value === 'c
const wideModal = computed(() => showTabs.value || phase.value === 'crop')
const modalTitle = computed(() => (phase.value === 'crop' ? 'Crop and resize' : 'Add an image'))
const modalTitle = computed(() =>
phase.value === 'crop' ? t('Crop and resize') : t('Add an image'),
)
const modalDescription = computed(() => {
if (phase.value !== 'crop' || !chosenFile.value) {
@@ -203,9 +219,12 @@ const outputSizeText = computed(() =>
formatEstimatedSize(estimateOutputBytes(outputSize.value.width, outputSize.value.height)),
)
const monoLine = computed(
() =>
`Uploads as ${outputSize.value.width} × ${outputSize.value.height} px · about ${outputSizeText.value}`,
const monoLine = computed(() =>
t('Uploads as {0} × {1} px · about {2}', [
outputSize.value.width,
outputSize.value.height,
outputSizeText.value,
]),
)
const targetWidthPixels = computed(() => widthPresetPixels(widthLabel.value))
@@ -214,9 +233,11 @@ const showNarrowNote = computed(
() => targetWidthPixels.value !== null && targetWidthPixels.value > cropWidth.value,
)
const narrowNoteText = computed(
() =>
`The crop is ${cropWidth.value} px wide, so it's uploaded at that size. Enlarging it would make it blurry.`,
const narrowNoteText = computed(() =>
t(
"The crop is {0} px wide, so it's uploaded at that size. Enlarging it would make it blurry.",
[cropWidth.value],
),
)
const dimsLabel = computed(() => `${cropWidth.value} × ${cropHeight.value}`)
@@ -505,12 +526,18 @@ function stagePixelScale(): number {
return boundsWidth.value / box.width
}
function pointFromEvent(event: MouseEvent | TouchEvent): { x: number; y: number } {
function pointFromEvent(event: MouseEvent | TouchEvent): {
x: number
y: number
} {
if ('touches' in event && event.touches.length > 0) {
return { x: event.touches[0]!.clientX, y: event.touches[0]!.clientY }
}
if ('changedTouches' in event && event.changedTouches.length > 0) {
return { x: event.changedTouches[0]!.clientX, y: event.changedTouches[0]!.clientY }
return {
x: event.changedTouches[0]!.clientX,
y: event.changedTouches[0]!.clientY,
}
}
const mouseEvent = event as MouseEvent
return { x: mouseEvent.clientX, y: mouseEvent.clientY }
@@ -548,7 +575,11 @@ function onFrameDragMove(event: MouseEvent | TouchEvent) {
const dx = (point.x - moveOrigin.x) * scale
const dy = (point.y - moveOrigin.y) * scale
rect.value = clampRect(
{ ...moveOrigin.rect, x: moveOrigin.rect.x + dx, y: moveOrigin.rect.y + dy },
{
...moveOrigin.rect,
x: moveOrigin.rect.x + dx,
y: moveOrigin.rect.y + dy,
},
boundsWidth.value,
boundsHeight.value,
)
@@ -775,11 +806,13 @@ onUnmounted(() => {
<template #label>
{{
phase === 'dragging-over'
? 'Drop to upload'
: 'Drop an image here, or choose a file'
? t('Drop to upload')
: t('Drop an image here, or choose a file')
}}
</template>
<template #description>PNG, JPEG, WEBP, SVG · max 10 MB</template>
<template #description>{{
t('PNG, JPEG, WEBP, SVG · max 10 MB')
}}</template>
</UFileUpload>
</div>
@@ -795,13 +828,17 @@ onUnmounted(() => {
class="h-13.5 w-18 border border-default"
/>
<div class="flex min-w-0 flex-col gap-0.5">
<p class="font-semibold text-highlighted">Already in your library</p>
<p class="text-xs text-toned">{{ duplicateText }}</p>
<p class="font-semibold text-highlighted">
{{ t('Already in your library') }}
</p>
<p class="text-xs text-toned">
{{ duplicateText }}
</p>
</div>
</div>
<div class="flex flex-col gap-2 md:flex-row">
<UButton class="h-11 justify-center md:h-8" @click="useExisting">
Use existing
{{ t('Use existing') }}
</UButton>
<UButton
color="neutral"
@@ -809,7 +846,7 @@ onUnmounted(() => {
class="h-11 justify-center border border-accented bg-default text-highlighted ring-0 md:h-8"
@click="uploadAnyway"
>
Upload anyway
{{ t('Upload anyway') }}
</UButton>
</div>
</div>
@@ -822,7 +859,9 @@ onUnmounted(() => {
<div
ref="imageBoxRef"
class="relative h-full max-w-full"
:style="{ aspectRatio: `${boundsWidth} / ${boundsHeight}` }"
:style="{
aspectRatio: `${boundsWidth} / ${boundsHeight}`,
}"
:aria-label="`${chosenFile?.name}, ${boundsWidth} × ${boundsHeight} px`"
role="img"
>
@@ -846,7 +885,11 @@ onUnmounted(() => {
data-testid="crop-frame"
tabindex="0"
role="group"
aria-label="Crop area. Arrow keys move it, Shift and arrow keys resize it."
:aria-label="
t(
'Crop area. Arrow keys move it, Shift and arrow keys resize it.',
)
"
class="absolute cursor-move touch-none outline outline-white"
:style="{
left: `${(rect.x / boundsWidth) * 100}%`,
@@ -900,7 +943,9 @@ onUnmounted(() => {
<div class="flex flex-col gap-1.5">
<div class="flex items-center gap-1">
<span class="flex-1 font-medium text-highlighted">Aspect ratio</span>
<span class="flex-1 font-medium text-highlighted">{{
t('Aspect ratio')
}}</span>
<UButton
color="neutral"
variant="ghost"
@@ -908,7 +953,7 @@ onUnmounted(() => {
icon="i-lucide-rotate-ccw-square"
@click="rotateImage"
>
Rotate
{{ t('Rotate') }}
</UButton>
<UButton
color="neutral"
@@ -917,12 +962,12 @@ onUnmounted(() => {
icon="i-lucide-undo-2"
@click="resetCrop"
>
Reset
{{ t('Reset') }}
</UButton>
</div>
<div
role="radiogroup"
aria-label="Aspect ratio"
:aria-label="t('Aspect ratio')"
class="flex w-full border border-accented sm:w-max"
>
<button
@@ -940,16 +985,16 @@ onUnmounted(() => {
"
@click="selectRatio(ratio)"
>
{{ ratio }}
{{ ratio === 'Original' || ratio === 'Free' ? t(ratio) : ratio }}
</button>
</div>
</div>
<div class="flex flex-col gap-1.5">
<span class="font-medium text-highlighted">Width</span>
<span class="font-medium text-highlighted">{{ t('Width') }}</span>
<div
role="radiogroup"
aria-label="Width"
:aria-label="t('Width')"
class="flex w-full border border-accented sm:w-max"
>
<button
@@ -967,7 +1012,7 @@ onUnmounted(() => {
"
@click="selectWidth(widthOption)"
>
{{ widthOption }}
{{ widthOption === 'Original' ? t(widthOption) : widthOption }}
</button>
</div>
<span class="font-mono text-xs text-muted">{{ monoLine }}</span>
@@ -992,7 +1037,7 @@ onUnmounted(() => {
:class="succeeded ? 'size-6 text-success' : 'size-6 text-dimmed'"
/>
<p class="text-sm text-highlighted">
<template v-if="succeeded">Image added</template>
<template v-if="succeeded">{{ t('Image added') }}</template>
<template v-else>{{ chosenFile?.name }} {{ uploadPercent }}%</template>
</p>
<UProgress
@@ -1014,15 +1059,19 @@ onUnmounted(() => {
class="flex flex-col items-center gap-3 rounded-md border border-error px-6 py-8 text-center"
>
<UIcon name="i-lucide-file-x" class="size-6 text-error" />
<p class="text-sm font-semibold text-highlighted">{{ rejectedTitle }}</p>
<p class="text-sm font-semibold text-highlighted">
{{ rejectedTitle }}
</p>
<p class="text-sm text-toned">{{ rejectedText }}</p>
</div>
</div>
<div v-else-if="phase === 'failed'" class="flex flex-col gap-1">
<p class="text-sm font-semibold text-highlighted">Upload failed</p>
<p class="text-sm font-semibold text-highlighted">
{{ t('Upload failed') }}
</p>
<p class="text-sm text-toned">
The server didn't accept the file. Your article hasn't changed.
{{ t("The server didn't accept the file. Your article hasn't changed.") }}
</p>
</div>
</div>
@@ -1047,7 +1096,7 @@ onUnmounted(() => {
:disabled="!selected"
@click="insertSelected"
>
Insert image
{{ t('Insert image') }}
</UButton>
<UButton
v-if="showRejectedActions"
@@ -1055,7 +1104,7 @@ onUnmounted(() => {
:class="FOOTER_BUTTON_CLASS"
@click="chooseAnotherFile"
>
Choose another file
{{ t('Choose another file') }}
</UButton>
<UButton
v-if="showFailedActions"
@@ -1063,7 +1112,7 @@ onUnmounted(() => {
:class="FOOTER_BUTTON_CLASS"
@click="tryAgain"
>
Try again
{{ t('Try again') }}
</UButton>
<template v-if="showCropActions">
<UButton
@@ -1074,10 +1123,10 @@ onUnmounted(() => {
class="text-highlighted"
@click="chooseAnotherFile"
>
Choose another file
{{ t('Choose another file') }}
</UButton>
<UButton block :class="FOOTER_BUTTON_CLASS" @click="confirmCropUpload">
Upload image
{{ t('Upload image') }}
</UButton>
</template>
</div>