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,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>
|
||||
|
||||
Reference in new issue
Block a user