feat: Add internationalization (i18n) support
Implemented multi-language support for the application: - Added @nuxtjs/i18n plugin configuration - Created locale files for English (en-US) and Chinese (zh-CN) - Updated components to use translation keys - Added language switcher in header - Configured VSCode i18n-ally settings - Prepared translation infrastructure for future language expansions
This commit is contained in:
parent
31731c5bed
commit
e563d55852
36 changed files with 1048 additions and 356 deletions
19
.vscode/settings.json
vendored
19
.vscode/settings.json
vendored
|
|
@ -46,5 +46,22 @@
|
|||
"scss",
|
||||
"pcss",
|
||||
"postcss"
|
||||
]
|
||||
],
|
||||
"i18n-ally.dirStructure": "auto",
|
||||
"i18n-ally.localesPaths": [
|
||||
"i18n/locales"
|
||||
],
|
||||
"i18n-ally.extract.keygenStyle": "camelCase",
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.enabledParsers": [
|
||||
"json"
|
||||
],
|
||||
"i18n-ally.namespace": true,
|
||||
"i18n-ally.sortKeys": true,
|
||||
"i18n-ally.sourceLanguage": "en-US",
|
||||
"i18n-ally.displayLanguage": "en-US",
|
||||
"i18n-ally.parsers.typescript.compilerOptions": {
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"i18n-ally.enabledFrameworks": ["vue"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ const colorMode = useColorMode()
|
|||
<Moon
|
||||
class="w-5 h-5 transition-all scale-0 dark:scale-100"
|
||||
/>
|
||||
<span class="sr-only">Toggle theme</span>
|
||||
<span class="sr-only">{{ $t('theme.toggle') }}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
|
|
@ -26,21 +26,21 @@ const colorMode = useColorMode()
|
|||
@click="colorMode.preference = 'light'"
|
||||
>
|
||||
<Sun class="w-4 h-4 mr-1" />
|
||||
Light
|
||||
{{ $t('theme.light') }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
class="cursor-pointer"
|
||||
@click="colorMode.preference = 'dark'"
|
||||
>
|
||||
<Moon class="w-4 h-4 mr-1" />
|
||||
Dark
|
||||
{{ $t('theme.dark') }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
class="cursor-pointer"
|
||||
@click="colorMode.preference = 'system'"
|
||||
>
|
||||
<Laptop class="w-4 h-4 mr-1" />
|
||||
System
|
||||
{{ $t('theme.system') }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ const { title } = useAppConfig()
|
|||
:as="NuxtLink"
|
||||
to="/dashboard"
|
||||
>
|
||||
Dashboard
|
||||
{{ $t('dashboard.title') }}
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ onBeforeUnmount(() => {
|
|||
<Card>
|
||||
<CardHeader class="flex flex-row justify-between items-center pb-2 space-y-0">
|
||||
<CardTitle class="text-sm font-medium">
|
||||
Visits
|
||||
{{ $t('dashboard.visits') }}
|
||||
</CardTitle>
|
||||
<MousePointerClick class="w-4 h-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
|
|
@ -55,7 +55,7 @@ onBeforeUnmount(() => {
|
|||
<Card>
|
||||
<CardHeader class="flex flex-row justify-between items-center pb-2 space-y-0">
|
||||
<CardTitle class="text-sm font-medium">
|
||||
Visitors
|
||||
{{ $t('dashboard.visitors') }}
|
||||
</CardTitle>
|
||||
<Users class="w-4 h-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
|
|
@ -66,7 +66,7 @@ onBeforeUnmount(() => {
|
|||
<Card>
|
||||
<CardHeader class="flex flex-row justify-between items-center pb-2 space-y-0">
|
||||
<CardTitle class="text-sm font-medium">
|
||||
Referers
|
||||
{{ $t('dashboard.referers') }}
|
||||
</CardTitle>
|
||||
<Flame class="w-4 h-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
|
|
|
|||
|
|
@ -97,32 +97,32 @@ onBeforeMount(() => {
|
|||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="today">
|
||||
Today
|
||||
{{ $t('dashboard.date_picker.today') }}
|
||||
</SelectItem>
|
||||
<SelectItem value="last-24h">
|
||||
Last 24 hours
|
||||
{{ $t('dashboard.date_picker.last_24h') }}
|
||||
</SelectItem>
|
||||
<SelectSeparator />
|
||||
<SelectItem value="this-week">
|
||||
This week
|
||||
{{ $t('dashboard.date_picker.this_week') }}
|
||||
</SelectItem>
|
||||
<SelectItem value="last-7d">
|
||||
Last 7 days
|
||||
{{ $t('dashboard.date_picker.last_7d') }}
|
||||
</SelectItem>
|
||||
<SelectSeparator />
|
||||
<SelectItem value="this-month">
|
||||
This month
|
||||
{{ $t('dashboard.date_picker.this_month') }}
|
||||
</SelectItem>
|
||||
<SelectItem value="last-30d">
|
||||
Last 30 days
|
||||
{{ $t('dashboard.date_picker.last_30d') }}
|
||||
</SelectItem>
|
||||
<SelectSeparator />
|
||||
<SelectItem value="last-90d">
|
||||
Last 90 days
|
||||
{{ $t('dashboard.date_picker.last_90d') }}
|
||||
</SelectItem>
|
||||
<SelectSeparator />
|
||||
<SelectItem value="custom">
|
||||
Custom
|
||||
{{ $t('dashboard.date_picker.custom') }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
|
@ -130,7 +130,7 @@ onBeforeMount(() => {
|
|||
<Dialog v-model:open="openCustomDateRange">
|
||||
<DialogContent class="w-auto max-w-[95svw] max-h-[95svh] md:max-w-screen-md grid-rows-[auto_minmax(0,1fr)_auto]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Custom Date</DialogTitle>
|
||||
<DialogTitle>{{ $t('dashboard.date_picker.custom_title') }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Tabs
|
||||
default-value="range"
|
||||
|
|
@ -138,10 +138,10 @@ onBeforeMount(() => {
|
|||
<div class="flex justify-center">
|
||||
<TabsList>
|
||||
<TabsTrigger value="date">
|
||||
Date
|
||||
{{ $t('dashboard.date_picker.single_date') }}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="range">
|
||||
Date Range
|
||||
{{ $t('dashboard.date_picker.date_range') }}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ onBeforeMount(() => {
|
|||
#left
|
||||
>
|
||||
<h3 class="text-xl font-bold leading-10">
|
||||
{{ link.slug }}'s Stats
|
||||
{{ link.slug }} {{ $t('dashboard.stats') }}
|
||||
</h3>
|
||||
</template>
|
||||
<DashboardDatePicker @update:date-range="changeDate" />
|
||||
|
|
|
|||
|
|
@ -16,15 +16,15 @@ function logOut() {
|
|||
</AlertDialogTrigger>
|
||||
<AlertDialogContent class="max-w-[95svw] max-h-[95svh] md:max-w-lg grid-rows-[auto_minmax(0,1fr)_auto]">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>LogOut ?</AlertDialogTitle>
|
||||
<AlertDialogTitle>{{ $t('logout.title') }}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to log out ?
|
||||
{{ $t('logout.confirm') }}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogCancel>{{ $t('common.cancel') }}</AlertDialogCancel>
|
||||
<AlertDialogAction @click="logOut">
|
||||
LogOut
|
||||
{{ $t('logout.action') }}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@ const route = useRoute()
|
|||
<TabsTrigger
|
||||
value="/dashboard/links"
|
||||
>
|
||||
Links
|
||||
{{ $t('nav.links') }}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="/dashboard/analysis">
|
||||
Analysis
|
||||
{{ $t('nav.analysis') }}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ function formatTime(tick) {
|
|||
<template>
|
||||
<Card class="px-0 py-6 md:px-6">
|
||||
<CardTitle class="px-6 md:px-0">
|
||||
Views
|
||||
{{ $t('dashboard.views') }}
|
||||
</CardTitle>
|
||||
<component
|
||||
:is="chart"
|
||||
|
|
|
|||
|
|
@ -29,15 +29,15 @@ async function deleteLink() {
|
|||
</AlertDialogTrigger>
|
||||
<AlertDialogContent class="max-w-[95svw] max-h-[95svh] md:max-w-lg grid-rows-[auto_minmax(0,1fr)_auto]">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogTitle>{{ $t('links.delete_confirm_title') }}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will really delete your link from servers.
|
||||
{{ $t('links.delete_confirm_desc') }}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogCancel>{{ $t('common.cancel') }}</AlertDialogCancel>
|
||||
<AlertDialogAction @click="deleteLink">
|
||||
Continue
|
||||
{{ $t('common.continue') }}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
|
|
|||
|
|
@ -116,10 +116,10 @@ async function onSubmit(formData) {
|
|||
dialogOpen.value = false
|
||||
emit('update:link', newLink, isEdit ? 'edit' : 'create')
|
||||
if (isEdit) {
|
||||
toast('Link updated successfully')
|
||||
toast(t('links.update_success'))
|
||||
}
|
||||
else {
|
||||
toast('Link created successfully')
|
||||
toast(t('links.create_success'))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -135,19 +135,19 @@ const { previewMode } = useRuntimeConfig().public
|
|||
variant="outline"
|
||||
@click="randomSlug"
|
||||
>
|
||||
Create Link
|
||||
{{ $t('links.create') }}
|
||||
</Button>
|
||||
</slot>
|
||||
</DialogTrigger>
|
||||
<DialogContent class="max-w-[95svw] max-h-[95svh] md:max-w-lg grid-rows-[auto_minmax(0,1fr)_auto]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ link.id ? 'Edit Link' : 'Create Link' }}</DialogTitle>
|
||||
<DialogTitle>{{ link.id ? $t('links.edit') : $t('links.create') }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<p
|
||||
v-if="previewMode"
|
||||
class="text-sm text-muted-foreground"
|
||||
>
|
||||
The preview mode link is valid for up to 24 hours.
|
||||
{{ $t('links.preview_mode_tip') }}
|
||||
</p>
|
||||
<AutoForm
|
||||
class="px-2 space-y-2 overflow-y-auto"
|
||||
|
|
@ -185,11 +185,11 @@ const { previewMode } = useRuntimeConfig().public
|
|||
variant="secondary"
|
||||
class="mt-2 sm:mt-0"
|
||||
>
|
||||
Close
|
||||
{{ $t('common.close') }}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button type="submit">
|
||||
Save
|
||||
{{ $t('common.save') }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</AutoForm>
|
||||
|
|
|
|||
|
|
@ -80,15 +80,15 @@ function updateLinkList(link, type) {
|
|||
v-if="!isLoading && listComplete"
|
||||
class="flex items-center justify-center text-sm"
|
||||
>
|
||||
No more links
|
||||
{{ $t('links.no_more') }}
|
||||
</div>
|
||||
<div
|
||||
v-if="listError"
|
||||
class="flex items-center justify-center text-sm"
|
||||
>
|
||||
Loading links failed,
|
||||
{{ $t('links.load_failed') }}
|
||||
<Button variant="link" @click="getLinks">
|
||||
Try again
|
||||
{{ $t('common.try_again') }}
|
||||
</Button>
|
||||
</div>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ const props = defineProps({
|
|||
})
|
||||
const emit = defineEmits(['update:link'])
|
||||
|
||||
const { t } = useI18n()
|
||||
const editPopoverOpen = ref(false)
|
||||
|
||||
const { host, origin } = location
|
||||
|
|
@ -31,6 +32,11 @@ function updateLink(link, type) {
|
|||
emit('update:link', link, type)
|
||||
editPopoverOpen.value = false
|
||||
}
|
||||
|
||||
function copyLink() {
|
||||
copy(shortLink)
|
||||
toast(t('links.copy_success'))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -69,7 +75,7 @@ function updateLink(link, type) {
|
|||
<Copy
|
||||
v-else
|
||||
class="w-4 h-4 ml-1 shrink-0"
|
||||
@click.prevent="copy(shortLink);toast('Copy successful!')"
|
||||
@click.prevent="copyLink"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -134,7 +140,7 @@ function updateLink(link, type) {
|
|||
<SquarePen
|
||||
class="w-5 h-5 mr-2"
|
||||
/>
|
||||
Edit
|
||||
{{ $t('common.edit') }}
|
||||
</div>
|
||||
</DashboardLinksEditor>
|
||||
|
||||
|
|
@ -149,7 +155,7 @@ function updateLink(link, type) {
|
|||
>
|
||||
<Eraser
|
||||
class="w-5 h-5 mr-2"
|
||||
/> Delete
|
||||
/> {{ $t('common.delete') }}
|
||||
</div>
|
||||
</DashboardLinksDelete>
|
||||
</PopoverContent>
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ onMounted(() => {
|
|||
</div>
|
||||
<Button variant="outline" @click="downloadQRCode">
|
||||
<Download class="w-4 h-4 mr-2" />
|
||||
Download QR Code
|
||||
{{ $t('links.download_qr_code') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -59,8 +59,8 @@ onMounted(() => {
|
|||
size="sm"
|
||||
class="relative justify-start w-full h-10 bg-background text-muted-foreground sm:w-32 md:w-48"
|
||||
>
|
||||
<span class="hidden md:inline-flex">Search Links...</span>
|
||||
<span class="inline-flex md:hidden">Search</span>
|
||||
<span class="hidden md:inline-flex">{{ $t('links.search_placeholder') }}</span>
|
||||
<span class="inline-flex md:hidden">{{ $t('common.search') }}</span>
|
||||
<kbd class="pointer-events-none absolute right-[0.3rem] top-[0.6rem] hidden h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 sm:flex">
|
||||
<span class="text-xs">⌘</span>K
|
||||
</kbd>
|
||||
|
|
@ -68,12 +68,12 @@ onMounted(() => {
|
|||
</TriggerTemplate>
|
||||
<SearchTemplate>
|
||||
<Command v-model:search-term="searchTerm" v-model="selectedLink" class="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
<CommandInput placeholder="Type to search..." />
|
||||
<CommandList :class="{ 'max-h-none': !isDesktop }">
|
||||
<CommandInput :placeholder="$t('links.search_placeholder')" />
|
||||
<CommandList>
|
||||
<CommandEmpty v-if="searchTerm">
|
||||
No links found.
|
||||
{{ $t('links.no_results') }}
|
||||
</CommandEmpty>
|
||||
<CommandGroup heading="Links">
|
||||
<CommandGroup :heading="$t('links.group_title')">
|
||||
<CommandItem v-for="link in filteredLinks" :key="link.item?.id" class="cursor-pointer" :value="link.item" @select="selectLink(link.item)">
|
||||
<div class="flex gap-1 w-full">
|
||||
<div class="inline-flex overflow-hidden flex-1 gap-1 items-center">
|
||||
|
|
|
|||
|
|
@ -1,44 +1,39 @@
|
|||
<script setup>
|
||||
import pluralize from 'pluralize'
|
||||
|
||||
defineProps({
|
||||
tabs: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
rawTabs: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
function type2name(type) {
|
||||
if (['os'].includes(type))
|
||||
return type.toUpperCase()
|
||||
|
||||
return pluralize(type.replace(type[0], type[0].toUpperCase()))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Tabs
|
||||
:default-value="tabs[0]"
|
||||
:default-value="rawTabs[0]"
|
||||
class="flex flex-col"
|
||||
>
|
||||
<TabsList class="w-fit">
|
||||
<TabsTrigger
|
||||
v-for="tab in tabs"
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="tab"
|
||||
:value="tab"
|
||||
:value="rawTabs[index]"
|
||||
>
|
||||
{{ type2name(tab) }}
|
||||
{{ tab }}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent
|
||||
v-for="tab in tabs"
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="tab"
|
||||
:value="tab"
|
||||
:value="rawTabs[index]"
|
||||
class="flex-1"
|
||||
>
|
||||
<DashboardMetricsMetric
|
||||
:type="tab"
|
||||
:name="type2name(tab)"
|
||||
:type="rawTabs[index]"
|
||||
:name="tab"
|
||||
class="h-full"
|
||||
/>
|
||||
</TabsContent>
|
||||
|
|
|
|||
|
|
@ -1,25 +1,50 @@
|
|||
<script setup>
|
||||
const { t } = useI18n()
|
||||
|
||||
const tabs = {
|
||||
location: ['country', 'region', 'city'],
|
||||
referer: ['referer', 'slug'],
|
||||
time: ['language', 'timezone'],
|
||||
device: ['device', 'deviceType'],
|
||||
browser: ['os', 'browser', 'browserType'],
|
||||
}
|
||||
|
||||
const translatedTabs = computed(() => ({
|
||||
location: tabs.location.map(tab => t(`dashboard.metrics.${tab}`)),
|
||||
referer: tabs.referer.map(tab => t(`dashboard.metrics.${tab}`)),
|
||||
time: tabs.time.map(tab => t(`dashboard.metrics.${tab}`)),
|
||||
device: tabs.device.map(tab => t(`dashboard.metrics.${tab}`)),
|
||||
browser: tabs.browser.map(tab => t(`dashboard.metrics.${tab}`)),
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="grid gap-8 lg:grid-cols-12">
|
||||
<LazyDashboardMetricsLocations class="col-span-1 lg:col-span-8" />
|
||||
<DashboardMetricsGroup
|
||||
class="lg:col-span-4"
|
||||
:tabs="['country', 'region', 'city']"
|
||||
:tabs="translatedTabs.location"
|
||||
:raw-tabs="tabs.location"
|
||||
/>
|
||||
<DashboardMetricsGroup
|
||||
class="lg:col-span-6"
|
||||
:tabs="['referer', 'slug']"
|
||||
:tabs="translatedTabs.referer"
|
||||
:raw-tabs="tabs.referer"
|
||||
/>
|
||||
<DashboardMetricsGroup
|
||||
class="lg:col-span-6"
|
||||
:tabs="['language', 'timezone']"
|
||||
:tabs="translatedTabs.time"
|
||||
:raw-tabs="tabs.time"
|
||||
/>
|
||||
<DashboardMetricsGroup
|
||||
class="lg:col-span-6"
|
||||
:tabs="['device', 'deviceType']"
|
||||
:tabs="translatedTabs.device"
|
||||
:raw-tabs="tabs.device"
|
||||
/>
|
||||
<DashboardMetricsGroup
|
||||
class="lg:col-span-6"
|
||||
:tabs="['os', 'browser', 'browserType']"
|
||||
:tabs="translatedTabs.browser"
|
||||
:raw-tabs="tabs.browser"
|
||||
/>
|
||||
</main>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -21,12 +21,12 @@ defineProps({
|
|||
<div
|
||||
class="h-12 px-4 font-medium text-left align-middle text-muted-foreground "
|
||||
>
|
||||
Name
|
||||
{{ $t('dashboard.name') }}
|
||||
</div>
|
||||
<div
|
||||
class="h-12 px-4 font-medium text-right align-middle text-muted-foreground"
|
||||
>
|
||||
Count
|
||||
{{ $t('dashboard.count') }}
|
||||
</div>
|
||||
</div>
|
||||
<VList
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ const Tooltip = {
|
|||
<template>
|
||||
<Card class="flex flex-col md:h-[500px]">
|
||||
<CardHeader>
|
||||
<CardTitle>Locations</CardTitle>
|
||||
<CardTitle>{{ $t('dashboard.locations') }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="flex-1 flex [&_[data-radix-aspect-ratio-wrapper]]:flex-1">
|
||||
<AspectRatio :ratio="65 / 30">
|
||||
|
|
|
|||
|
|
@ -75,9 +75,8 @@ onBeforeUnmount(() => {
|
|||
<Button
|
||||
variant="link"
|
||||
>
|
||||
<Maximize
|
||||
class="mr-2 w-4 h-4"
|
||||
/> DETAILS
|
||||
<Maximize class="w-4 h-4 mr-2" />
|
||||
{{ $t('dashboard.details') }}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent class="max-w-[95svw] max-h-[95svh] md:max-w-screen-md grid-rows-[auto_minmax(0,1fr)_auto]">
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ function formatName(name, type) {
|
|||
v-else
|
||||
class="w-full truncate"
|
||||
>
|
||||
{{ formatName(name, type) || '(None)' }}
|
||||
{{ formatName(name, type) || $t('dashboard.none') }}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent v-if="name">
|
||||
|
|
|
|||
|
|
@ -3,18 +3,18 @@
|
|||
class="flex flex-col items-center max-w-4xl p-8 mx-auto my-12 text-center bg-black rounded-lg md:px-20 md:py-20"
|
||||
>
|
||||
<h2 class="text-4xl tracking-tight text-white md:text-6xl">
|
||||
Deployment immediately
|
||||
{{ $t('home.cta.title') }}
|
||||
</h2>
|
||||
<p class="mt-4 text-lg text-slate-400 md:text-xl">
|
||||
With just a few simple clicks, you can start deploying without any expenses.
|
||||
{{ $t('home.cta.description') }}
|
||||
</p>
|
||||
<div class="flex mt-5">
|
||||
<HomeLink
|
||||
href="https://github.com/ccbikai/sink?tab=readme-ov-file#%EF%B8%8F-deployment"
|
||||
type="inverted"
|
||||
title="Start Deploy"
|
||||
:title="$t('home.cta.button')"
|
||||
>
|
||||
Start Deploy
|
||||
{{ $t('home.cta.button') }}
|
||||
</HomeLink>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,41 +1,36 @@
|
|||
<script setup>
|
||||
import { AreaChart, Hourglass, Link, Paintbrush, ServerOff, Sparkles } from 'lucide-vue-next'
|
||||
|
||||
const features = ref([
|
||||
const { t } = useI18n()
|
||||
const features = computed(() => [
|
||||
{
|
||||
title: 'URL Shortening',
|
||||
description:
|
||||
'Compress your URLs to their minimal length.',
|
||||
title: t('home.features.url_shortening.title'),
|
||||
description: t('home.features.url_shortening.description'),
|
||||
icon: Link,
|
||||
},
|
||||
{
|
||||
title: 'Analytics',
|
||||
description:
|
||||
'Monitor link analytics and gather insightful statistics.',
|
||||
title: t('home.features.analytics.title'),
|
||||
description: t('home.features.analytics.description'),
|
||||
icon: AreaChart,
|
||||
},
|
||||
{
|
||||
title: 'Serverless',
|
||||
description:
|
||||
'Deploy without the need for traditional servers.',
|
||||
title: t('home.features.serverless.title'),
|
||||
description: t('home.features.serverless.description'),
|
||||
icon: ServerOff,
|
||||
},
|
||||
{
|
||||
title: 'Customizable Slug',
|
||||
description:
|
||||
'Support for personalized slugs and case sensitivity.',
|
||||
title: t('home.features.customizable_slug.title'),
|
||||
description: t('home.features.customizable_slug.description'),
|
||||
icon: Paintbrush,
|
||||
},
|
||||
{
|
||||
title: 'AI Slug',
|
||||
description:
|
||||
'Leverage AI to generate slugs.',
|
||||
title: t('home.features.ai_slug.title'),
|
||||
description: t('home.features.ai_slug.description'),
|
||||
icon: Sparkles,
|
||||
},
|
||||
{
|
||||
title: 'Link Expiration',
|
||||
description:
|
||||
'Set expiration dates for your links.',
|
||||
title: t('home.features.link_expiration.title'),
|
||||
description: t('home.features.link_expiration.description'),
|
||||
icon: Hourglass,
|
||||
},
|
||||
])
|
||||
|
|
@ -45,10 +40,10 @@ const features = ref([
|
|||
<main class="pt-16 md:py-12">
|
||||
<div class="md:pb-12">
|
||||
<h2 class="text-4xl font-bold lg:text-5xl lg:tracking-tight">
|
||||
Features
|
||||
{{ $t('home.features.title') }}
|
||||
</h2>
|
||||
<p class="my-8 text-lg md:mb-0 text-slate-600">
|
||||
Simple and sufficient functionality
|
||||
{{ $t('home.features.subtitle') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ const { title, description, github } = useAppConfig()
|
|||
<AreaChart
|
||||
class="w-5 h-5"
|
||||
/>
|
||||
Dashboard
|
||||
{{ $t('dashboard.title') }}
|
||||
</HomeLink>
|
||||
<HomeLink
|
||||
size="lg"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="mt-16 md:mt-0 md:py-12 ">
|
||||
<h2 class="font-bold text-center text-slate-600">
|
||||
Built with awesome technologies
|
||||
{{ $t('home.logos.title') }}
|
||||
</h2>
|
||||
<div class="flex flex-wrap items-center justify-center gap-8 mt-10 md:gap-20">
|
||||
<img
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ const { twitter } = useAppConfig()
|
|||
orientation="vertical"
|
||||
class="h-4"
|
||||
/>
|
||||
<span>Follow me on X(Twitter)</span>
|
||||
<span>{{ $t('home.twitter.follow') }}</span>
|
||||
<ArrowRight class="w-4 h-4" />
|
||||
</a>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ const { title, email, telegram, blog, twitter, mastodon, github } = useAppConfig
|
|||
title="Email"
|
||||
class="text-gray-400 hover:text-gray-500"
|
||||
>
|
||||
<span class="sr-only">Email</span>
|
||||
<span class="sr-only">{{ $t('layouts.footer.social.email') }}</span>
|
||||
<GmailIcon
|
||||
class="w-6 h-6"
|
||||
/>
|
||||
|
|
@ -41,7 +41,7 @@ const { title, email, telegram, blog, twitter, mastodon, github } = useAppConfig
|
|||
title="Telegram"
|
||||
class="text-gray-400 hover:text-gray-500"
|
||||
>
|
||||
<span class="sr-only">Telegram</span>
|
||||
<span class="sr-only">{{ $t('layouts.footer.social.telegram') }}</span>
|
||||
<TelegramIcon
|
||||
class="w-6 h-6"
|
||||
/>
|
||||
|
|
@ -53,7 +53,7 @@ const { title, email, telegram, blog, twitter, mastodon, github } = useAppConfig
|
|||
title="Blog"
|
||||
class="text-gray-400 hover:text-gray-500"
|
||||
>
|
||||
<span class="sr-only">Blog</span>
|
||||
<span class="sr-only">{{ $t('layouts.footer.social.blog') }}</span>
|
||||
<BloggerIcon
|
||||
class="w-6 h-6"
|
||||
/>
|
||||
|
|
@ -66,7 +66,7 @@ const { title, email, telegram, blog, twitter, mastodon, github } = useAppConfig
|
|||
title="Twitter"
|
||||
class="text-gray-400 hover:text-gray-500"
|
||||
>
|
||||
<span class="sr-only">Twitter</span>
|
||||
<span class="sr-only">{{ $t('layouts.footer.social.twitter') }}</span>
|
||||
<XIcon
|
||||
class="w-6 h-6"
|
||||
/>
|
||||
|
|
@ -79,7 +79,7 @@ const { title, email, telegram, blog, twitter, mastodon, github } = useAppConfig
|
|||
title="Mastodon"
|
||||
class="text-gray-400 hover:text-gray-500"
|
||||
>
|
||||
<span class="sr-only">Mastodon</span>
|
||||
<span class="sr-only">{{ $t('layouts.footer.social.mastodon') }}</span>
|
||||
<MastodonIcon
|
||||
class="w-6 h-6"
|
||||
/>
|
||||
|
|
@ -92,7 +92,7 @@ const { title, email, telegram, blog, twitter, mastodon, github } = useAppConfig
|
|||
title="GitHub"
|
||||
class="text-gray-400 hover:text-gray-500"
|
||||
>
|
||||
<span class="sr-only">GitHub</span>
|
||||
<span class="sr-only">{{ $t('layouts.footer.social.github') }}</span>
|
||||
<GitHubIcon
|
||||
class="w-6 h-6"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,15 @@ import SwitchTheme from '../SwitchTheme.vue'
|
|||
|
||||
const showMenu = ref(false)
|
||||
const { title, github } = useAppConfig()
|
||||
|
||||
const nuxtApp = useNuxtApp()
|
||||
const i18n = nuxtApp.$i18n
|
||||
const { setLocale, locales } = useI18n()
|
||||
const currentLocale = ref(i18n.locale.value)
|
||||
|
||||
watch(currentLocale, (newLocale) => {
|
||||
setLocale(newLocale)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -58,13 +67,13 @@ const { title, github } = useAppConfig()
|
|||
</a>
|
||||
<div class="w-full mx-4" />
|
||||
<div
|
||||
class="flex flex-col items-start justify-end w-full pt-4 md:items-center md:w-1/3 md:flex-row md:py-0"
|
||||
class="flex flex-col items-start justify-end w-full pt-4 md:items-center md:w-3/5 md:flex-row md:py-0"
|
||||
>
|
||||
<a
|
||||
class="w-full px-6 py-2 mr-0 text-gray-700 cursor-pointer dark:text-gray-300 md:px-3 md:mr-2 lg:mr-3 md:w-auto"
|
||||
href="/dashboard"
|
||||
:title="`${title} Dashboard`"
|
||||
>Dashboard</a>
|
||||
>{{ $t('dashboard.title') }}</a>
|
||||
<a
|
||||
:href="github"
|
||||
target="_blank"
|
||||
|
|
@ -75,6 +84,17 @@ const { title, github } = useAppConfig()
|
|||
class="w-5 h-5 mr-1"
|
||||
/>
|
||||
GitHub</a>
|
||||
<Tabs v-model="currentLocale" class="md:ml-2 lg:ml-3">
|
||||
<TabsList>
|
||||
<TabsTrigger
|
||||
v-for="locale in locales"
|
||||
:key="locale.code"
|
||||
:value="locale.code"
|
||||
>
|
||||
{{ locale.language }}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<span class="ml-1">
|
||||
<SwitchTheme />
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { AlertCircle } from 'lucide-vue-next'
|
|||
import { toast } from 'vue-sonner'
|
||||
import { z } from 'zod'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const LoginSchema = z.object({
|
||||
token: z.string().describe('SiteToken'),
|
||||
})
|
||||
|
|
@ -25,7 +27,7 @@ async function onSubmit(form) {
|
|||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
toast.error('Login failed, please try again.', {
|
||||
toast.error(t('login.failed'), {
|
||||
description: e.message,
|
||||
})
|
||||
}
|
||||
|
|
@ -36,10 +38,10 @@ async function onSubmit(form) {
|
|||
<Card class="w-full max-w-sm">
|
||||
<CardHeader>
|
||||
<CardTitle class="text-2xl">
|
||||
Login
|
||||
{{ $t('login.title') }}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your site token to login.
|
||||
{{ $t('login.description') }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="grid gap-4">
|
||||
|
|
@ -51,13 +53,13 @@ async function onSubmit(form) {
|
|||
>
|
||||
<Alert v-if="previewMode">
|
||||
<AlertCircle class="w-4 h-4" />
|
||||
<AlertTitle>Tips</AlertTitle>
|
||||
<AlertTitle>{{ $t('login.tips') }}</AlertTitle>
|
||||
<AlertDescription>
|
||||
The site token for preview mode is <code class="font-mono text-green-500">SinkCool</code> .
|
||||
{{ $t('login.preview_token') }} <code class="font-mono text-green-500">SinkCool</code> .
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<Button class="w-full">
|
||||
Login
|
||||
{{ $t('login.submit') }}
|
||||
</Button>
|
||||
</AutoForm>
|
||||
</CardContent>
|
||||
|
|
|
|||
11
i18n/i18n.config.ts
Normal file
11
i18n/i18n.config.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { currentLocales } from './i18n'
|
||||
|
||||
export default defineI18nConfig(() => {
|
||||
return {
|
||||
legacy: false,
|
||||
availableLocales: currentLocales.map(l => l.code),
|
||||
fallbackLocale: 'zh-CN',
|
||||
fallbackWarn: true,
|
||||
missingWarn: true,
|
||||
}
|
||||
})
|
||||
28
i18n/i18n.ts
Normal file
28
i18n/i18n.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import type { LocaleObject } from '@nuxtjs/i18n'
|
||||
|
||||
const locales: LocaleObject[] = [
|
||||
{
|
||||
code: 'zh-CN',
|
||||
file: 'zh-CN.json',
|
||||
name: '简体中文',
|
||||
language: '简',
|
||||
},
|
||||
{
|
||||
code: 'en-US',
|
||||
file: 'en-US.json',
|
||||
name: 'English',
|
||||
language: 'En',
|
||||
},
|
||||
]
|
||||
|
||||
function buildLocales() {
|
||||
const useLocales = Object.values(locales).reduce((acc, data) => {
|
||||
acc.push(data)
|
||||
|
||||
return acc
|
||||
}, <LocaleObject[]>[])
|
||||
|
||||
return useLocales.sort((a, b) => a.code.localeCompare(b.code))
|
||||
}
|
||||
|
||||
export const currentLocales = buildLocales()
|
||||
161
i18n/locales/en-US.json
Normal file
161
i18n/locales/en-US.json
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
{
|
||||
"common": {
|
||||
"i18n": "Multi Language",
|
||||
"abbreviations": "En",
|
||||
"current_language": "English",
|
||||
"try_again": "Try again",
|
||||
"cancel": "Cancel",
|
||||
"continue": "Continue",
|
||||
"close": "Close",
|
||||
"save": "Save",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"delete_success": "Delete successful!",
|
||||
"search": "Search"
|
||||
},
|
||||
"theme": {
|
||||
"toggle": "Toggle theme",
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"system": "System"
|
||||
},
|
||||
"login": {
|
||||
"title": "Login",
|
||||
"description": "Enter your site token to login",
|
||||
"tips": "Tips",
|
||||
"preview_token": "The site token for preview mode is",
|
||||
"submit": "Login",
|
||||
"failed": "Login failed, please try again"
|
||||
},
|
||||
"logout": {
|
||||
"title": "LogOut?",
|
||||
"confirm": "Are you sure you want to log out?",
|
||||
"action": "LogOut"
|
||||
},
|
||||
"layouts": {
|
||||
"footer": {
|
||||
"copyright": "Products of HTML.ZONE",
|
||||
"social": {
|
||||
"email": "Email",
|
||||
"telegram": "Telegram",
|
||||
"blog": "Blog",
|
||||
"twitter": "Twitter",
|
||||
"mastodon": "Mastodon",
|
||||
"github": "GitHub"
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
"select_language": "Select Language"
|
||||
}
|
||||
},
|
||||
"nav": {
|
||||
"links": "Links",
|
||||
"analysis": "Analysis"
|
||||
},
|
||||
"home": {
|
||||
"features": {
|
||||
"title": "Features",
|
||||
"subtitle": "Simple and sufficient functionality",
|
||||
"url_shortening": {
|
||||
"title": "URL Shortening",
|
||||
"description": "Compress your URLs to their minimal length"
|
||||
},
|
||||
"analytics": {
|
||||
"title": "Analytics",
|
||||
"description": "Monitor link analytics and gather insightful statistics"
|
||||
},
|
||||
"serverless": {
|
||||
"title": "Serverless",
|
||||
"description": "Deploy without the need for traditional servers"
|
||||
},
|
||||
"customizable_slug": {
|
||||
"title": "Customizable Slug",
|
||||
"description": "Support for personalized slugs and case sensitivity"
|
||||
},
|
||||
"ai_slug": {
|
||||
"title": "AI Slug",
|
||||
"description": "Leverage AI to generate slugs"
|
||||
},
|
||||
"link_expiration": {
|
||||
"title": "Link Expiration",
|
||||
"description": "Set expiration dates for your links"
|
||||
}
|
||||
},
|
||||
"cta": {
|
||||
"title": "Deployment immediately",
|
||||
"description": "With just a few simple clicks, you can start deploying without any expenses",
|
||||
"button": "Start Deploy"
|
||||
},
|
||||
"logos": {
|
||||
"title": "Built with awesome technologies"
|
||||
},
|
||||
"twitter": {
|
||||
"follow": "Follow me on X(Twitter)"
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"visits": "Visits",
|
||||
"views": "Views",
|
||||
"visitors": "Visitors",
|
||||
"referers": "Referers",
|
||||
"stats": "'s Stats",
|
||||
"custom_date": "Custom Date",
|
||||
"date": "Date",
|
||||
"date_range": "Date Range",
|
||||
"locations": "Locations",
|
||||
"details": "Details",
|
||||
"name": "Name",
|
||||
"count": "Count",
|
||||
"loading": "Loading",
|
||||
"no_data": "No Data",
|
||||
"none": "(None)",
|
||||
"date_picker": {
|
||||
"today": "Today",
|
||||
"last_24h": "Last 24 hours",
|
||||
"this_week": "This week",
|
||||
"last_7d": "Last 7 days",
|
||||
"this_month": "This month",
|
||||
"last_30d": "Last 30 days",
|
||||
"last_90d": "Last 90 days",
|
||||
"custom": "Custom",
|
||||
"custom_title": "Custom Date",
|
||||
"single_date": "Date",
|
||||
"date_range": "Date Range"
|
||||
},
|
||||
"metrics": {
|
||||
"country": "Country",
|
||||
"region": "Region",
|
||||
"city": "City",
|
||||
"referer": "Referer",
|
||||
"slug": "Slug",
|
||||
"language": "Language",
|
||||
"timezone": "Timezone",
|
||||
"device": "Device",
|
||||
"deviceType": "Device Type",
|
||||
"os": "OS",
|
||||
"browser": "Browser",
|
||||
"browserType": "Browser Type"
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"create": "Create Link",
|
||||
"edit": "Edit Link",
|
||||
"delete_confirm_title": "Are you absolutely sure?",
|
||||
"delete_confirm_desc": "This action cannot be undone. This will really delete your link from servers.",
|
||||
"delete_success": "Delete successful!",
|
||||
"update_success": "Link updated successfully",
|
||||
"create_success": "Link created successfully",
|
||||
"preview_mode_tip": "The preview mode link is valid for up to 24 hours.",
|
||||
"search_placeholder": "Search links...",
|
||||
"no_results": "No links found.",
|
||||
"group_title": "Links",
|
||||
"copy_success": "Copy successful!",
|
||||
"created_at": "Created At",
|
||||
"updated_at": "Updated At",
|
||||
"expires_at": "Expires At",
|
||||
"no_more": "No more links",
|
||||
"load_failed": "Loading links failed,",
|
||||
"download_qr_code": "Download QR Code"
|
||||
}
|
||||
}
|
||||
161
i18n/locales/zh-CN.json
Normal file
161
i18n/locales/zh-CN.json
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
{
|
||||
"common": {
|
||||
"i18n": "多语言",
|
||||
"abbreviations": "简",
|
||||
"current_language": "简体中文",
|
||||
"try_again": "重试",
|
||||
"cancel": "取消",
|
||||
"continue": "继续",
|
||||
"close": "关闭",
|
||||
"save": "保存",
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
"delete_success": "删除成功!",
|
||||
"search": "搜索"
|
||||
},
|
||||
"theme": {
|
||||
"toggle": "切换主题",
|
||||
"light": "浅色",
|
||||
"dark": "深色",
|
||||
"system": "跟随系统"
|
||||
},
|
||||
"login": {
|
||||
"title": "登录",
|
||||
"description": "请输入站点令牌以登录",
|
||||
"tips": "提示",
|
||||
"preview_token": "预览模式的站点令牌是",
|
||||
"submit": "登录",
|
||||
"failed": "登录失败,请重试"
|
||||
},
|
||||
"logout": {
|
||||
"title": "退出登录?",
|
||||
"confirm": "确定要退出登录吗?",
|
||||
"action": "退出登录"
|
||||
},
|
||||
"layouts": {
|
||||
"footer": {
|
||||
"copyright": "产品来自 HTML.ZONE",
|
||||
"social": {
|
||||
"email": "邮箱",
|
||||
"telegram": "Telegram",
|
||||
"blog": "博客",
|
||||
"twitter": "Twitter",
|
||||
"mastodon": "Mastodon",
|
||||
"github": "GitHub"
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
"select_language": "选择语言"
|
||||
}
|
||||
},
|
||||
"nav": {
|
||||
"links": "链接",
|
||||
"analysis": "分析"
|
||||
},
|
||||
"home": {
|
||||
"features": {
|
||||
"title": "功能特点",
|
||||
"subtitle": "简单而实用的功能",
|
||||
"url_shortening": {
|
||||
"title": "URL 缩短",
|
||||
"description": "将您的 URL 压缩到最小长度"
|
||||
},
|
||||
"analytics": {
|
||||
"title": "数据分析",
|
||||
"description": "监控链接分析并收集有价值的统计数据"
|
||||
},
|
||||
"serverless": {
|
||||
"title": "无服务器",
|
||||
"description": "无需传统服务器即可部署"
|
||||
},
|
||||
"customizable_slug": {
|
||||
"title": "自定义短链",
|
||||
"description": "支持个性化短链和大小写敏感"
|
||||
},
|
||||
"ai_slug": {
|
||||
"title": "AI 短链",
|
||||
"description": "利用 AI 生成短链"
|
||||
},
|
||||
"link_expiration": {
|
||||
"title": "链接过期",
|
||||
"description": "为您的链接设置过期日期"
|
||||
}
|
||||
},
|
||||
"cta": {
|
||||
"title": "立即部署",
|
||||
"description": "只需几个简单的点击,您就可以免费开始部署",
|
||||
"button": "开始部署"
|
||||
},
|
||||
"logos": {
|
||||
"title": "使用优秀的技术构建"
|
||||
},
|
||||
"twitter": {
|
||||
"follow": "在 X(Twitter) 上关注我"
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "仪表盘",
|
||||
"visits": "访问量",
|
||||
"views": "浏览量",
|
||||
"visitors": "访客数",
|
||||
"referers": "引荐来源",
|
||||
"stats": "的统计",
|
||||
"custom_date": "自定义日期",
|
||||
"date": "日期",
|
||||
"date_range": "日期范围",
|
||||
"locations": "地理位置",
|
||||
"details": "详情",
|
||||
"name": "名称",
|
||||
"count": "数量",
|
||||
"loading": "加载中",
|
||||
"no_data": "暂无数据",
|
||||
"none": "(无)",
|
||||
"date_picker": {
|
||||
"today": "今天",
|
||||
"last_24h": "最近24小时",
|
||||
"this_week": "本周",
|
||||
"last_7d": "最近7天",
|
||||
"this_month": "本月",
|
||||
"last_30d": "最近30天",
|
||||
"last_90d": "最近90天",
|
||||
"custom": "自定义",
|
||||
"custom_title": "自定义日期",
|
||||
"single_date": "单个日期",
|
||||
"date_range": "日期范围"
|
||||
},
|
||||
"metrics": {
|
||||
"country": "国家",
|
||||
"region": "地区",
|
||||
"city": "城市",
|
||||
"referer": "来源",
|
||||
"slug": "短链",
|
||||
"language": "语言",
|
||||
"timezone": "时区",
|
||||
"device": "设备",
|
||||
"deviceType": "设备类型",
|
||||
"os": "操作系统",
|
||||
"browser": "浏览器",
|
||||
"browserType": "浏览器类型"
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"create": "创建链接",
|
||||
"edit": "编辑链接",
|
||||
"delete_confirm_title": "确定要删除吗?",
|
||||
"delete_confirm_desc": "此操作无法撤销。这将从服务器中永久删除您的链接。",
|
||||
"delete_success": "删除成功!",
|
||||
"update_success": "链接更新成功",
|
||||
"create_success": "链接创建成功",
|
||||
"preview_mode_tip": "预览模式下的链接最多有效24小时。",
|
||||
"search_placeholder": "搜索链接...",
|
||||
"no_results": "未找到链接。",
|
||||
"group_title": "链接列表",
|
||||
"copy_success": "复制成功!",
|
||||
"created_at": "创建时间",
|
||||
"updated_at": "更新时间",
|
||||
"expires_at": "过期时间",
|
||||
"no_more": "没有更多链接",
|
||||
"load_failed": "加载链接失败,",
|
||||
"download_qr_code": "下载二维码"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import { currentLocales } from './i18n/i18n'
|
||||
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
|
||||
|
|
@ -7,6 +9,7 @@ export default defineNuxtConfig({
|
|||
'@nuxt/eslint',
|
||||
'@nuxtjs/tailwindcss',
|
||||
'@nuxtjs/color-mode',
|
||||
'@nuxtjs/i18n',
|
||||
],
|
||||
devtools: { enabled: true },
|
||||
|
||||
|
|
@ -69,4 +72,21 @@ export default defineNuxtConfig({
|
|||
standalone: false,
|
||||
},
|
||||
},
|
||||
|
||||
i18n: {
|
||||
locales: currentLocales,
|
||||
compilation: {
|
||||
strictMessage: false,
|
||||
escapeHtml: true,
|
||||
},
|
||||
lazy: true,
|
||||
strategy: 'no_prefix',
|
||||
detectBrowserLanguage: {
|
||||
useCookie: true,
|
||||
cookieKey: 'sink_i18n_redirected',
|
||||
redirectOn: 'root',
|
||||
},
|
||||
baseUrl: '/',
|
||||
defaultLocale: 'en-US',
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@
|
|||
"vaul-vue": "^0.3.0",
|
||||
"vee-validate": "^4.15.0",
|
||||
"virtua": "^0.39.2",
|
||||
"vue": "^3.5.13",
|
||||
"vue-sonner": "^1.3.0",
|
||||
"vue3-simple-icons": "^13.2.0",
|
||||
"zod": "^3.24.1"
|
||||
|
|
@ -48,6 +49,7 @@
|
|||
"@nuxt/eslint-config": "^0.7.4",
|
||||
"@nuxthub/core": "0.7.11",
|
||||
"@nuxtjs/color-mode": "^3.5.2",
|
||||
"@nuxtjs/i18n": "^9.0.0",
|
||||
"@nuxtjs/tailwindcss": "^6.12.2",
|
||||
"@vueuse/integrations": "^12.2.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
|
|
|
|||
716
pnpm-lock.yaml
716
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue